> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/opentrack/opentrack/llms.txt
> Use this file to discover all available pages before exploring further.

# Hamilton Filter

> Quaternion-based filter using Slerp interpolation for smooth, artifact-free rotations with deadzone control

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.

<Note>
  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.
</Note>

## 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

```mermaid theme={null}
graph LR
    A[Yaw/Pitch/Roll Input] --> B[Convert to Quaternion]
    B --> C[Calculate Angle]
    C --> D{Angle > Deadzone?}
    D -->|Yes| E[Slerp to Input]
    D -->|No| F[Stay at Current]
    E --> G[Convert to YPR Output]
    F --> G
```

## Algorithm

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

### Rotation Smoothing

```cpp theme={null}
// 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

```cpp theme={null}
// 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);
```

<Warning>
  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.
</Warning>

## Parameters

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

### Rotation Parameters

<ParamField path="max_radius_smoothing" type="slider" default="0.01" range="[0.001, 25.0]">
  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).
</ParamField>

<ParamField path="smoothing_power_rot" type="slider" default="0.01" range="[0.001, 4.0]">
  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)`
</ParamField>

<ParamField path="dead_zone_radius_rot" type="slider" default="0.01" range="[0.001, 0.5]" unit="degrees">
  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
</ParamField>

### Translation Parameters

<ParamField path="max_distance_smoothing" type="slider" default="0.01" range="[0.001, 20.0]" unit="cm">
  Maximum translation distance for full smoothing (centimeters).

  * **Lower values**: More smoothing, slower positional response
  * **Higher values**: Less smoothing, faster positional response
</ParamField>

<ParamField path="smoothing_power_dist" type="slider" default="0.01" range="[0.001, 4.0]">
  Power curve exponent for translation smoothing.

  * **Lower values**: More linear response
  * **Higher values**: Keeps heavy smoothing longer
</ParamField>

<ParamField path="dead_zone_radius_dist" type="slider" default="0.01" range="[0.001, 0.5]" unit="cm">
  Translation deadzone in centimeters. Movements smaller than this are ignored.
</ParamField>

### Zoom-Dependent Rotation

<ParamField path="smoothing_power_zoom" type="slider" default="0.01" range="[0.001, 4.0]">
  Additional rotation power when leaning back (negative Z).

  This creates more smoothing when you lean back, useful for reducing shake when observing distant objects.
</ParamField>

<ParamField path="max_z" type="slider" default="0.01" range="[0.001, 100.0]" unit="cm">
  Maximum Z distance (backward lean) for full zoom smoothing effect.

  The zoom contribution is calculated as:

  ```cpp theme={null}
  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);
  ```
</ParamField>

## Tuning Guide

### For Smooth Cinematic Movement

Maximize smoothing for video recording or demos:

```yaml theme={null}
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
```

<Accordion title="Why these settings?">
  * 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
</Accordion>

### For Responsive Gaming

Minimize lag while keeping quaternion benefits:

```yaml theme={null}
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
```

<Accordion title="Why these settings?">
  * 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
</Accordion>

### Balanced (General Purpose)

```yaml theme={null}
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:

```cpp theme={null}
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:

```cpp theme={null}
pos_last = Lerp(pos_last, input, alpha);
```

* Simple weighted average: `output = (1-alpha)*start + alpha*end`
* Sufficient for Cartesian coordinates
* Very fast computation

<Note>
  The quaternion representation is internal only. Input and output remain as Yaw/Pitch/Roll Euler angles for compatibility with OpenTrack.
</Note>

## Deadzone Behavior

The Hamilton filter implements "soft" deadzones:

```cpp theme={null}
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

<Warning>
  Setting deadzones too high can create a "sticky" feeling where small movements don't register. Start small and increase only if jitter is visible.
</Warning>

## Zoom-Dependent Smoothing

The Hamilton filter includes a unique feature: rotation smoothing increases when leaning backward:

```cpp theme={null}
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.

<Accordion title="Disable zoom smoothing">
  Set `smoothing_power_zoom` to `0.001` (minimum) to effectively disable this feature.
</Accordion>

## Comparison to Other Filters

| Feature         | Hamilton         | Alpha Spectrum   | EWMA             | Accela            |
| --------------- | ---------------- | ---------------- | ---------------- | ----------------- |
| Rotation method | Quaternion Slerp | Per-axis EMA     | Per-axis EMA     | Per-axis spline   |
| Gimbal lock     | ✅ Impossible     | ⚠️ Possible      | ⚠️ Possible      | ⚠️ Possible       |
| Rotation path   | Shortest arc     | Axis-independent | Axis-independent | Axis-independent  |
| Auto adaptation | ❌ No             | ✅ Yes            | ✅ Yes            | ⚠️ Velocity-based |
| Parameters      | 8                | 8+               | 3                | 4                 |
| CPU usage       | Low              | Medium           | Low              | Low               |
| Best for        | Cinematic        | Advanced tuning  | Auto adaptation  | Fast action       |

<Accordion title="When to choose Hamilton">
  **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)
</Accordion>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Rotations feel sluggish">
    * 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)
  </Accordion>

  <Accordion title="Still seeing rotation jitter">
    * 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
  </Accordion>

  <Accordion title="Translations feel different than rotations">
    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`
  </Accordion>

  <Accordion title="Deadzone feels sticky">
    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.
  </Accordion>

  <Accordion title="Extra smoothing when leaning back is unwanted">
    Set `smoothing_power_zoom` to minimum (0.001) to disable the zoom-dependent rotation smoothing feature.
  </Accordion>
</AccordionGroup>

## Implementation Details

### Quaternion Conversion

Input Euler angles are converted to quaternions:

```cpp theme={null}
tQuat quat_input = QuatFromYPR(&input[Yaw]);
```

After Slerp interpolation, output is converted back:

```cpp theme={null}
QuatToYPR(quat_last, &output[Yaw]);
```

This conversion happens every frame but is computationally cheap compared to the benefits of Slerp interpolation.

### Angle Calculation

```cpp theme={null}
double angle = AngleBetween(quat_input, quat_last);
```

This uses the quaternion dot product to find the angle between two orientations:

```cpp theme={null}
angle = 2 * acos(abs(dot(q1, q2)))
```

The result is always the shortest rotation angle between the two orientations.

### Alpha Scaling

The line:

```cpp theme={null}
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`):

```cpp theme={null}
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`):

```cpp theme={null}
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

<CardGroup cols={2}>
  <Card title="Filter Overview" icon="list" href="/filters/overview">
    Compare all available filters
  </Card>

  <Card title="Alpha Spectrum" icon="brain" href="/filters/alpha-spectrum">
    Advanced adaptive filter
  </Card>

  <Card title="EWMA Filter" icon="chart-line" href="/filters/ewma">
    Automatic noise-adaptive filtering
  </Card>

  <Card title="Accela Filter" icon="gauge" href="/filters/accela">
    Velocity-dependent acceleration filter
  </Card>
</CardGroup>
