Combine Restitution in Ammo.js Using Custom Callbacks

This article explains how to override the default multiplicative restitution (bounciness) behavior in Ammo.js by implementing a custom callback mechanism. You will learn how to configure your rigid bodies, intercept collision contacts, and apply a custom mathematical formula to combine the restitution values of two colliding materials.

The Default Behavior vs. Custom Combination

By default, the Bullet physics engine (which Ammo.js is ported from) calculates the combined restitution of two colliding bodies by multiplying their individual restitution values:

\[\text{Combined Restitution} = \text{Restitution}_A \times \text{Restitution}_B\]

If you want a different behavior—such as taking the maximum bounciness, the average, or a custom physical material lookup—you must manually calculate and apply the combined restitution value using a post-simulation step callback or by iterating through the collision manifolds.


Step 1: Flag the Collision Objects

To tell Ammo.js that you want to customize the material properties of specific colliding objects, you should enable the custom material callback flag on the rigid bodies.

// Assuming bodyA and bodyB are Ammo.btRigidBody instances
const CF_CUSTOM_MATERIAL_CALLBACK = 8; // Bullet's flag value

bodyA.setCollisionFlags(bodyA.getCollisionFlags() | CF_CUSTOM_MATERIAL_CALLBACK);
bodyB.setCollisionFlags(bodyB.getCollisionFlags() | CF_CUSTOM_MATERIAL_CALLBACK);

// Set individual restitution values
bodyA.setRestitution(0.9); // Highly bouncy
bodyB.setRestitution(0.5); // Moderately bouncy

Step 2: Implement the Custom Callback Loop

The most reliable way to modify contact points in Ammo.js is to iterate through the active collision manifolds immediately after the physics world steps. This acts as your custom callback.

Here is the implementation to calculate custom combined restitution (in this example, using the maximum value instead of multiplication):

function processCustomRestitution(physicsWorld) {
    const dispatcher = physicsWorld.getDispatcher();
    const numManifolds = dispatcher.getNumManifolds();

    for (let i = 0; i < numManifolds; i++) {
        const manifold = dispatcher.getManifoldByIndexInternal(i);
        const obA = Ammo.castObject(manifold.getBody0(), Ammo.btRigidBody);
        const obB = Ammo.castObject(manifold.getBody1(), Ammo.btRigidBody);

        // Check if both objects have the custom material flag enabled
        const flagA = obA.getCollisionFlags() & CF_CUSTOM_MATERIAL_CALLBACK;
        const flagB = obB.getCollisionFlags() & CF_CUSTOM_MATERIAL_CALLBACK;

        if (flagA || flagB) {
            const numContacts = manifold.getNumContacts();
            
            for (let j = 0; j < numContacts; j++) {
                const pt = manifold.getContactPoint(j);
                
                // Retrieve the individual restitution values
                const resA = obA.getRestitution();
                const resB = obB.getRestitution();

                // Custom logic: Use the maximum restitution instead of multiplying
                const combinedRestitution = Math.max(resA, resB);

                // Apply the custom combined restitution to the contact point
                pt.set_m_combinedRestitution(combinedRestitution);
            }
        }
    }
}

Step 3: Integrate with Your Game Loop

To ensure your custom restitution is applied before the solver resolves the constraints, execute the callback function immediately after calling stepSimulation in your main update loop.

function update(deltaTime) {
    // 1. Step the physical simulation
    physicsWorld.stepSimulation(deltaTime, 10);

    // 2. Apply custom restitution logic to the active contacts
    processCustomRestitution(physicsWorld);

    // 3. Continue with rendering and other game logic
}