Safe Variable Time Step Main Loop in Ammo.js
Implementing physics in web applications using ammo.js (a WebAssembly
port of the Bullet Physics engine) requires a robust simulation loop.
While fixed time steps are ideal for physics stability, real-world
browser frame rates fluctuate due to rendering loads and hardware
differences. This article explains how to safely implement a variable
time step in your ammo.js main loop by configuring the
stepSimulation parameters correctly to prevent physics
jitter, slow-downs, and object tunneling.
Understanding stepSimulation
The core of safe time stepping in ammo.js lies in the
dynamicsWorld.stepSimulation method. This method takes
three parameters:
dynamicsWorld.stepSimulation(timeStep, maxSubSteps, fixedTimeStep);timeStep: The actual elapsed time (in seconds) since the last frame. This is your variable delta time.maxSubSteps: The maximum number of internal fixed steps ammo.js is allowed to take in a single frame to catch up with the elapsed real time.fixedTimeStep: The duration of each internal simulation step. By default, this is1 / 60(about 16.67 milliseconds).
The Mechanics of Safe Stepping
Ammo.js resolves the conflict between variable frame rates and physics stability by running a fixed-step simulation inside a variable-step game loop.
When you pass the variable timeStep to
stepSimulation, ammo.js accumulates this time. It then runs
as many internal steps of size fixedTimeStep as necessary
to match the accumulated time. If there is leftover time that does not
fit into a full fixedTimeStep, ammo.js automatically
interpolates the positions of the physics bodies for smooth
rendering.
Implementation Guide
To implement this safely, use the following pattern in your application loop:
let lastTime = performance.now();
const fixedTimeStep = 1 / 60; // 60Hz physics step
const maxSubSteps = 10; // Prevents the "spiral of death"
function tick() {
requestAnimationFrame(tick);
const currentTime = performance.now();
// Calculate delta time in seconds
let deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
// 1. Cap the maximum delta time
// If the tab loses focus or lags severely, this prevents
// the physics engine from attempting to simulate hundreds of steps at once.
if (deltaTime > 0.25) {
deltaTime = 0.25;
}
// 2. Step the simulation
// Ammo.js will run up to 10 substeps of 1/60s to cover the deltaTime.
dynamicsWorld.stepSimulation(deltaTime, maxSubSteps, fixedTimeStep);
// 3. Update your graphics/render engine
updateGraphicsTransforms();
renderer.render(scene, camera);
}
requestAnimationFrame(tick);Critical Rules for Stability
To ensure the simulation remains stable and performant, adhere to the following rules:
- Never set
maxSubStepsto 0: SettingmaxSubStepsto 0 forces ammo.js to use a completely variable time step size equal totimeStep. This disables internal fixed substeps, leading to unstable physics behavior, clipping through walls, and unpredictable constraint behavior. - Ensure
maxSubSteps * fixedTimeStep >= deltaTime: Under normal running conditions, the maximum time covered by your substeps must be greater than your frame delta time. For a 60Hz physics step (0.0166s) and a 60FPS target frame rate,maxSubStepsshould be at least2to handle minor frame drops. Setting it to10provides a safe buffer for heavier lag spikes. - Clamp the input Delta Time: When a user switches
browser tabs,
requestAnimationFramepauses. When they return,deltaTimecan be several seconds long. Without clampingdeltaTime(e.g., capping it at0.25seconds), ammo.js will attempt to calculate hundreds of frames of physics in a single render step, freezing the browser in a performance loop known as the “spiral of death.”