Ammo.js Custom Broadphase Collision Filtering

This article explains how to filter specific collision pairs from generating contact points in Ammo.js by implementing a custom broadphase overlap callback. While Ammo.js provides default collision groups and masks for basic filtering, complex projects often require dynamic, object-specific collision rules. By overriding the broadphase overlap filter callback, you can intercept colliding pairs before they reach the narrowphase stage, saving performance and preventing unwanted contact point generation.

Step 1: Implement the btOverlapFilterCallback

To intercept collision pairs during the broadphase, you must instantiate and define a custom btOverlapFilterCallback. This callback features a needBroadphaseCollision method that determines whether a collision pair should proceed to narrowphase processing.

// Create the broadphase filter callback instance
const filterCallback = new Ammo.btOverlapFilterCallback();

// Override the collision decision method
filterCallback.needBroadphaseCollision = function(proxy0, proxy1) {
    // Cast broadphase proxies to collision objects
    const colObj0 = Ammo.castObject(proxy0.get_m_clientObject(), Ammo.btCollisionObject);
    const colObj1 = Ammo.castObject(proxy1.get_m_clientObject(), Ammo.btCollisionObject);

    // Retrieve user-defined identifiers (e.g., custom user pointers or IDs)
    const id0 = colObj0.getUserPointer();
    const id1 = colObj1.getUserPointer();

    // Define your custom filtering logic
    if (shouldPreventCollision(id0, id1)) {
        return false; // Reject the pair; no contact points will be generated
    }

    // Default bitmask-based group and mask filtering fallback
    const collides = (proxy0.get_m_collisionFilterGroup() & proxy1.get_m_collisionFilterMask()) !== 0 &&
                     (proxy1.get_m_collisionFilterGroup() & proxy0.get_m_collisionFilterMask()) !== 0;

    return collides; // Return true to allow narrowphase contact generation
};

Step 2: Register the Callback with the Physics World

After defining the callback, you must assign it to the overlapping pair cache of your btDiscreteDynamicsWorld. This ensures that the physics engine runs your custom logic during its broadphase update loop.

// Access the pair cache from your dynamics world and assign the callback
const pairCache = dynamicsWorld.getPairCache();
pairCache.setOverlapFilterCallback(filterCallback);

Step 3: Accessing User Data Safely

To identify which JavaScript objects correspond to the btCollisionObject instances in your callback, assign a unique pointer or index to each rigid body when initializing them:

// Assigning an ID or pointer reference when creating a body
const body = new Ammo.btRigidBody(constructionInfo);
body.setUserPointer(customObjectId); // customObjectId can be an integer ID

Inside the needBroadphaseCollision function, retrieve this identifier using getUserPointer() to run your comparison logic. Returning false from this callback immediately discards the pair, completely bypassing narrowphase manifold generation and collision dispatch.