Skip to main content
The Hamilton filter uses quaternion mathematics to provide smooth, natural rotations without gimbal lock artifacts. It applies Slerp (Spherical Linear Interpolation) for rotations and Lerp (Linear Interpolation) for translations.
This filter is ideal for users who want smooth, cinematic head movements and don’t mind some lag in exchange for artifact-free rotation paths.

How It Works

The Hamilton filter treats rotations and translations separately:
  • Rotations: Converted to quaternions, smoothed using Slerp interpolation
  • Translations: Smoothed using standard linear interpolation

Quaternion Advantage

Unlike Euler angle filters, quaternion interpolation:
  • ✅ No gimbal lock at 90° pitch
  • ✅ Smooth rotation along the shortest path
  • ✅ No axis order dependency
  • ✅ Natural feel for large rotations

Algorithm

The filter uses distance-based alpha calculation with power curves:

Rotation Smoothing

// Convert input to quaternion
tQuat quat_input = QuatFromYPR(&input[Yaw]);

// Calculate angle between current and input quaternion
double angle = AngleBetween(quat_input, quat_last);

// Calculate alpha from angle distance
alpha = (angle - rot_deadzone) / (rot_max + rot_deadzone + EPSILON);
alpha = clamp(alpha, 0.0, 1.0);

if (alpha > 0.0)
    alpha = pow(alpha, rot_pow + rot_zoom);

// Scale alpha to prevent overshoot
alpha *= (angle - rot_deadzone) / (angle + EPSILON);

// Slerp quaternions
quat_last = Slerp(quat_last, quat_input, alpha);

Translation Smoothing

// Calculate distance from current to input position
double dist = VectorDistance(&input[TX], pos_last);

// Calculate alpha from position distance
alpha = (dist - pos_deadzone) / (pos_max + pos_deadzone + EPSILON);
alpha = clamp(alpha, 0.0, 1.0);

if (alpha > 0.0)
    alpha = pow(alpha, pos_pow);

// Scale alpha to prevent overshoot
alpha *= (dist - pos_deadzone) / (dist + EPSILON);

// Lerp positions
pos_last = Lerp(pos_last, input, alpha);
The alpha scaling (distance - deadzone) / (distance + EPSILON) ensures that the center of the deadzone never moves closer to the input position than the distance. This prevents the view from jumping ahead of head movements.

Parameters

The Hamilton filter has 8 parameters split between rotation and translation:

Rotation Parameters

max_radius_smoothing
slider
default:"0.01"
Maximum rotation angle for full smoothing (degrees).
  • Lower values: More aggressive smoothing, slower response
  • Higher values: Less smoothing, faster response
This defines the distance at which alpha reaches 1.0 (no smoothing).
smoothing_power_rot
slider
default:"0.01"
Power curve exponent for rotation smoothing.
  • Lower values (< 1.0): More linear response
  • Higher values (> 1.0): Keeps heavy smoothing longer, opens up late
Applied as: alpha = pow(alpha, rot_pow + rot_zoom)
dead_zone_radius_rot
slider
default:"0.01"
Rotation deadzone in degrees. Movements smaller than this are ignored.
  • Too low: Visible jitter when trying to hold still
  • Too high: Small intentional movements feel unresponsive

Translation Parameters

max_distance_smoothing
slider
default:"0.01"
Maximum translation distance for full smoothing (centimeters).
  • Lower values: More smoothing, slower positional response
  • Higher values: Less smoothing, faster positional response
smoothing_power_dist
slider
default:"0.01"
Power curve exponent for translation smoothing.
  • Lower values: More linear response
  • Higher values: Keeps heavy smoothing longer
dead_zone_radius_dist
slider
default:"0.01"
Translation deadzone in centimeters. Movements smaller than this are ignored.

Zoom-Dependent Rotation

smoothing_power_zoom
slider
default:"0.01"
Additional rotation power when leaning back (negative Z).This creates more smoothing when you lean back, useful for reducing shake when observing distant objects.
max_z
slider
default:"0.01"
Maximum Z distance (backward lean) for full zoom smoothing effect.The zoom contribution is calculated as:
if (output[TZ] > 0) rot_zoom = 0;  // leaning forward, no effect
else rot_zoom = pow_zoom * (-output[TZ]) / (max_z + EPSILON);
rot_zoom = min(rot_zoom, pow_zoom);

