How to Implement Custom Friction in Ammo.js
This article explains how to override the default friction calculations in Ammo.js (the WebAssembly/JavaScript port of the Bullet physics engine) using a custom friction model. By default, Ammo.js calculates combined friction by multiplying the friction coefficients of two colliding bodies. To implement custom behaviors, such as averaging friction coefficients or applying material-specific friction lookup tables, you must leverage Bullet’s contact callback system using Emscripten’s function wrapping.
Step 1: Enable Custom Material Callbacks on Rigid Bodies
To tell the physics engine to bypass its default friction mixing
formula for specific objects, you must flag those rigid bodies with the
CF_CUSTOM_MATERIAL_CALLBACK collision flag.
// Retrieve existing collision flags and append the custom material callback flag
const CF_CUSTOM_MATERIAL_CALLBACK = 8; // Bullet constant value
const bodyA = new Ammo.btRigidBody(...);
const bodyB = new Ammo.btRigidBody(...);
bodyA.setCollisionFlags(bodyA.getCollisionFlags() | CF_CUSTOM_MATERIAL_CALLBACK);
bodyB.setCollisionFlags(bodyB.getCollisionFlags() | CF_CUSTOM_MATERIAL_CALLBACK);Step 2: Define the Custom Contact Callback
In Bullet, when two objects collide and at least one has the
CF_CUSTOM_MATERIAL_CALLBACK flag set, the engine fires a
global callback. You must write a JavaScript function that intercepts
this event, accesses the contact manifold point, and applies your custom
mathematical model to calculate the combined friction.
function customMaterialCombiner(cpPtr, colObj0WrapPtr, colObj1WrapPtr) {
// Wrap raw WebAssembly pointers into Ammo.js classes
const cp = Ammo.wrapPointer(cpPtr, Ammo.btManifoldPoint);
const colObj0Wrap = Ammo.wrapPointer(colObj0WrapPtr, Ammo.btCollisionObjectWrapper);
const colObj1Wrap = Ammo.wrapPointer(colObj1WrapPtr, Ammo.btCollisionObjectWrapper);
const body0 = Ammo.castObject(colObj0Wrap.getCollisionObject(), Ammo.btRigidBody);
const body1 = Ammo.castObject(colObj1Wrap.getCollisionObject(), Ammo.btRigidBody);
// Retrieve individual friction values
const friction0 = body0.getFriction();
const friction1 = body1.getFriction();
// IMPLEMENT CUSTOM FRICTION MODEL HERE
// Example: Standard addition/average instead of multiplication
const customCombinedFriction = (friction0 + friction1) / 2;
// Apply the custom friction to the contact point
cp.set_m_combinedFriction(customCombinedFriction);
// Return true to indicate the custom values were applied
return true;
}Step 3: Register the Callback with Ammo.js
Because Ammo.js runs in WebAssembly, JavaScript functions must be
converted into C++ compatible function pointers using
Ammo.addFunction. Once converted, assign the pointer to the
global gContactAddedCallback variable.
// 'iiii' represents: returns int, accepts three ints (pointers)
const callbackPointer = Ammo.addFunction(customMaterialCombiner, 'iiii');
// Assign the function pointer to the global Bullet callback
Ammo.setGContactAddedCallback(callbackPointer);Alternative: Direct Manifold Manipulation (Post-Collision Step)
If your build of Ammo.js does not expose
setGContactAddedCallback or addFunction, you
can manually override friction values immediately after the physics step
but before constraints are resolved. This is done by iterating through
all collision manifolds in the dispatcher.
function overrideFrictionManually(dynamicsWorld) {
const dispatcher = dynamicsWorld.getDispatcher();
const numManifolds = dispatcher.getNumManifolds();
for (let i = 0; i < numManifolds; i++) {
const manifold = dispatcher.getManifoldByIndexInternal(i);
const numContacts = manifold.getNumContacts();
if (numContacts === 0) continue;
const body0 = Ammo.castObject(manifold.getBody0(), Ammo.btRigidBody);
const body1 = Ammo.castObject(manifold.getBody1(), Ammo.btRigidBody);
// Check if our target bodies are in this manifold
const friction0 = body0.getFriction();
const friction1 = body1.getFriction();
// Calculate custom combined friction
const customFriction = Math.sqrt(friction0 * friction0 + friction1 * friction1);
// Apply to all contact points in the manifold
for (let j = 0; j < numContacts; j++) {
const cp = manifold.getContactPoint(j);
cp.set_m_combinedFriction(customFriction);
}
}
}
// Call this function inside your application render/physics loop:
// dynamicsWorld.stepSimulation(deltaTime);
// overrideFrictionManually(dynamicsWorld);