How to Use Ammo.js Collision Groups and Masks

This article explains how Ammo.js, the JavaScript port of the Bullet physics engine, manages collision filtering. You will learn how collision groups and masks work mathematically using bitwise operations, how to define them in JavaScript, and how to apply them to rigid bodies to control which objects collide with each other in a 3D simulation.

The Logic Behind Collision Filtering

Ammo.js determines whether two rigid bodies should collide by using 32-bit integer bitmasks. Each rigid body is assigned a collision group (representing what the object is) and a collision mask (representing what the object is allowed to collide with).

When two objects, Object A and Object B, are close to each other, the physics engine performs a bidirectional bitwise AND operation. A collision is registered only if both of the following conditions are met:

  1. The collision group of Object A matches the collision mask of Object B.
  2. The collision group of Object B matches the collision mask of Object A.

Mathematically, this check is written as:

(Group_A & Mask_B) !== 0  AND  (Group_B & Mask_A) !== 0

If either of these evaluations results in zero, the engine completely ignores the collision, saving CPU cycles.

Defining Groups and Masks

Because Ammo.js uses bitwise operations, you must define your groups using powers of two (using the bitwise left-shift operator <<). This ensures that each group occupies a unique bit in the 32-bit integer.

Here is an example of defining categories:

// Define distinct groups using bitwise shift
const COLLIDE_NONE    = 0;      // 00000000
const COLLIDE_DEFAULT = 1 << 0; // 00000001 (1)
const COLLIDE_PLAYER  = 1 << 1; // 00000010 (2)
const COLLIDE_ENEMY   = 1 << 2; // 00000100 (4)
const COLLIDE_GROUND  = 1 << 3; // 00001000 (8)

To create a mask that allows an object to collide with multiple groups, you combine the groups using the bitwise OR (|) operator:

// Player can collide with Ground and Enemies
const playerMask = COLLIDE_GROUND | COLLIDE_ENEMY;

// Enemy can collide with Ground, Player, and other Enemies
const enemyMask = COLLIDE_GROUND | COLLIDE_PLAYER | COLLIDE_ENEMY;

// Ground can collide with everything
const groundMask = COLLIDE_PLAYER | COLLIDE_ENEMY | COLLIDE_DEFAULT;

Applying Groups and Masks in Ammo.js

To apply these filters, you pass the group and mask as the second and third arguments when adding the rigid body to the physics world using the addRigidBody method.

// Assuming dynamicsWorld and rigid bodies are already initialized
const playerBody = createPlayerRigidBody();
const enemyBody = createEnemyRigidBody();
const groundBody = createGroundRigidBody();

// Add player to the world: Group is PLAYER, Mask is playerMask
dynamicsWorld.addRigidBody(playerBody, COLLIDE_PLAYER, playerMask);

// Add enemy to the world: Group is ENEMY, Mask is enemyMask
dynamicsWorld.addRigidBody(enemyBody, COLLIDE_ENEMY, enemyMask);

// Add ground to the world: Group is GROUND, Mask is groundMask
dynamicsWorld.addRigidBody(groundBody, COLLIDE_GROUND, groundMask);

By passing these parameters during the addRigidBody call, Ammo.js handles all broadphase collision filtering internally. This prevents unnecessary contact point calculations, significantly optimizing the performance of your physics simulation.