How to Use Ammo.js Internal Tick Callbacks

This article explains how to set up and use custom JavaScript callback functions within the ammo.js physics engine during its internal simulation ticks. You will learn why these callbacks are necessary for precise physics manipulation, how to register them using the dynamics world API, and how to differentiate between pre-tick and post-tick execution.

Yes, you can set up custom JavaScript callback functions that fire during the internal ticks of ammo.js. Because ammo.js is a direct port of the Bullet Physics library, it inherits the capability to execute user-defined code at each internal physics substep rather than just once per render frame.

Why Internal Tick Callbacks are Necessary

In a typical web application, the physics world is updated once per render frame using dynamicsWorld.stepSimulation(timeStep). However, to maintain simulation accuracy, ammo.js may split a single frame’s time step into multiple internal substeps (ticks).

If you apply forces or modify velocities in your main render loop, those changes only occur once per frame. To apply continuous forces (like gravity zones or wind) or to read collision data precisely at every physics calculation, you must hook into the internal substeps using tick callbacks.

The setInternalTickCallback Method

To register a callback, you use the setInternalTickCallback method available on the btDynamicsWorld object.

Syntax

dynamicsWorld.setInternalTickCallback(callbackPointer, userPersistentData, isPreTick);

Step-by-Step Implementation

Below is a practical example of how to implement and register a post-tick callback in JavaScript.

1. Create the Callback Function

The callback function receives two arguments from the physics engine: a pointer to the dynamics world and the duration of the current physics time step.

function myTickCallback(worldPtr, timeStep) {
    // Wrap the raw pointer back into an Ammo object to use it
    const world = Ammo.wrapPointer(worldPtr, Ammo.btDynamicsWorld);
    
    // Your custom physics logic goes here
    // For example, applying a constant upward force to a specific body
}

2. Wrap the Function with Emscripten

Because ammo.js runs in WebAssembly/asm.js, you must register the JavaScript function with Emscripten’s function table. The signature string 'vif' corresponds to: * v: void (return type) * i: int / pointer (first argument: worldPtr) * f: float (second argument: timeStep)

const callbackPointer = Ammo.addFunction(myTickCallback, 'vif');

3. Register the Callback with the Dynamics World

Pass the wrapped function pointer to your dynamics world.

// Register as a post-tick callback (runs after each physics substep)
dynamicsWorld.setInternalTickCallback(callbackPointer, 0, false);

If you need the logic to run before the physics engine calculates collisions and forces for that substep, set the third parameter to true:

// Register as a pre-tick callback (runs before each physics substep)
dynamicsWorld.setInternalTickCallback(callbackPointer, 0, true);

Cleaning Up

To prevent memory leaks when destroying your physics world or changing scenes, you should free the allocated Emscripten function pointer:

// Remove the callback from the world
dynamicsWorld.setInternalTickCallback(null);

// Free the function pointer from Emscripten memory
Ammo.removeFunction(callbackPointer);