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:

  • Zero Mass or Inertia Misconfigurations: Setting a dynamic body's mass to zero without properly calculating its local inertia.
  • Zero-Length Vector Operations: Normalizing a vector with a length of zero (e.g., applying a force direction based on two objects occupying the exact same coordinate).
  • Extremely Large Forces or Impulses: Applying a massive force in a single simulation step, causing the velocity to exceed floating-point limits.
  • Time Step Spikes: Passing a massive delta time (dt) to the step simulation function during frame drops or background tab switching.

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:

  • All rotation quaternions are normalized using .normalize() before being applied to a btTransform.
  • User-input forces or impulses are clamped to a safe maximum limit.
  • Vector calculations (such as custom gravitational pull or wind forces) check for division-by-zero errors before dividing by the distance.

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.