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_positionandcurrent_positionusing analphavalue. 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
deltaTimedirectly. While this looks “crisp,” it is mathematically unstable becausedeltaTimefluctuates, 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()fromDraw(), 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_stateandcurrent_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
deltaTimedirectly 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.