Collision groups & layers
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:
| layer | used for | collides with |
|---|---|---|
Layer.MOVING | dynamic and kinematic bodies | moving, non-moving |
Layer.NON_MOVING | static bodies | moving |
Layer.RIG | character/vehicle rig bodies | nothing — 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;
}
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.subGroupCount | how 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;
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.