User Safety: safe

Summary

We observed a visual discrepancy between two objects moving at the same velocity. One object used a fixed physics timestep with temporal interpolation, while the other used a variable frame-rate-dependent update. The result was that the interpolated object appeared to have motion blur or “ghosting,” while the variable-rate object appeared crisp but suffered from jitter (temporal aliasing). This postmortem analyzes the decoupling of simulation time from render time.

Root Cause

The issue stems from a fundamental mismatch between the Simulation State and the Visual Representation.

  • Fixed Timestep Logic: The physics engine updates in discrete chunks (e.g., 60Hz). Because the render loop (e.g., 144Hz) runs faster than the physics loop, there is always a “remainder” of time in the accumulator.
  • Interpolation Lag: To prevent jitter, we interpolate between last_position and current_position using an alpha value. This means the object being rendered is actually a visual representation of a state between two past physics steps.
  • The “Blur” Illusion: The motion blur is not actual pixel smearing; it is temporal aliasing. The interpolation creates a smooth transition, but because the object is technically being drawn at a position that hasn’t “happened” in the physics world yet, it loses the crisp alignment with the discrete physics steps, appearing “off” to the eye compared to the raw, un-interpolated movement.
  • The Variable Update Flaw: The second object updates using deltaTime directly. While this looks “crisp,” it is mathematically unstable because deltaTime fluctuates, leading to inconsistent velocity and physical inaccuracies.

Why This Happens in Real Systems

In high-performance graphics and simulation, we face a conflict between determinism and smoothness:

  • Determinism Requirements: Physics engines (like Box2D or PhysX) require fixed steps to ensure stability. If a collision happens at $t=1.0$, you cannot simulate it at $t=1.00001$ reliably.
  • Hardware Variance: Monitors run at different refresh rates (60Hz, 144Hz, 240Hz). A simulation tied to the frame rate will run at different speeds on different hardware.
  • The Decoupling Gap: When we decouple Update() from Draw(), we introduce a temporal gap. Interpolation fills this gap visually, but it introduces a one-frame latency to the visual position.

Real-World Impact

  • Gameplay Feel: In fast-paced shooters, the perceived latency between a player’s input and the visual feedback (caused by interpolation lag) can make the game feel “heavy” or “floaty.”
  • Visual Artifacts: In high-fidelity simulations, the discrepancy between interpolated visual meshes and non-interpolated collision bounds can cause objects to appear to pass through walls or float above surfaces.
  • Physics Instability: Attempting to solve the “blur” by removing interpolation leads to micro-stuttering, which is often more jarring to users than the perceived blur.

Example or Code

const step = 1 / 60; // Fixed physics step
let acc = 0; 
let pos1; // Physics-driven position
let lastPos1; // Previous physics state

function draw() {
  background(220);
  acc += deltaTime / 1000;

  lastPos1 = pos1.copy(); // Store state before update

  // Fixed Timestep Loop
  while(acc >= step) {
    pos1.x += 100 * step; 
    acc -= step;
  }

  // The Interpolation Coefficient (Alpha)
  const alpha = acc / step;

  // Visual Position = (Current * alpha) + (Previous * (1 - alpha))
  const interpPos = p5.Vector.lerp(lastPos1, pos1, alpha);

  // Render the interpolated (smooth but "laggy") object
  rect(interpPos.x, interpPos.y, 200, 50);
}

How Senior Engineers Fix It

A senior engineer recognizes that “blur” is often a symptom of temporal mismatch. To solve this, we use the following strategies:

  • Extrapolation instead of Interpolation: Instead of rendering between last_state and current_state (which introduces lag), we project the position forward using the current velocity: pos_render = pos_current + velocity * alpha. This removes the latency but can cause “overshoot” if the physics state changes abruptly.
  • Sub-stepping: Increasing the physics frequency (e.g., from 60Hz to 240Hz) reduces the error margin of the interpolation, making the “blur” virtually imperceptible.
  • Motion Vectors: In modern engines (Unreal/Unity), we don’t just move the object; we generate Motion Vectors. These vectors tell the GPU exactly how much a pixel moved between frames, allowing the Temporal Anti-Aliasing (TAA) hardware to resolve the motion correctly without artificial blur.
  • State Buffering: Maintaining a circular buffer of previous states to allow for more complex Hermite spline interpolation rather than simple linear interpolation (LERP).

Why Juniors Miss It

  • Confusing Smoothness with Accuracy: Juniors often assume that if it “looks smooth,” the math is correct. They fail to realize that a smooth line can be mathematically decoupled from the actual simulation state.
  • Ignoring the Accumulator: Many beginners try to pass deltaTime directly into physics equations. They don’t realize this makes the simulation non-deterministic, meaning the same inputs will yield different results on different machines.
  • Overlooking Latency: A junior might implement interpolation to fix jitter but will not notice that they have introduced a ~16ms-33ms visual delay between the physics engine’s truth and the user’s screen.

Leave a Comment