Ammo.js Internal Tick Callback for Custom Drag Profiles
This article explains how to implement custom, velocity-dependent drag profiles on rigid bodies in Ammo.js by leveraging the physics world’s internal tick callback. You will learn why this callback is necessary for accurate physics, how to write a custom quadratic drag formula, and how to register the callback within the WebAssembly environment of Ammo.js without causing memory leaks.
Why Use the Internal Tick Callback?
Ammo.js (a JavaScript port of the Bullet physics engine) calculates physics in small, discrete time intervals called substeps. If you apply forces to a rigid body inside your main JavaScript game loop, those forces are only applied once per frame. This approach misses the internal substeps, leading to inconsistent physics behaviors, frame-rate dependency, and jitter.
By registering a callback via setInternalTickCallback,
you run your custom drag logic directly before
(isPreTick = true) or after
(isPreTick = false) each internal physics simulation
substep. For applying forces like aerodynamic drag, you must use a
pre-tick callback so the forces are integrated into the current physics
step.
Step 1: Define the Drag Logic
Standard linear damping in Ammo.js is a simple exponential decay. Real-world fluid resistance (drag) is typically quadratic, meaning the force increases with the square of the velocity (\(F_d = \frac{1}{2} \rho v^2 C_d A\)).
To apply this custom drag, you need to: 1. Retrieve the rigid body’s current linear velocity. 2. Calculate the speed (magnitude of the velocity vector). 3. Compute the drag force vector acting in the opposite direction of the velocity. 4. Apply the resulting force to the body.
Step 2: Implement the Callback Code
Below is the implementation of the drag calculation inside the internal tick callback.
// Array to keep track of rigid bodies requiring custom drag profiles
const activeDragBodies = [];
function customDragCallback(worldPtr, timeStep) {
for (let i = 0; i < activeDragBodies.length; i++) {
const body = activeDragBodies[i];
// Skip calculation if the body is sleeping to save performance
if (!body.isActive()) continue;
const velocity = body.getLinearVelocity();
const vx = velocity.x();
const vy = velocity.y();
const vz = velocity.z();
const speed = Math.sqrt(vx * vx + vy * vy + vz * vz);
// Avoid division by zero for stationary bodies
if (speed > 0.0001) {
// Drag coefficient combining fluid density, drag coefficient, and reference area
const dragCoeff = body.customDragCoeff || 0.47;
// Quadratic drag formula: F = dragCoeff * speed^2
const dragMagnitude = dragCoeff * speed * speed;
// Calculate directional forces (opposite to velocity direction)
const forceX = -(vx / speed) * dragMagnitude;
const forceY = -(vy / speed) * dragMagnitude;
const forceZ = -(vz / speed) * dragMagnitude;
// Apply force to the center of mass
const dragForce = new Ammo.btVector3(forceX, forceY, forceZ);
body.applyCentralForce(dragForce);
// Clean up WebAssembly memory allocation
Ammo.destroy(dragForce);
}
}
}Step 3: Register the Callback with Ammo.js
Because Ammo.js runs in WebAssembly/C++, you cannot pass a standard
JavaScript function directly to the engine. You must wrap the JavaScript
function using Ammo.addFunction to create a functional
pointer that the WebAssembly runtime can execute.
// 1. Wrap the JS function for WebAssembly
const tickCallbackPointer = Ammo.addFunction(customDragCallback);
// 2. Register the callback with the dynamics world
// The third parameter (true) registers this as a "pre-tick" callback
physicsWorld.setInternalTickCallback(tickCallbackPointer, 0, true);Memory Management and Performance
When implementing custom drag profiles, keep the following performance considerations in mind:
- Memory Leaks: Every temporary vector created using
new Ammo.btVector3()inside the callback must be explicitly destroyed usingAmmo.destroy(vector)before the function execution scope ends. Failure to do so will quickly crash the browser tab due to WebAssembly memory exhaustion. - Sleeping State: Always check
body.isActive(). Applying forces to a resting rigid body can inadvertently wake it up, preventing the physics engine from putting idle objects to sleep and degrading overall performance.