How to Interpolate Rendering in Matter.js
Decoupling your physics update rate from your display refresh rate is essential for achieving smooth visual motion in Matter.js. When physics runs at a fixed delta time (such as 60Hz) while the screen renders at variable or higher refresh rates (such as 120Hz or 144Hz), bodies can appear to jitter. This article outlines how to implement a fixed-timestep game loop, track previous body states, and compute an interpolation factor to smoothly render Matter.js bodies between physics updates.
The Accumulator Pattern
To interpolate rendering, you must run physics at a deterministic,
fixed interval while letting the render loop run as fast as the monitor
refreshes via requestAnimationFrame.
An accumulator stores elapsed real-world time. Whenever the accumulator exceeds your fixed physics step duration, you advance the physics engine by that exact interval and subtract the duration from the accumulator. Any remaining time represents the fractional progress toward the next physics step—this fraction is your interpolation factor (\(\alpha\)).
Tracking Body States
Interpolation requires knowing where a body was at the previous physics step and where it is currently. Matter.js does not store previous positions automatically for rendering purposes, so you must store them manually before updating the engine.
Attach a custom property to each body:
function trackBodyState(body) {
body.renderState = {
previousPosition: { x: body.position.x, y: body.position.y },
previousAngle: body.angle,
currentPosition: { x: body.position.x, y: body.position.y },
currentAngle: body.angle
};
}Implementing the Loop
Replace the default Matter.Runner with a custom
requestAnimationFrame loop.
const engine = Matter.Engine.create();
const fixedDelta = 1000 / 60; // 60 updates per second (approx. 16.66ms)
let accumulator = 0;
let lastTime = performance.now();
function gameLoop(currentTime) {
requestAnimationFrame(gameLoop);
let frameTime = currentTime - lastTime;
lastTime = currentTime;
// Prevent spiral of death on lag spikes
if (frameTime > 250) {
frameTime = 250;
}
accumulator += frameTime;
// Fixed physics updates
while (accumulator >= fixedDelta) {
// Save current positions as previous before updating
const bodies = Matter.Composite.allBodies(engine.world);
for (let i = 0; i < bodies.length; i++) {
const body = bodies[i];
if (!body.renderState) trackBodyState(body);
body.renderState.previousPosition.x = body.position.x;
body.renderState.previousPosition.y = body.position.y;
body.renderState.previousAngle = body.angle;
}
// Advance simulation
Matter.Engine.update(engine, fixedDelta);
// Record the new current positions
for (let i = 0; i < bodies.length; i++) {
const body = bodies[i];
body.renderState.currentPosition.x = body.position.x;
body.renderState.currentPosition.y = body.position.y;
body.renderState.currentAngle = body.angle;
}
accumulator -= fixedDelta;
}
// Alpha is the progress toward the next frame [0.0, 1.0)
const alpha = accumulator / fixedDelta;
render(alpha);
}
requestAnimationFrame(gameLoop);Calculating Render Transforms
When rendering with a custom Canvas, Pixi.js, or Three.js pipeline, calculate the visual position using linear interpolation (lerp):
\[\text{Render Position} = \text{Previous} + (\text{Current} - \text{Previous}) \times \alpha\]
function lerp(start, end, alpha) {
return start + (end - start) * alpha;
}
function render(alpha) {
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
const bodies = Matter.Composite.allBodies(engine.world);
for (let i = 0; i < bodies.length; i++) {
const body = bodies[i];
if (!body.renderState) continue;
// Calculate interpolated values
const renderX = lerp(body.renderState.previousPosition.x, body.renderState.currentPosition.x, alpha);
const renderY = lerp(body.renderState.previousPosition.y, body.renderState.currentPosition.y, alpha);
const renderAngle = lerp(body.renderState.previousAngle, body.renderState.currentAngle, alpha);
// Draw the body using interpolated values
context.save();
context.translate(renderX, renderY);
context.rotate(renderAngle);
// Draw body geometry relative to its origin
context.beginPath();
const vertices = body.vertices;
context.moveTo(vertices[0].x - body.position.x, vertices[0].y - body.position.y);
for (let j = 1; j < vertices.length; j++) {
context.lineTo(vertices[j].x - body.position.x, vertices[j].y - body.position.y);
}
context.closePath();
context.fillStyle = '#2c3e50';
context.fill();
context.restore();
}
}Important Considerations
- Disable Built-in Rendering: The default
Matter.Renderis designed to draw raw physics bodies without interpolation. Avoid usingMatter.Render.run()and draw the scenes manually via custom canvas code or external rendering libraries. - Rotational Wrapping: When interpolating angles that cross boundaries (e.g., from \(-\pi\) to \(\pi\)), compute the shortest angular distance before interpolating to avoid bodies rapidly spinning backwards.
- Sleeping Bodies: Check
body.isSleeping. Skip interpolating stationary or sleeping bodies to reduce overhead.