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);