Tuning Guide

For Smooth Cinematic Movement

Maximize smoothing for video recording or demos:
max_radius_smoothing: 25.0
smoothing_power_rot: 2.5
dead_zone_radius_rot: 0.05

max_distance_smoothing: 20.0
smoothing_power_dist: 2.0
dead_zone_radius_dist: 0.2

smoothing_power_zoom: 1.5
max_z: 30.0
  • High max radius/distance = smoothing applied over large movements
  • High power curves = keeps smoothing strong even during motion
  • Moderate deadzones = eliminates jitter without feeling sticky
  • Zoom smoothing = extra stability when leaning back to look at distance

For Responsive Gaming

Minimize lag while keeping quaternion benefits:
max_radius_smoothing: 5.0
smoothing_power_rot: 0.5
dead_zone_radius_rot: 0.02

max_distance_smoothing: 8.0
smoothing_power_dist: 0.5
dead_zone_radius_dist: 0.1

smoothing_power_zoom: 0.5
max_z: 15.0
  • Lower max radius/distance = less smoothing overall
  • Low power curves (< 1.0) = more linear, immediate response
  • Small deadzones = don’t suppress intentional micro-adjustments
  • Reduced zoom smoothing = maintain responsiveness when leaning back

Balanced (General Purpose)

max_radius_smoothing: 10.0
smoothing_power_rot: 1.2
dead_zone_radius_rot: 0.03

max_distance_smoothing: 12.0
smoothing_power_dist: 1.0
dead_zone_radius_dist: 0.15

smoothing_power_zoom: 1.0
max_z: 20.0

Understanding Slerp vs Lerp

Slerp (Spherical Linear Interpolation)

Used for quaternion rotations:
quat_last = Slerp(quat_last, quat_input, alpha);
  • Interpolates along the shortest arc on a 4D unit sphere
  • Maintains constant angular velocity
  • Prevents gimbal lock and axis flipping
  • More expensive computationally than Lerp

Lerp (Linear Interpolation)

Used for position vectors:
pos_last = Lerp(pos_last, input, alpha);
  • Simple weighted average: output = (1-alpha)*start + alpha*end
  • Sufficient for Cartesian coordinates
  • Very fast computation
The quaternion representation is internal only. Input and output remain as Yaw/Pitch/Roll Euler angles for compatibility with OpenTrack.

Deadzone Behavior

The Hamilton filter implements “soft” deadzones:
alpha = (distance - deadzone) / (max_distance + deadzone + EPSILON);
alpha = clamp(alpha, 0.0, 1.0);
Within deadzone (distance < deadzone):
  • alpha becomes negative, clamped to 0
  • No interpolation occurs
  • Output stays at current position
Just outside deadzone (distance ≈ deadzone):
  • alpha is very small
  • Slow interpolation begins
  • Gradual transition, not a hard cut
Far outside deadzone (distance >> deadzone):
  • alpha increases toward 1.0
  • Faster interpolation
  • Response speed depends on power curve
Setting deadzones too high can create a “sticky” feeling where small movements don’t register. Start small and increase only if jitter is visible.

Zoom-Dependent Smoothing

The Hamilton filter includes a unique feature: rotation smoothing increases when leaning backward:
const double pow_zoom = s.kPowZoom;
const double max_z = s.kMaxZ;
double rot_zoom = pow_zoom;

if (output[TZ] > 0) rot_zoom = 0;  // leaning forward
else rot_zoom *= -output[TZ] / (max_z + EPSILON);  // leaning back
rot_zoom = fmin(rot_zoom, pow_zoom);

// Apply to rotation power
alpha = pow(alpha, rot_pow + rot_zoom);
Use case: When you lean back to look at distant objects, this feature adds extra rotational smoothing to reduce shake, mimicking how you naturally stabilize your head when focusing on far targets.
Set smoothing_power_zoom to 0.001 (minimum) to effectively disable this feature.

Comparison to Other Filters

