Collision groups & layers

Group and sub-group filtering, and the object/broad-phase layers the library presets.

Jolt filters collisions at three levels: broad-phase layers, object layers, and per-body collision groups. The first two are preset by this library; groups and sub-groups are yours.

Preset layers

Each <Physics> world sets up three object layers and maps them onto three broad-phase layers:

layerused forcollides with
Layer.MOVINGdynamic and kinematic bodiesmoving, non-moving
Layer.NON_MOVINGstatic bodiesmoving
Layer.RIGcharacter/vehicle rig bodiesnothing — rigs are driven, not collided

Layer and NUM_OBJECT_LAYERS are exported if you need the constants, but the tables themselves aren't configurable yet. Body layers follow from the type you give a <RigidBody>: static uses NON_MOVING, kinematic and dynamic use MOVING, rig uses RIG. (Layer.KINEMATIC is declared but nothing is assigned to it.)

Groups and sub-groups

Group filtering is the "this specific pair of objects shouldn't collide" filter. Object layers stay the broad category filter.

A group is a set of bodies that filter against each other; a subGroup is that body's id within the group. Two bodies consult the filter only when their group ids match — bodies in different groups, and bodies with no group at all, always collide normally. Within a group, every sub-group pair collides until you turn one off.

<RigidBody group={7} subGroup={1}>
    <mesh>
        <boxGeometry args={[5, 0.5, 8]} />
        <meshStandardMaterial color="#ff4060" />
    </mesh>
</RigidBody>
import { useJolt } from '@react-three/jolt';
import { useEffect } from 'react';

function Trapdoor() {
    const { bodySystem } = useJolt();

    useEffect(() => {
        // the platform (sub group 1) and the player (sub group 2) stop colliding
        bodySystem.disableCollision(1, 2);
        return () => bodySystem.enableCollision(1, 2);
    }, [bodySystem]);

    return null;
}
Important

Breaking change. The old hard-coded sub-group 0/1/2 semantics are gone. Sub-group 0 no longer means "pass through everyone in my group", 2 no longer means "collide only with my group": the table starts fully enabled and you switch pairs off explicitly with disableCollision. Give every body that needs its own filtering relationship its own sub-group id.

<RigidBody group subGroup>

Both props are reactive — writing them after creation pushes a new CollisionGroup through BodyInterface.SetCollisionGroup and wakes the body, so the change takes effect on the next step. A body that never asked for a group gets one lazily the first time you set either. group={0} and subGroup={0} are real ids and are no longer swallowed by a truthy check. Setting a prop back to undefined does not clear the group.

The same thing on a BodyState:

body.group = 7;
body.subGroup = 2;
// `collisionGroup` / `collisionSubGroup` are aliases of the same pair
body.collisionSubGroup = 3;

Or, creating bodies by hand:

const handle = bodySystem.addBody(mesh, { group: 7, subGroup: 1 });
const body = bodySystem.getBody(handle);
if (body) body.subGroup = 2;

The filter table

call
bodySystem.setGroupCollision(a, b, enabled)turn a sub-group pair on or off
bodySystem.disableCollision(a, b)setGroupCollision(a, b, false)
bodySystem.enableCollision(a, b)setGroupCollision(a, b, true)
bodySystem.isCollisionEnabled(a, b)current state of that pair
bodySystem.subGroupCounthow many sub-group ids the table holds. Default 256

The table is a Jolt GroupFilterTable, built the first time a grouped body is created. Raise subGroupCount before that if you need more than 256 ids — the table is sized once:

bodySystem.subGroupCount = 1024;
Caution

Sub-group ids are range checked here, because Jolt only bounds-checks the table with an assert that is compiled out of the release WASM — an id past the end of the table would scribble over the heap. An out-of-range id (not an integer, negative, or >= subGroupCount) is dropped with a devWarn naming the valid range, rather than throwing; isCollisionEnabled fails open and returns true. Nothing in this API throws, so turn on setDebug(true) while you are wiring filtering up.

Filtering a sub-group against itself (a === b) is also rejected with a warning: Jolt stores only the lower triangle of the table, so the (n, n) slot aliases a real pair's bit. Give every body in a group its own sub-group id instead of relying on it.

Lifecycle

Every grouped body owns its own CollisionGroup, created with the body and destroyed when the body is removed — handles are recycled, so a stale group is never left behind. The filter table is reference counted and released by bodySystem.destroy(), which <Physics> calls for you on unmount.

Sensors

A body with isSensor reports contacts but blocks nothing — orthogonal to groups, and the basis of trigger volumes and force fields.

<RigidBody isSensor>
    <mesh>
        <boxGeometry args={[4, 4, 4]} />
    </mesh>
</RigidBody>

What isn't exposed yet

Broad-phase layer filters, object-layer pair tables and per-cast body/shape filters exist in Jolt and are set to permissive defaults here. Queries do expose their filter objects (raycaster.bodyFilter, raycaster.objectFilter, …) if you are willing to work with raw Jolt — mind the ownership rules if you replace one.