How to Fix Ammo.js NaN Physics Explosions

In 3D web development, sudden physics “explosions” where objects disappear or fly off-screen are often caused by NaN (Not a Number) values propagating through the Ammo.js physics engine. This article explains why these errors occur, how to systematically isolate the root cause of the calculation breakdown, and how to implement defensive coding practices to prevent simulation crashes.

Common Causes of NaN Values in Ammo.js

Ammo.js is a direct WebAssembly/asm.js port of the C++ Bullet Physics engine. When a mathematical operation results in an undefined state, the underlying engine outputs NaN. Once a single coordinate, velocity, or rotation vector becomes NaN, it rapidly infects the entire physics pipeline, causing immediate simulation collapse. The most common triggers include:

Step 1: Isolate the Triggering Object

To find which object is causing the crash, write a utility function to monitor your physics bodies. In your main update loop, right before and after calling stepSimulation(), traverse your active rigid bodies and check their transform matrices and linear/angular velocities for NaN values.

function checkNaN(rigidBody) {
    const transform = rigidBody.getWorldTransform();
    const origin = transform.getOrigin();
    if (isNaN(origin.x()) || isNaN(origin.y()) || isNaN(origin.z())) {
        return true;
    }
    const linVel = rigidBody.getLinearVelocity();
    if (isNaN(linVel.x()) || isNaN(linVel.y()) || isNaN(linVel.z())) {
        return true;
    }
    return false;
}

Print a warning and pause the rendering loop the exact millisecond checkNaN returns true. This allows you to inspect the call stack and identify the offending object.

Step 2: Sanitize Inputs to the Physics World

Never pass unverified JavaScript data directly to Ammo.js bindings. Ensure that:

Step 3: Implement Safe Time Stepping

If a browser tab goes out of focus, the time delta can spike from 16ms to several seconds. When passed to Ammo.js, this causes objects to move massive distances in a single frame, resulting in deep interpenetration and explosive ejection forces.

Fix this by clamping your maximum step size and using fixed substepping:

const maxSubSteps = 10;
const fixedTimeStep = 1 / 60; // 16.67ms

// Clamp delta time to a maximum of 0.1 seconds to prevent spikes
let dt = Math.min(clock.getDelta(), 0.1); 

physicsWorld.stepSimulation(dt, maxSubSteps, fixedTimeStep);

Step 4: Clamp Velocities

To prevent runaway numerical integration, limit the maximum velocity of your rigid bodies. This stops exponential force accumulation before it leads to a math overflow.

const maxVelocity = 100; // Define a safe speed limit for your world
const velocity = body.getLinearVelocity();
const speed = Math.sqrt(
    velocity.x() * velocity.x() + 
    velocity.y() * velocity.y() + 
    velocity.z() * velocity.z()
);

if (speed > maxVelocity) {
    const ratio = maxVelocity / speed;
    velocity.setValue(
        velocity.x() * ratio,
        velocity.y() * ratio,
        velocity.z() * ratio
    );
    body.setLinearVelocity(velocity);
}

Implementing these checks creates a robust boundary around the WebAssembly memory space, ensuring Ammo.js remains stable even under extreme gameplay conditions.