FeatureHamiltonAlpha SpectrumEWMAAccela
Rotation methodQuaternion SlerpPer-axis EMAPer-axis EMAPer-axis spline
Gimbal lock✅ Impossible⚠️ Possible⚠️ Possible⚠️ Possible
Rotation pathShortest arcAxis-independentAxis-independentAxis-independent
Auto adaptation❌ No✅ Yes✅ Yes⚠️ Velocity-based
Parameters88+34
CPU usageLowMediumLowLow
Best forCinematicAdvanced tuningAuto adaptationFast action
Choose Hamilton if:
  • You want artifact-free rotation interpolation
  • You need consistent smoothing regardless of head orientation
  • You’re making videos or demos and want cinematic movement
  • You don’t need automatic noise adaptation
Choose something else if:
  • You want automatic smoothing adjustment (use EWMA)
  • You need minimal latency for competitive gaming (use Accela)
  • You want advanced predictive filtering (use Alpha Spectrum)

Troubleshooting

  • Decrease max_radius_smoothing to 5.0 or lower
  • Decrease smoothing_power_rot to 0.5 or lower
  • Reduce dead_zone_radius_rot to minimum (0.001)
  • Increase max_radius_smoothing to 15.0 or higher
  • Increase smoothing_power_rot to 2.0 or higher
  • Slightly increase dead_zone_radius_rot to 0.05-0.1
  • Check tracker mounting and ensure no external vibration
This is normal - the filter uses separate parameters for rotation vs translation.Adjust translation parameters independently:
  • max_distance_smoothing
  • smoothing_power_dist
  • dead_zone_radius_dist
Deadzones are probably set too high:
  • Rotation deadzone should typically be < 0.1°
  • Translation deadzone should typically be < 0.3 cm
Try cutting your current deadzone values in half.
Set smoothing_power_zoom to minimum (0.001) to disable the zoom-dependent rotation smoothing feature.

Implementation Details

Quaternion Conversion

Input Euler angles are converted to quaternions:
tQuat quat_input = QuatFromYPR(&input[Yaw]);
After Slerp interpolation, output is converted back:
QuatToYPR(quat_last, &output[Yaw]);
This conversion happens every frame but is computationally cheap compared to the benefits of Slerp interpolation.

Angle Calculation

double angle = AngleBetween(quat_input, quat_last);
This uses the quaternion dot product to find the angle between two orientations:
angle = 2 * acos(abs(dot(q1, q2)))
The result is always the shortest rotation angle between the two orientations.

Alpha Scaling

The line:
alpha *= (distance - deadzone) / (distance + EPSILON);
is critical. It ensures:
  • Alpha is scaled down proportionally to deadzone
  • The filtered output never “jumps ahead” of the input
  • Smooth transitions when entering/exiting deadzone

Code Reference

Relevant source files:
  • Implementation: filter-hamilton/ftnoir_filter_hamilton.cpp
  • Header/settings: filter-hamilton/ftnoir_filter_hamilton.h
  • Dialog: filter-hamilton/ftnoir_filter_hamilton_dialog.cpp
  • Quaternion utilities: compat/hamilton-tools.h

Key Functions

Rotation filtering (ftnoir_filter_hamilton.cpp:61-77):
double angle = AngleBetween(quat_input, quat_last);
alpha = (angle - rot_deadzone) / (rot_max + rot_deadzone + EPSILON);
alpha = std::min(1.0, std::max(0.0, alpha));
if (alpha > 0.0)
    alpha = pow(alpha, rot_pow + rot_zoom);
alpha *= (angle - rot_deadzone) / (angle + EPSILON);
quat_last = Slerp(quat_last, quat_input, alpha);
Position filtering (ftnoir_filter_hamilton.cpp:29-46):
double dist = VectorDistance(&input[TX], pos_last);
alpha = (dist - pos_deadzone) / (pos_max + pos_deadzone + EPSILON);
alpha = std::min(1.0, std::max(0.0, alpha));
if (alpha > 0.0)
    alpha = pow(alpha, pos_pow);
alpha *= (dist - pos_deadzone) / (dist + EPSILON);
pos_last = Lerp(pos_last, input, alpha);

Next Steps

Filter Overview

Compare all available filters

Alpha Spectrum

Advanced adaptive filter

EWMA Filter

Automatic noise-adaptive filtering

Accela Filter

Velocity-dependent acceleration filter