React Three Jolt
Full documentation content.
`@react-three/jolt` (or `r3/jolt`) puts the [Jolt Physics](https://github.com/jrouwe/JoltPhysics)
engine behind a declarative [react-three-fiber](https://github.com/pmndrs/react-three-fiber)
component tree: wrap a mesh in `` and it falls.

```bash
npm install @react-three/jolt jolt-physics
```
> [!WARNING]
> **Status: alpha.** Every API on this site is subject to change, and some of it will. The
> library is under active development again after a long pause — see
> [the milestones](https://github.com/pmndrs/react-three-jolt/milestones) for what "1.0" means
> here. Pin an exact version if you build on it today.
## What Jolt is
Jolt is the rigid body engine written for *Horizon Forbidden West*: a modern, multi-core-friendly
C++ engine that also powers [Godot Jolt](https://github.com/godot-jolt/godot-jolt), compiled to
WebAssembly by [JoltPhysics.js](https://github.com/jrouwe/JoltPhysics.js). It is deterministic,
fast at scale, and unusually deep — vehicles, characters, ragdolls, soft bodies, heightfields and
a large constraint library are all first-class in the engine itself rather than bolted on.
The price of that depth is an API with sharp edges. Jolt is a C++ library reached through an
Emscripten binding, which means manual memory management, layer and filter setup before a single
body exists, and a lot of ceremony between "I have a mesh" and "it falls". This library exists to
take that on for you.
## What this library adds
- **`` and ``** — a world and bodies that follow React's lifecycle, with
shapes derived automatically from your three.js geometry.
- **A `BodyState` per body** — one object with plain three.js types (`THREE.Vector3`,
`THREE.Quaternion`) for reading and writing pose, velocity, damping, friction, mass and
degrees of freedom, instead of a `BodyInterface` and a `BodyID`.
- **Systems, not singletons** — `PhysicsSystem`, `BodySystem`, `ConstraintSystem` and the query
classes are ordinary objects you can reach from `useJolt()` when the components aren't enough.
- **Memory ownership rules that hold** — conversion helpers that always return something you own,
scoped (`withJolt`) and scratch (`joltScratch`) variants for hot paths, and bodies/shapes that
free their WASM allocations when they unmount. See [Memory & lifecycle](/advanced/memory).
- **Extras** — instanced bodies, heightfields, a character controller, a camera rig, four- and
two-wheeled vehicles, and an input-to-command mapper.
## Jolt or Rapier?
[`@react-three/rapier`](https://github.com/pmndrs/react-three-rapier) is the sibling library most
people compare this to, and it is the right default today. Both wrap a WASM physics engine for
r3f; they differ in maturity and in what the engine underneath is good at.
| | `@react-three/rapier` | `@react-three/jolt` |
| --- | --- | --- |
| Status | stable, widely used | alpha, API in motion |
| Engine | Rapier (Rust) | Jolt (C++), the *Horizon Forbidden West* engine |
| Colliders | explicit `` etc. plus `colliders` autogeneration | autogenerated from geometry, or an explicit [``](/api/shapes) |
| Strengths | 2D and 3D, mature docs and ecosystem, determinism | scale (many bodies), built-in vehicles/characters, deep constraint set, heightfields |
| Memory | handled by wasm-bindgen | manual — the library owns it for you, but the rules leak out when you touch Jolt directly |
Pick Rapier if you want something dependable right now. Pick Jolt if you are pushing on body
counts, want Jolt's vehicle/character systems, or are willing to trade stability for the engine's
ceiling — and are happy to report bugs.
## Where to go next
- [Installation](/getting-started/installation) — peers, bundler setup, Vite and Next.js.
- [Physics](/api/physics) — the world component and every prop it takes.
- [RigidBody](/api/rigid-body) — bodies, props and the `BodyState` API.
- [Memory & lifecycle](/advanced/memory) — read this before you touch a `Jolt.*` object.
]]>
[!NOTE]
> These used to be the separate packages `@react-three/jolt-addons` and
> `@react-three/jolt-controllers`. They were always released in lockstep with core, and keeping
> them apart risked a second copy of core in the dependency tree — which breaks badly, because
> core owns a single global handle on the WASM module. Both npm packages are deprecated; change
> the import path and delete them from your `package.json`.
## Peer dependencies
Nothing here is bundled — every one of these has to exist in your app, at these ranges:
| package | range | why |
| --- | --- | --- |
| `react`, `react-dom` | `>=19.0.0` | `@react-three/fiber` 10 is currently tested against `19.2.x`; that is the safest choice today. |
| `@react-three/fiber` | `>=10.0.0-0` | currently `10.0.0-alpha.5` |
| `three` | `>=0.185` | |
| `jolt-physics` | `>=1.1.0` | the engine itself |
```bash
npm install react@19.2.8 react-dom@19.2.8 \
three@^0.186.0 \
@react-three/fiber@10.0.0-alpha.5 \
jolt-physics@^1.1.0 \
@react-three/jolt
```
> [!NOTE]
> `jolt-physics` is a **peer**, not a dependency, on purpose: it keeps a second copy of a
> multi-megabyte WASM module out of your bundle, and it lets you choose the
> [build variant](#choosing-a-jolt-build) yourself.
## Your first scene
```tsx
import { Canvas } from '@react-three/fiber';
import { Physics, RigidBody } from '@react-three/jolt';
import { Suspense } from 'react';
export function App() {
return (
);
}
```
`` suspends while the WASM module loads, so it needs a `` boundary above it —
see [SSR & Suspense](/advanced/ssr-and-suspense) for the details and the failure modes.
## Bundlers
By default the library imports `jolt-physics`, whose main entry is the **`wasm-compat`** build:
the WASM binary is base64-encoded inside the JavaScript, so there is no second file to serve and
no asset URL to configure. That is why most setups need nothing at all.
### Vite
Works out of the box.
### Next.js with Turbopack
Works out of the box with the default bundler, in both `next dev` and `next build`, App Router
included. Remember that a `
` is the entry point to the simulation. Everything physical has to be inside one, the
way everything three.js has to be inside a `
` wraps three.js objects, builds a Jolt shape from their geometry, creates the body,
and keeps the two in sync every frame.
```tsx
import { RigidBody } from '@react-three/jolt';
;
```
Children render inside an `` the body drives. Anything non-visual (lights, groups,
your own components) can live in there too; only meshes contribute to the collision shape.
To place the collision shape yourself — a trigger volume with no mesh, a cheap primitive standing
in for a detailed model, several pieces making up one body — use the named collider components
and ``. See [Colliders](/api/colliders).
## Props
| prop | type | notes |
| --- | --- | --- |
| `type` | `'dynamic' \| 'static' \| 'kinematic' \| 'rig'` | motion type. Defaults to dynamic |
| `shape` | [`ShapeType`](/api/shapes#shapetype) | overrides shape autodetection for this body |
| `colliders` | `false \| 'cuboid' \| 'ball' \| 'hull' \| 'trimesh'` | what to do about the meshes inside the body — see [Colliders](/api/colliders#) |
| `dynamicMeshStrategy` | `'convex' \| 'decompose' \| 'error'` | what a **dynamic** body does with a trimesh shape — see [Trimeshes](/api/shapes#trimeshes). Falls back to ``, then `'convex'` |
| `position` | `number[]` | setting it after creation **teleports** the body |
| `rotation` | `number[]` | Euler angles, radians. Also a teleport |
| `scale` | `number[]` | scales the collision shape as well as the object. Applied **while the body is created**, not a frame later |
| `quaternion` | `number[]` | accepted, currently only stored on the context |
| `mass` | `number` | kilograms. Ignored on static/kinematic bodies, which have infinite mass |
| `friction` | `number` | `0` (ice) to `1` (glue). Jolt's default is `0.2`. Reactive |
| `restitution` | `number` | bounciness, `0`–`1`. Default `0`. Reactive |
| `gravityFactor` | `number` | multiplier on world gravity for this body. Default `1`. Reactive |
| `linearDamping` | `number` | Jolt's default is `0.05` |
| `angularDamping` | `number` | Jolt's default is `0.05` |
| `isSensor` | `boolean` | fires sensor events, causes no collision response |
| `group` / `subGroup` | `number` | [collision filtering](/api/collision-groups). Reactive, and `0` is a real id |
| `dof` | `{ x?, y?, z?, rotX?, rotY?, rotZ? }` | per-axis degrees of freedom |
| `lockRotations` | `boolean` | shorthand for all three rotations off |
| `lockTranslations` | `boolean` | shorthand for all three translations off |
| `allowObstruction` | `boolean` | used by the [camera rig](/api/controllers#camerarig) to let the camera see through this body |
| `obstructionTimelimit` | `number` | ms; switches obstruction to `'temporal'` |
| `onlyInitialize` | `boolean` | apply `position`/`rotation` at creation only, then stop watching them |
| `debug` | `boolean` | draw this body's collision shape |
| `activateOnChange` | `boolean` | default `true`. `false` stops `position`/`rotation`/`velocity`/`angularVelocity`/`scale`/`group`/`subGroup` writes from waking a sleeping body — see [Activation](#activation) |
| `matrixAutoUpdate` | `boolean` | default `true`. `false` skips three's per-object matrix recompute in the physics sync — see [matrixAutoUpdate](#matrixautoupdate) |
| `onContactAdded` | `(payload: CollisionEnterPayload) => void` | deprecated alias of `onCollisionEnter` — see [Events](#events) |
| `onContactRemoved` | `(payload: CollisionPayload) => void` | deprecated alias of `onCollisionExit` |
| `onContactPersisted` | `(payload: CollisionEnterPayload) => void` | deprecated alias of `onCollisionPersist` |
| `ref` | `Ref` | see [the ref caveat](#the-ref-caveat) |
Event props — `onCollisionEnter`, `onCollisionPersist`, `onCollisionExit`, `onSensorEnter`,
`onSensorExit`, `onSleep`, `onWake`, `onContactValidate` (and the `onIntersectionEnter` /
`onIntersectionExit` aliases) — have a [section of their own](#events).
```tsx
console.log(e.other.handle, e.normal)}
>
```
> [!NOTE]
> Writing `position` or `rotation` moves the body *instantly* — it does not push it there. For
> kinematic motion use [`setKinematicTarget`](#kinematic-platforms), and for dynamic bodies use
> forces and impulses.
## The ref
`` gives you the [`BodyState`](#bodystate) for that body, once it exists.
```tsx
import { BodyState, RigidBody } from '@react-three/jolt';
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
export function Ball() {
const body = useRef(null);
useEffect(() => {
body.current?.addImpulse(new THREE.Vector3(0, 40, 0));
}, []);
return (
);
}
```
### The ref caveat
The `ref` prop is currently typed `any`, so **TypeScript does not check it**:
`useRef(null)` compiles, but so does `useRef(null)`. You are
annotating, not verifying. The ref is also `null` until the body is created — which happens in an
effect, and waits for child `` components if there are any — so always guard with `?.`.
Tracked in [#160](https://github.com/pmndrs/react-three-jolt/issues/160), along with doc comments
for every `BodyState` member.
### Without a ref
Every body is also reachable through the body system:
```tsx
import { BodyState, useJolt } from '@react-three/jolt';
import { useEffect } from 'react';
import * as THREE from 'three';
function Nudge({ handle }: { handle: number }) {
const { bodySystem } = useJolt();
useEffect(() => {
const body = bodySystem.getBody(handle);
if (body) body.position = new THREE.Vector3(0, 3, 2);
bodySystem.dynamicBodies.forEach((state: BodyState) => {
state.addImpulse(new THREE.Vector3(0, 2, 1));
});
}, [bodySystem, handle]);
return null;
}
```
`bodySystem` holds `dynamicBodies`, `staticBodies` and `kinematicBodies` as `Map`,
keyed by handle; `getBody(handle)` searches all three. A handle is Jolt's
`BodyID.GetIndexAndSequenceNumber()`, and **Jolt recycles them** — don't hold one across a body's
lifetime.
## `BodyState`
One object per body. Everything on it speaks three.js types; the Jolt side is an implementation
detail.
### Pose
| member | |
| --- | --- |
| `position` | `THREE.Vector3`, get/set (setting teleports) — activation per [`activateOnChange`](#activation) |
| `rotation` | `THREE.Quaternion`, get/set — same activation rules as `position` |
| `scale` | `THREE.Vector3 \| number[] \| number`, get/set — rebuilds the scaled shape; same activation rules |
| `setPosition(position, { activate? })` | method form of `position`, with a one-call activation override (#167) |
| `setRotation(rotation, { activate? })` | method form of `rotation` |
| `setScale(scale, { activate? })` | method form of `scale` |
| `setPositionAndRotation(position, rotation, { activate? })` | both at once |
| `getPosition(asJolt?)` | `asJolt` returns a `Jolt.RVec3` — see [ownership](/advanced/memory) |
| `getMatrix(matrix)` / `setMatrix(matrix)` | `THREE.Matrix4` in/out |
| `readPose(outPosition, outRotation)` | allocation-free live pose |
| `getInterpolatedPose(alpha, outPosition, outRotation)` | allocation-free blend of the last two steps |
| `resetPoseCache()` | drop the pose history, e.g. after a teleport, so the next frame doesn't lerp across it |
| `moveKinematic(position, rotation?, deltaTime?)` | drive a kinematic body for one step |
| `setKinematicTarget(position, rotation?)` | aim a kinematic body; the step loop drives it |
| `clearKinematicTarget()` | stop driving it |
| `isStatic` | read-only |
```tsx
const position = useRef(new THREE.Vector3());
const rotation = useRef(new THREE.Quaternion());
useFrame(() => {
body.current?.readPose(position.current, rotation.current);
});
```
The `position`/`rotation` getters allocate a new three.js object each read — fine for effects,
avoid in a per-frame loop, where `readPose` exists.
### Kinematic platforms
A kinematic body is moved by you and pushes everything it touches. Drive it with a target pose,
never by writing `position` — that teleports, and a teleport carries nothing with it.
```tsx
const platform = useRef(null);
useFrame((state) => {
// the step loop re-aims this every substep with that substep's real dt
platform.current?.setKinematicTarget([Math.sin(state.elapsed) * 5, 2, 0]);
});
```
`setKinematicTarget` is the one to reach for: the target is sticky and re-applied at the top of
every substep, so the body converges on it whatever the frame rate, and a body that has arrived
simply parks there. `moveKinematic(position, rotation?, deltaTime?)` is the one-shot version —
it applies once, with `deltaTime` defaulting to the world's step length
(`physicsSystem.timeStep`, or the last frame delta when the world steps with `timeStep="vary"`).
Leaving `rotation` out keeps the body's current rotation in both.
Riders are carried because `MoveKinematic` gives the platform a real velocity, which is what
wakes the sleeping bodies resting on it — this works with stock body settings. Two things are
still worth tuning for a demo that has to feel solid:
- **friction**: the default is `0.2`, which is slippery. A rider on a fast platform will lag
behind it and can slide off. Raise `friction` on the platform and/or the riders (`0.8`–`1`).
- **sleeping**: a rider that is out of contact for a moment (a bumpy ride) can fall asleep in
mid-air's worth of time and miss the next push. `` removes the question at the cost of never letting the scene idle.
### Moving a static body
`position`/`setPosition`, `rotation`/`setRotation` and `setPositionAndRotation` work on **every**
motion type, including static bodies (issue #61) — `SetPosition`/`SetRotation` update the
broadphase regardless of motion type, and the three.js object is brought along by a small "dirty
statics" drain in `PhysicsSystem.onUpdate`, since the frame loop otherwise only walks bodies that
can be awake (dynamic/kinematic).
```ts
// fine: an occasional reposition of scenery
wall.current!.position = new THREE.Vector3(4, 0, 0);
```
It is a **teleport**, not simulation, whatever the motion type — the same reason `resetPoseCache()`
is called for you on every write. For a static body specifically that means:
- Nothing resting on it is carried along, the way a kinematic platform carries riders.
- Sleeping neighbours are not woken by the move.
- Contacts are resolved on the next step as if the body had always been at its new position —
there is no sweep, no intermediate collision.
Moving a static body **every frame** is an anti-pattern for exactly those reasons: it looks like
motion but has none of its physical consequences. Reach for `type="kinematic"` with
[`setKinematicTarget`](#kinematic-platforms) (or `moveKinematic`) for anything that moves
repeatedly — statics are for the occasional reposition of scenery (moving a platform into place at
level start, snapping a door open). [`activateOnChange`](#activation) has no effect either way: a
static body never activates, since a static is never simulated.
### Activation
Setters that can move a body (`position`, `rotation`, `velocity`, `angularVelocity`, `scale`,
`group`, `subGroup`) used to wake a sleeping body unconditionally (issue #167). `activateOnChange`
controls that per body:
```tsx
const crate = useRef(null);
useEffect(() => {
// repositioning sleeping scenery shouldn't wake it just to move it and let it fall back
// asleep a moment later
crate.current!.activateOnChange = false;
crate.current!.position = new THREE.Vector3(3, 0.5, -2);
}, []);
```
- Default `true` — every setter's behavior before this flag existed. Nothing changes for
existing code.
- `` sets it at the component level.
- Each setter's method form (`setPosition`, `setRotation`, `setVelocity`, `setAngularVelocity`,
`setScale`, `setGroup`, `setSubGroup`) takes a one-call `{ activate }` override that wins over
the flag, e.g. `body.setPosition(v, { activate: true })` even while `activateOnChange` is
`false`.
- A **static** body never activates, regardless of either flag — activating one asserts inside
Jolt and means nothing, since a static is never simulated.
- Turn `activateOnChange` off for bulk repositioning of sleeping bodies: re-laying out a level's
sleeping props, snapping a stack of crates back to a saved layout, and similar "instant, no one
needs to notice" moves where waking every body just to move it — and having it fall back asleep
a step later — is wasted broadphase/island work.
### `matrixAutoUpdate`
Opt-in perf optimisation (issue #168). By default, the physics frame sync writes each body's pose
into `object.position`/`object.quaternion`, and three.js recomposes `object.matrix` from those
every frame on its own. With `bodyState.matrixAutoUpdate = false` (or
``), the sync instead composes the pose straight into
`object.matrix` and flags `matrixWorldNeedsUpdate`, and turns off three's own
`Object3D.matrixAutoUpdate` so nothing recomposes it a second time. `object.position` and
`object.quaternion` are left exactly as they were at creation — reading them no longer tells you
where the object is; use `bodyState.position` / `readPose` instead.
```tsx
```
> [!CAUTION]
> Only correct when this body's three.js parent transform is stable between physics steps — the
> scene root, or a group that never moves, rotates or scales. The composed matrix is relative to
> the parent space captured once in `invertedWorldMatrix` at body creation, exactly like every
> other synced pose (this constraint already existed; `matrixAutoUpdate` does not add it, it
> inherits it) — a moving parent was never supported by the sync loop.
Measured with 1000 dynamic bodies stepped for 120 frames
(`packages/react-three-jolt/test/matrix-auto-update.test.ts`): roughly a 4–6% reduction in
sync-loop time, from skipping three's `Object3D.updateMatrix()` and an extra `Matrix4.decompose()`
per body. Worth it for scenes with hundreds to thousands of always-visible dynamic bodies; not
worth the readability cost for a handful.
### Motion
| member | |
| --- | --- |
| `velocity` | `THREE.Vector3`, get/set — activation per [`activateOnChange`](#activation) |
| `angularVelocity` | `THREE.Vector3`, get/set — same activation rules |
| `setVelocity(velocity, { activate? })` | method form of `velocity`, with a one-call override |
| `setAngularVelocity(angularVelocity, { activate? })` | method form of `angularVelocity` |
| `applyForce(force)` | |
| `applyTorque(torque)` | |
| `addImpulse(impulse)` | |
| `isSleeping` | read-only |
### Material and mass
`friction`, `restitution`, `mass`, `gravityFactor`, `linearDamping`, `angularDamping`,
`isSensor` — all plain get/set number/boolean properties. `color` gets/sets the three.js material
colour (and the per-instance colour on instanced bodies).
`mass` is the body's *simulated* mass in kilograms, read from its motion properties rather than
from the shape's density, so it reflects a `mass` prop or a later write. Static and kinematic
bodies have infinite mass in Jolt and report `0`; setting it on one does nothing (a
[`devWarn`](/advanced/memory#debug-output) names which motion type ignored it), and so does
setting it to `0` or a negative number on a dynamic body. Setting it scales the inverse mass and
the inertia tensor together and leaves the body's [degrees of freedom](#degrees-of-freedom)
untouched — unlike going through Jolt's `SetMassProperties` directly, which would reset them to
"all".
### Degrees of freedom
| member | |
| --- | --- |
| `dof` | `{ x, y, z, rotX, rotY, rotZ }` booleans, read |
| `setDof(dof)` | write some or all of them |
| `setEnabledTranslations(x, y, z)` / `setEnabledRotations(x, y, z)` | |
| `lockTranslations()` / `lockRotations()` | |
| `rawDOF` | the Jolt bitfield, if you want it |
### Filtering and identity
`handle`, `body` (the raw `Jolt.Body`), `BodyID`, `object` (the three.js object), `meshType`,
`isInstance`, and `group` / `subGroup` (plus the `collisionGroup` / `collisionSubGroup` aliases) —
see [Collision groups & layers](/api/collision-groups).
`setGroup(group, { activate? })` / `setSubGroup(subGroup, { activate? })` are the method forms
with a one-call [activation](#activation) override (#167).
### Shape
| member | |
| --- | --- |
| `shape` | the live `Jolt.Shape`, get/set |
| `scale` | `THREE.Vector3 \| number[] \| number`, get/set — wraps the shape in a `ScaledShape` |
| `isMutableCompound` | is this body's shape a `MutableCompoundShape` |
| `mutableCompound` | that shape, cast; throws a clear error if it isn't one |
| `addSubShape(descriptor)` | add a child to a mutable compound; returns its index |
| `removeSubShape(index)` | remove one (higher indices shift down) |
| `modifySubShape(index, { position?, rotation? })` | move one |
| `notifyShapeChanged(previousCenterOfMass?, updateMassProperties?)` | tell Jolt the shape changed underneath it |
The three edit methods call `notifyShapeChanged` for you, so the body's mass properties and
broadphase bounds follow the edit. You only call it yourself if you mutate a shape by hand through
`body.shape`. See [mutable compounds](/api/shapes#mutable-compounds).
### Contacts
| member | |
| --- | --- |
| `events` | this body's [`Emitter`](#events) |
| `on(type, fn)` | subscribe; returns the unsubscribe |
| `onCollisionEnter/Persist/Exit(fn)`, `onSensorEnter/Exit(fn)`, `onSleep(fn)`, `onWake(fn)`, `onContactValidate(fn)` | named sugar for `on()` |
| `eventMask` | bitfield of what this body is currently listening for |
| `isContacting(handle)` | how many sub-shape manifolds are open against that body (`0` = not touching) |
| `contacts` | `Map`, the same bookkeeping |
| `dispose()` | close every open pair and drop every listener |
| `addContactListener(fn, 'added' \| 'removed' \| 'persisted')` | **deprecated**, returns an unsubscribe |
| `removeContactListener(fn)` | **deprecated**; removes the listener from every channel |
| `addActivationListener(fn)` / `removeActivationListener(fn)` | **deprecated**; fires for both sleep and wake |
### Motion sources
A motion source is a body that pushes *other* bodies when they touch it — conveyors, bounce pads,
force fields, teleporters. These are Jolt patterns the library packages up rather than Jolt
objects.
```tsx
const conveyor = useRef(null);
useEffect(() => {
// local-space vector: rotate the pad and the push rotates with it
conveyor.current?.activateMotionSource(new THREE.Vector3(-2.4, 0, 0));
conveyor.current!.motionAsSurfaceVelocity = true; // belt-like, instead of an impulse
}, []);
```
| member | |
| --- | --- |
| `activateMotionSource(linearVector, angularVector?)` | turn the body into a motion source |
| `motionType` | `'linear'` (impulse toward a vector) or `'angular'` (torque) |
| `motionAsSurfaceVelocity` | apply Jolt's surface velocity instead of an impulse — more realistic while contact lasts |
| `useRotation` | `false` puts the vector in **world** space (what force fields usually want) |
| `isTeleporter` | move the contacting body to the linear vector's position on the next step |
| `isConveyor`, `isMotionSource`, `motionActive` | state flags |
| `allowObstruction`, `obstructionType`, `obstructionTimelimit` | camera obstruction behaviour |
Combine with `isSensor` for a force field: contacts still fire, but nothing is blocked.
### Lifecycle
`destroy(ignoreThree?)` tears the body down, but you rarely call it — `` unmounting
calls `bodySystem.removeBody(handle)` for you, which also removes any constraints attached to
the body first. See [Memory & lifecycle](/advanced/memory).
## Constraints
`useConstraint` joins two bodies and keeps the constraint alive for the lifetime of the
component. It is removed on unmount and re-created when the type, bodies or option *values*
change.
```tsx
import { BodyState, RigidBody, useConstraint } from '@react-three/jolt';
import { useRef } from 'react';
export function Swing() {
const anchor = useRef(null);
const weight = useRef(null);
useConstraint('distance', anchor, weight, { point1: [0, 5, 0], min: 1, max: 4 });
return (
<>
>
);
}
```
Types: `fixed`, `point`, `distance`, `hinge` (alias `revolute`), `slider` (alias `prismatic`),
`cone`, `swingTwist`, `sixDOF`. Options cover anchors (`point1`, `point2`, `position`), axes
(`axis`, `normal`, `twistAxis`, `planeAxis`), limits (`min`, `max`, `angle`, `twistMin`,
`twistMax`), friction, `space`, and `spring` / `motor` sub-objects. The hook returns a ref to the
typed Jolt constraint (`ConstraintTypeMap[T]`), so a `'hinge'` gives you a `Jolt.HingeConstraint`
with `SetTargetAngularVelocity` and friends.
> [!CAUTION]
> A constraint must be removed **before** either of its bodies. The library does this for you
> (`bodySystem.removeBody` removes the body's constraints first), but if you reach for
> `constraintSystem` directly, keep the order. Never call `Raw.module.destroy()` on a constraint:
> they are reference counted, and that is a double free.
## Instanced bodies
`` replaces ``: the child mesh describes the shape and
material, `count` says how many, and every instance gets its own body. The ref gives you an array
of `BodyState`.
```tsx
import { BodyState, InstancedRigidBodies } from '@react-three/jolt';
import { useRef } from 'react';
export function Confetti() {
const bodies = useRef(null);
return (
);
}
```
| prop | type | default | |
| --- | --- | --- | --- |
| `count` | `number` | `150` | changing it adds or removes bodies incrementally |
| `color` | `THREE.ColorRepresentation` | `'#D9594C'` | seeds a *new* mesh's instance colours |
| `position` | `THREE.Vector3 \| [number, number, number]` | — | |
| `rotation` | `THREE.Euler \| [number, number, number]` | — | |
It takes the same [event props](#events) as `` — `onCollisionEnter`,
`onCollisionPersist`, `onCollisionExit`, `onSensorEnter`, `onSensorExit`, `onIntersectionEnter`,
`onIntersectionExit`, `onSleep`, `onWake` — subscribed on every instance body. The payload's
**`target.index`** says which instance fired:
```tsx
console.log('instance', e.target.index, 'hit', e.other.handle)}
>
```
> [!NOTE]
> Instances with no explicit position all start at roughly the same place (with a small jitter)
> and push each other apart — which is either a bug or a shape fountain, depending on what you
> wanted. On unmount every instance body is destroyed and the `InstancedMesh`'s GPU buffers are
> released; a geometry or material you passed as a child stays owned by three-fiber.
## Events
Every event in the library — step, contact, sensor, sleep/wake — goes through one primitive and
one set of names. A concept is spelled the same way whether you reach it as a React prop, a hook,
or an imperative subscription.
| concept | prop | imperative |
| --- | --- | --- |
| started touching | `onCollisionEnter` | `body.onCollisionEnter(fn)` / `body.on('collisionEnter', fn)` |
| still touching | `onCollisionPersist` | `body.onCollisionPersist(fn)` |
| stopped touching | `onCollisionExit` | `body.onCollisionExit(fn)` |
| entered a sensor | `onSensorEnter` (alias `onIntersectionEnter`) | `body.onSensorEnter(fn)` |
| left a sensor | `onSensorExit` (alias `onIntersectionExit`) | `body.onSensorExit(fn)` |
| went to sleep | `onSleep` | `body.onSleep(fn)` |
| woke up | `onWake` | `body.onWake(fn)` |
| accept/reject a contact | `onContactValidate` | `body.onContactValidate(fn)` |
The same names exist world-wide on [``](/api/physics#world-events), plus `onSettled` and
`onActivityChange`.
```tsx
console.log(e.other.object?.name, e.normal)}>
```
Handler identity is deliberately **not** a subscription dependency, so an inline arrow does not
resubscribe on every render, and StrictMode's mount → cleanup → mount leaves exactly one
subscription.
### Subscribing imperatively
```ts
import type { BodyState } from '@react-three/jolt';
function watch(body: BodyState) {
const off = body.on('collisionEnter', (e) => console.log(e.other.handle));
off(); // unsubscribe
}
```
**Every subscription returns its own unsubscribe, and removal never compares function identity.**
Registering the same function twice gives two subscriptions and two handles, so an inline arrow is
just as removable as a named one. `body.onCollisionEnter(fn)` and friends are one-line sugar for
the same thing, spelled exactly like the props. The old `removeContactListener(fn)` /
`removeActivationListener(fn)` methods still exist, are deprecated, and now remove *every*
subscription made for that function.
From a component, `useBodyEvent` and `useWorldEvent` do the same with an effect's lifetime:
```tsx
import { BodyState, useBodyEvent, useWorldEvent } from '@react-three/jolt';
function Counter({ body }: { body: BodyState | undefined }) {
useBodyEvent(body, 'collisionEnter', (e) => console.log(e.penetration));
useWorldEvent('settled', () => console.log('the world stopped moving'));
return null;
}
```
### Payloads
```ts
import type { BodyState } from '@react-three/jolt';
import type * as THREE from 'three';
interface CollisionTarget {
body: BodyState | undefined;
object: THREE.Object3D | undefined;
handle: number; // BodyID.GetIndexAndSequenceNumber()
subShapeId: number; // SubShapeID.GetValue(); -1 for a shape with no sub-shapes
index?: number; // instance index, for
}
interface CollisionPayload {
target: CollisionTarget; // the body this handler is registered on
other: CollisionTarget;
flipped: boolean; // true when `target` is Jolt's body 2
contactCount: number; // open sub-shape manifolds between the two bodies
}
interface CollisionEnterPayload extends CollisionPayload {
normal: THREE.Vector3; // world space, from `other` toward `target`
penetration: number;
points: THREE.Vector3[]; // world space; points.length === pointCount
pointCount: number;
}
```
`collisionEnter` and `collisionPersist` carry the manifold (`CollisionEnterPayload`);
`collisionExit`, `sensorEnter` and `sensorExit` carry `CollisionPayload`. `sleep` and `wake` carry
`ActivationPayload` (`{ body, handle }`), and `contactValidate` carries `ValidatePayload`
(`{ target, other, baseOffset }`). All of them are exported types.
> [!CAUTION]
> **Payloads are pooled and reused.** Read what you need inside the handler, or clone it; do not
> retain the payload, its `normal`, its `points` or either `CollisionTarget`. This is the contract
> r3f pointer events and rapier's `TempContactManifold` already carry. Under
> [``](/api/physics#debug) a payload is poisoned (`NaN` and frozen) after dispatch,
> so retaining one fails loudly in development and costs nothing in production.
```tsx
// do
setPoint(e.points[0].clone())}>{mesh};
// don't
setLastContact(e)}>{mesh};
```
`body` and `object` are `undefined` when the other side is a Jolt body that was never registered
with `BodySystem` — the [vehicle](/api/controllers#vehicle) chassis and the character controller's
rig anchor are both like this. `handle` is always valid.
### Which part of a compound was hit
`payload.targetSubShape` and `payload.otherSubShape` turn a raw `SubShapeID` into the ``
that produced it:
```ts
interface SubShapeRef {
id: number; // SubShapeID.GetValue()
index: number; // top-level compound child, or -1 for a leaf shape
userData: number; // the tag stamped on the /descriptor, or 0
descriptor: ShapeDescriptor | undefined; // what it was built from, `name` included
}
```
**They are resolved on first read.** Touching `targetSubShape` is what walks the shape; a handler
that never asks costs nothing per contact, which is why they are getters rather than fields. The
`SubShapeRef` is pooled like the rest of the payload.
`index` comes from the bit path Jolt packs into the id, and `userData` from
`Shape::GetSubShapeUserData`, which resolves all the way down to the leaf that was hit. The two
answer different questions: `index` is *which child of the body's shape*, `userData` is *which
declaration*, at any nesting depth. `descriptor` is only filled in when the body kept the
description it was built from — `` and the automatic `describeObject` path both do;
`bodySystem.addBody(object, { shape })` does not unless you also pass `shapeDescriptor`.
```tsx
console.log('hit', e.targetSubShape.descriptor?.name)}>
```
#### Per-`` handlers
A `` takes the same `onCollisionEnter` / `onCollisionPersist` / `onCollisionExit` /
`onSensorEnter` / `onSensorExit` props as a ``, scoped to itself:
```tsx
```
They subscribe on the parent body and filter by sub-shape, so they cost the same as the body's
own handlers plus one integer compare. A `` that has any of them and no explicit
`userData` is assigned one automatically (from the top of the 32-bit range, well clear of your
own numbering). Pass `userData` yourself to choose the tag, and `name` to label the descriptor.
Two caveats, both from how Jolt resolves a `SubShapeID`:
- a `` with handlers should be a **leaf**. Jolt resolves a contact down to the leaf shape,
so an intermediate compound's own tag is never what a contact reports. A *root* `` that
is the whole compound is the exception and is handled: it is the whole body, so its handlers
simply are the body's, unfiltered.
- for a two-child compound, child 1's id is `0xFFFFFFFF` — the same word as the "empty" id, since
Jolt pads the unused high bits with ones. `index` is therefore resolved against the body's
shape rather than from the id alone, and is `-1` only when the shape genuinely has no children.
### Ordering
Per **substep**, not per rendered frame — under a fixed `timeStep` one frame may run several:
```
beforeStep → pending body actions → joltInterface.Step() → queued events → afterStep
```
and within the event flush:
```
collisionExit, sensorExit → collisionEnter, sensorEnter → collisionPersist → sleep, wake
```
Exits come first so a handler keeping a "things I'm touching" set is never transiently
over-counted while a contact migrates between sub-shapes. Within one kind, world-level handlers
run before per-body ones, and per-body fires for Jolt's body 1 then body 2, each with its own
`target` / `other` / `flipped`.
**You may do anything from a handler.** It runs after `Step()` has returned, so adding, moving and
removing bodies is safe; the mutation lands at the top of the next substep. Each handler is
dispatched inside its own `try/catch`, so a throw is logged and does not abort the rest of the
frame.
### `onContactValidate`
The one exception to all of the above: it runs **synchronously inside the step**, because its
answer is what Jolt asked for. It must be fast and must not touch bodies. Return `false` to reject
the contact — one-way platforms, team pass-through:
```tsx
(e.other.object?.userData.team === 'blue' ? false : true)}>
```
### What Jolt does that may surprise you
- **A body going to sleep closes its contacts.** Jolt removes the manifolds of a deactivated body,
so a box resting on the floor reports `collisionExit` when it falls asleep and `collisionEnter`
again when something wakes it. If you need "is it still resting on something", use
`body.isContacting(handle)` together with `onSleep`.
- **Destroying a body closes its pairs.** The peer gets exactly one `collisionExit`, on the next
step, with `other.body === undefined` (the body is already gone).
- **`contactCount` counts sub-shape manifolds**, not contact points. A box on a floor is 1; a
compound shape resting on two of its children is 2.
- **Contacts on heightfields are unreliable** — they fire per triangle, so "stopped touching" may
never arrive.
### Cost
If nothing is listening, contacts cost a pair refcount and nothing else: no manifold is wrapped,
no payload is built, nothing is queued. Each `BodyState` publishes an `eventMask` bitfield and the
world emitter another; the Jolt callback ors them and bails out. `onCollisionPersist` is therefore
free to leave unsubscribed even though Jolt reports persisted contacts every step. Up to four
contact points are copied per event. The pair refcount behind `isContacting()` is maintained
whether or not anyone is listening, because it is public API in its own right.
### Design notes
The answers this implementation took to the event RFC's open questions, recorded here so they can
be changed:
- **Sensor event names.** `onSensorEnter` / `onSensorExit` are canonical (they match the existing
`isSensor` prop), **with** `onIntersectionEnter` / `onIntersectionExit` as documented
rapier-compatible aliases, resolved with one `??` at subscribe time. If both are given, the
canonical one wins.
- **Pooled payloads.** Pooled, with dev-mode poisoning under ``. Allocating fresh
payloads shows up as GC pressure in contact-heavy scenes, and the "don't retain the event"
contract already exists in r3f.
- **The 900 ms contact debounce is gone**, along with `BodyState.contactThreshold` and
`contactTimestamps`. Enter/persist/exit now come from Jolt's **sub-shape pairs** rather than a
body-level refcount: Jolt guarantees one `OnContactRemoved` per `OnContactAdded` for the same
`SubShapeIDPair`, so the first pair opening is the enter and the last one closing is the exit.
The debounce only existed to hide the flicker a body-level count showed when a manifold split
between sub-shapes, and a 900 ms window silently swallowed legitimate re-collisions (a bouncing
ball).
- **`` collision granularity.** Once per pair, `target` = the lower handle. `flipped` is
reported truthfully (true when the chosen target is Jolt's body 2) so `normal` stays
interpretable.
- **`on(type, fn)` vs named methods.** Both. `on()` is the documented primitive; the named
`onCollisionEnter(fn)` etc. are sugar.
The old `onContactAdded` / `onContactRemoved` / `onContactPersisted` props are kept as deprecated
aliases for `onCollisionEnter` / `onCollisionExit` / `onCollisionPersist`. They now receive the new
payload; their declared type never matched what was actually dispatched, so nothing that worked
before breaks.
]]>
` reads
the geometry of the meshes inside it and picks a shape for you.
## Automatic shapes
Every mesh under a `` contributes a shape, chosen from its geometry type:
| geometry | shape |
| --- | --- |
| `BoxGeometry` | box (from the geometry's own parameters) |
| `SphereGeometry` | sphere (bounding sphere radius) |
| `CapsuleGeometry` | capsule |
| `CylinderGeometry` | cylinder |
| anything else | **convex hull** of the merged vertices |
Non-parametric geometry — an imported GLTF, a lathe, a custom `BufferGeometry` — becomes a
convex hull. That is a deliberate default: hulls are fast and always valid, where a triangle mesh
is neither.
Override per body, or for the whole world:
```tsx
// one body
// every body that doesn't say otherwise
{children}
```
### `ShapeType`
```ts
type ShapeType =
| 'box'
| 'sphere'
| 'capsule'
| 'taperedCapsule'
| 'cylinder'
| 'taperedCylinder'
| 'convex'
| 'trimesh'
| 'heightfield'
| 'compound'
| 'staticCompound'
| 'mutableCompound'
| 'scaled'
| 'offsetCenterOfMass';
```
``, `` and `` all accept this one union
(issue #211 — it used to be two separate, slightly different unions called `AutoShape` and
`ShapeType`; `AutoShape` still exists as a deprecated alias of `ShapeType` so old imports keep
compiling). `'compound'` is a documented alias of `'staticCompound'`, normalised before anything
else looks at it — `type="compound"` and `type="staticCompound"` always produce identical
descriptors. `'scaled'` and `'offsetCenterOfMass'` are decorator shapes: they wrap a `child`
descriptor rather than describing geometry directly (see [the shape pipeline](#the-shape-pipeline)).
### Scaled meshes
A mesh below a described object contributes its own scale: `` inside a
`` produces a collider twice the size, not a collider at the drawn size. The **root**
object's scale is deliberately left out, because that belongs to the body
([`BodyState.scale`](#scaling), which wraps the shape in a `ScaledShape` you can change again
later) and baking it in as well would apply it twice. Pass `applyObjectScale` to
[`describeShape`](#the-shape-pipeline) if you want it baked in anyway.
### Trimeshes
A triangle mesh is only ever used when you ask for it — `shape="trimesh"` — because it is the
most restricted shape Jolt has. Per Jolt's own docs, dynamic and kinematic trimeshes cannot
collide with each other or with heightfields, and trying throws. Use them for static level
geometry; use `convex` (or a compound of convex parts) for anything that moves.
**Dynamic bodies cannot use a trimesh.** A Jolt mesh shape is a one-sided triangle soup with no
inside, so mesh vs mesh does not collide at all and a dynamic mesh body sinks through the world
and ends up with a `NaN` position ([issue #112](https://github.com/pmndrs/react-three-jolt/issues/112),
[Jolt docs](https://jrouwe.github.io/JoltPhysics/#dynamic-mesh-shapes)). Asking for one warns and
builds a convex hull of the same points instead. Pick a different behaviour with
`dynamicMeshStrategy`:
| value | behaviour |
| --- | --- |
| `'convex'` (default) | warn and use a convex hull of the mesh's points |
| `'error'` | throw, so the mistake is loud |
| `'decompose'` | reserved for a convex decomposition; throws with an explanation for now |
Static (and kinematic) bodies keep a real mesh shape. The converted body's mass and inertia come
from the hull's real `GetMassProperties()` rather than a fabricated solid box.
Set it per body with ``, or world wide with
`` (issue #211 — a per-body value always wins over the
world's default):
```tsx
// this one body throws instead of silently converting
// every dynamic body that doesn't say otherwise
{children}
```
It is also still a `GenerateBodyOptions` field for anyone building bodies directly:
```ts
const handle = bodySystem.addBody(mesh, { shapeType: 'trimesh', dynamicMeshStrategy: 'error' });
```
Note that the `'convex'` warning goes through `devWarn`, so you only see it after
[`setDebug(true)`](/advanced/memory#debug-output).
## Compound shapes
Put several meshes in one `` and you get a compound shape — positioned in the body's
local space, exactly as they render:
```tsx
```
## ``
When the collision shape should not match what is drawn — a simple hull around a detailed model,
an invisible trigger volume, a hand-tuned capsule for a character — declare it with ``.
It renders nothing; it only describes geometry to Jolt.
```tsx
import { RigidBody, Shape } from '@react-three/jolt';
;
```
A `` containing a `` waits for it before creating the body, and the top-level
`` becomes the body's shape. Nesting `` inside `` builds a compound.
| prop | type | |
| --- | --- | --- |
| `type` | [`ShapeType`](#shapetype) | default `'box'` |
| `position` | `number[]` | offset within the parent compound. Default `[0, 0, 0]` |
| `rotation` | `[number, number, number]` | Euler, radians. Default `[0, 0, 0]` |
| `scale` | `number[] \| number` | wraps the generated shape in a `ScaledShape` |
| `radius` | `number` | sphere, capsule, cylinder, tapered capsule |
| `height` | `number` | capsule, cylinder, tapered capsule, tapered cylinder |
| `topRadius` | `number` | tapered capsule / tapered cylinder |
| `bottomRadius` | `number` | tapered capsule / tapered cylinder; falls back to `radius` |
| `convexRadius` | `number` | box, cylinder, tapered cylinder, convex hull |
| `size` | `number[] \| number` | box extents; a number means a cube |
| `geometry` | `THREE.BufferGeometry` | derive a `convex` or `trimesh` shape from geometry |
| `points` / `vertices` / `verts` | `THREE.Vector3[] \| number[]` | hull points or mesh vertices |
| `indices` / `indexes` | `number[][] \| number[]` | trimesh indices |
| `object` / `mesh` | `THREE.Object3D` / `THREE.Mesh` | describe an object instead |
| `blockSize` | `number` | heightfield block size |
| `dynamic` | `boolean` | build a mutable compound instead of a static one |
| `ref` | `Ref` | `{ descriptor, shape }` |
Changing `size`, `radius`, `height`, `scale` or the children now replaces the shape exactly once
and releases the superseded one; unmounting releases everything the component owns.
> [!WARNING]
> `dynamic` cannot change after the first render — the component throws
> `Cannot change dynamic prop after initialization`.
### Mutable compounds
`` builds a real Jolt `MutableCompoundShape`, so a child `` mounting,
unmounting or *moving* edits the live compound in place instead of rebuilding it — and the body is
told, so its mass properties and broadphase bounds follow.
```tsx
import { RigidBody, Shape } from '@react-three/jolt';
function Growing({ extra }: { extra: boolean }) {
return (
{extra && }
);
}
```
Changing a child's *geometry* (its size, radius, type) still rebuilds the whole compound — only
position and rotation changes take the in-place path.
The same thing imperatively, on a [`BodyState`](/api/rigid-body#shape):
```ts
const index = body.addSubShape({ type: 'sphere', radius: 0.5, position: [0, 1, 0] });
body.modifySubShape(index, { position: [0, 1.5, 0] });
body.removeSubShape(index);
```
Each of those calls `AdjustCenterOfMass()` and `BodyInterface.NotifyShapeChanged` for you.
`removeSubShape` shifts every higher index down by one, so remove from the back if you are
holding several. Calling them on a body whose shape is a *static* compound throws a message
telling you to build it from a `{ type: 'mutableCompound' }` descriptor (or a ``)
instead.
The free functions `addSubShape(compound, descriptor, index?)`, `removeSubShape(compound, index)`
and `modifySubShape(compound, index, transform)` do the same to a bare shape, without notifying a
body. `isMutableCompoundShape(shape)`, `asMutableCompoundShape(shape)`, `subShapeCount(shape)` and
`getSubShapeTransform(compound, index)` round the set out.
## Heightfields
`` builds a plane and creates a matching Jolt heightfield body. There are three ways
to say what the terrain looks like, in order of precedence — `samples` wins over `generator`,
which wins over `url`:
```tsx
import { Heightfield } from '@react-three/jolt';
; // raw samples (#45)
Math.sin(x * 0.1) * 4} size={64} />; // callback (#45)
; // image, asynchronous
```
`samples` and `generator` are **synchronous**: the geometry and the body exist by the time the
component has mounted, so what is drawn and what is simulated are the same numbers. The `url`
path still loads (and can be superseded or cancelled) asynchronously — changing `url` cancels the
in-flight load and replaces the body, and at most one heightfield body exists per component at a
time (issue #152).
| prop | type | default | |
| --- | --- | --- | --- |
| `url` | `string` | — | heightmap image; also used as the display texture. Ignored once `samples`/`generator` is set |
| `texture` | `string` | — | use a different texture for display |
| `width` / `height` | `number` | `128` | world size of the plane — ignored when `samples`/`generator` set `scale` |
| `size` | `number` | `256` | **samples per edge** — see below |
| `displacementScale` | `number` | `25.6` | maximum height, `url` path only |
| `samples` | `Float32Array \| ArrayLike` | — | ready-made height samples, `size * size` numbers, row major (`row * size + col`) |
| `generator` | `(x: number, z: number) => number` | — | build the heights from a callback instead of samples/image — see [Generation](#generation) |
| `scale` | `[x, y, z]` | `[1, 1, 1]` | distance between samples on x/z and the height multiplier; used by `samples`/`generator`. The field is `(size - 1) * scale` across |
| `friction` | `number` | — | friction of the whole field (issue #46) |
| `restitution` | `number` | — | restitution (bounciness) of the whole field |
| `materials` | `SurfaceMaterial[]` | — | per-quad surface materials — see [Surface materials](#surface-materials) |
| `materialIndex` | `(x, z) => number \| Uint8Array \| ArrayLike` | — | which material each **quad** uses; only meaningful with 2+ `materials` |
| `blockSize` | `number` | `2` | Jolt's heightfield block size; `size` must be a multiple of it |
| `color` | `THREE.ColorRepresentation` | `'#8F2D56'` | material colour for the built-in plane material |
| `position` | `Vector3Tuple` | — | |
`materialIndex` (a callback) and `generator` are compared by identity, and `materials`/`samples`
by value (an inline array literal is fine for `materials`) — but a function has no value equality,
so a `materialIndex`/`generator` written inline creates a new function every render and rebuilds
the body every render. Define it outside the component, or memoize it.
### The sample-size rule
`size` is the number of height samples along each edge, and Jolt is strict about it (this is the
same check `describeShape`'s `heightfield` path and `generateHeightfield` use). The grid must be:
- **square** — `size × size` samples;
- a **multiple of the block size** (`2` by default, the `blockSize` prop);
- **at least two blocks** wide, i.e. `size >= 2 * blockSize`.
A power of two is the most efficient but is not required. Break the rule and you get a thrown
error naming the offending count rather than a silent failure — the plane is built with
`size - 1` segments per edge so that it produces exactly `size` samples.
> [!CAUTION]
> Heightfields are heavy. Keep them few and modest, and combine them with ordinary static bodies
> rather than making one enormous field. Contact events on heightfields are unreliable: they fire
> per triangle, which confuses the contact bookkeeping, so "stopped touching" may never arrive.
### Generation
`heightfield/heightfield.ts` builds Jolt-ready sample grids without an image, from noise or from
your own callback:
```ts
import { generateHeightfield, Heightfield } from '@react-three/jolt';
// deterministic: the same options always produce the same samples, on any machine
const { samples } = generateHeightfield({ size: 64, noise: 'psrd', octaves: 4, seed: 7 });
;
```
| function | |
| --- | --- |
| `generateHeightfield(options)` | → `{ samples, size, min, max }`, from noise (`'psrd'`, `'simplex'`, or your own `(x, z) => value`) |
| `samplesFromGenerator(size, generator, spacing?, blockSize?)` | → `{ samples, size, min, max }`, sampling any `(x, z) => height` callback — what `` uses internally |
| `heightfieldToGeometry(samples, size, scale?)` | → a `THREE.PlaneGeometry` with `size - 1` segments per edge, already rotated flat (+y up), one vertex per sample |
| `validateHeightfieldSize(size, blockSize?)` | the [sample-size rule](#the-sample-size-rule) check, thrown or returned |
| `toSampleArray(samples)` | normalise any `ArrayLike` to the `Float32Array` Jolt wants |
Both `generateHeightfield` and `generator` see **field-local world coordinates**: `(0, 0)` is the
centre of the field, x grows with the column index and z with the row index, both scaled by
`spacing`/`scale[0]`. That is exactly where the corresponding vertex of `heightfieldToGeometry`
ends up, so `(x, z) => Math.sin(x * 0.1)` does what it looks like it does, and feeding that
geometry to `bodySystem.addHeightfield` (what `` does under the hood) round-trips the
same numbers back out — render and physics can't drift apart.
Generation is **synchronous**, on the calling thread: a 512×512 field with 4 octaves is a handful
of milliseconds; a 2048×2048 one is not, and will drop frames. There is no built-in worker — pass
`samples` you generated wherever you like (a worker, the server, a previous session) to
`` and nothing here has to move off thread.
### Surface materials
Jolt's `HeightFieldShapeSettings` carries a `PhysicsMaterialList` and one material index per
**quad** (`(size - 1)^2` of them, row major), but its JS binding's `PhysicsMaterial` has no
properties of its own — friction always comes from the two bodies in contact, combined as
`sqrt(f1 * f2)`. `SurfaceMaterialTable` (`heightfield/materials.ts`) bridges that gap: it creates
one bare `PhysicsMaterial` per entry for the shape to index into, and remembers which pointer
means which `{ friction, restitution, name }` so the contact listener can resolve a sub-shape id
back to a friction value synchronously, inside `Step()`, and write it into that contact's
`ContactSettings.mCombinedFriction`/`mCombinedRestitution` — the same combine rules Jolt uses by
default (`sqrt` for friction, `max` for restitution), so a material only replaces *its* side of
the calculation.
```tsx
import { Heightfield, heightfieldMaterialIndices } from '@react-three/jolt';
const materials = [
{ name: 'ice', friction: 0.01, restitution: 0 },
{ name: 'grip', friction: 2, restitution: 0 }
];
// low friction on -x, high friction on +x - called once per quad, at its centre
const materialIndex = (x: number) => (x < 0 ? 0 : 1);
;
```
With only one entry in `materials`, it applies to the whole field and no index map is needed.
`heightfieldMaterialIndices(size, source, spacing?)` is what `` calls internally to
turn a `materialIndex` callback or array into the `Uint8Array` Jolt wants — use it directly when
building a body with `bodySystem.addHeightfield()` yourself. Jolt stores one `uint8` per quad, so
at most `MAX_SURFACE_MATERIALS` (256) materials per heightfield.
> [!NOTE]
> **Ownership**: materials are freed by the shape, not by you. `new PhysicsMaterial()` starts at
> ref-count 0; building the list and assigning it to the shape settings takes it to 2, and
> destroying the settings/list drops it back to the shape's own reference. Releasing the shape is
> what frees the materials — `SurfaceMaterialTable.dispose()` only drops JS-side bookkeeping (the
> pointer map and a scratch `SubShapeID`), never the materials themselves. You never call this
> yourself for `` — `BodyState.destroy()` disposes the table for you.
`friction`/`restitution` passed directly to `` (or `addHeightfield`'s options) set
the whole body's values instead, same as any other body — they combine with `materials` only in
that a quad with no material falls back to the body's own friction/restitution.
## Helper floors
```tsx
import { MeshFloor } from '@react-three/jolt';
import { Floor } from '@react-three/jolt/addons';
; // a static box RigidBody
; // a bumpy Jolt mesh floor
```
`` (addons) is a plain static `` with a box — what you want most of the time.
`` builds a Jolt mesh floor body directly and derives the three.js geometry from the
resulting shape; it is also a readable example of talking to Jolt by hand.
## The shape pipeline
Everything above is built on one pipeline. Three overlapping entry points used to each have their
own idea of how a three.js geometry maps onto a Jolt shape; they are now thin wrappers over
`describeShape` → `generateShape`.
```ts
import { describeShape, generateShape, releaseShape } from '@react-three/jolt';
import * as THREE from 'three';
const geometry = new THREE.CapsuleGeometry(0.5, 2);
// 1. describe: a plain object. Allocates nothing on the WASM heap and survives JSON.stringify.
const descriptor = describeShape(geometry);
// { type: 'capsule', radius: 0.5, height: 2, offset: [0, 0, 0] }
// 2. generate: a Jolt.Shape you own, with a reference count of 1
const shape = generateShape(descriptor);
// 3. release when you're done
releaseShape(shape);
```
| function | |
| --- | --- |
| `describeShape(objectOrGeometry, options?)` | → `ShapeDescriptor` |
| `describeGeometry(geometry, options?)` / `describeObject(object, options?)` | the two halves of it |
| `describeShapeFromOptions(type?, options?)` | build a descriptor from ``-style options |
| `generateShape(descriptor)` | → `Jolt.Shape`, caller owned, released with `releaseShape` |
| `createShapeSettings(descriptor)` | → `Jolt.ShapeSettings`, for compound children and body creation |
| `createShapeFromSettings(settings, destroySettings?)` | realise settings you already have |
| `releaseShape(shape)` | drop your reference |
| `descriptorKey(descriptor)` / `stableKey(value)` | a stable string identity (long vertex arrays are hashed) |
| `scaleShape(shape, scale)` | wrap in a `ScaledShape`; `AddRef`s the inner shape and hands back one reference |
| `validScaleFor(shape, scale)` | the nearest scale Jolt will accept for that shape |
`descriptorKey` is what ``'s effects depend on, which is why passing equal props does not
rebuild anything.
`describeShape` options:
| option | |
| --- | --- |
| `type` | force a shape type instead of inferring one |
| `convexRadius` | for the box/cylinder paths; clamped to what Jolt accepts |
| `blockSize` | heightfield block size |
| `applyObjectScale` | bake the **root** object's own scale in too (default `false` — see [Scaled meshes](#scaled-meshes)) |
### `ShapeDescriptor`
A serialisable description: a type tag, its size parameters, an optional local
`position`/`rotation` (used when it is a compound child; a root descriptor is positioned by the
body), an informational `offset`, and an optional `userData`.
| tag | fields |
| --- | --- |
| `box` | `size` (full extents), `convexRadius?` |
| `sphere` | `radius` |
| `capsule` | `radius`, `height` (cylindrical section, excluding the caps) |
| `taperedCapsule` | `height`, `topRadius`, `bottomRadius` |
| `cylinder` | `radius`, `height` (full), `convexRadius?` |
| `taperedCylinder` | `height`, `topRadius`, `bottomRadius`, `convexRadius?` |
| `convex` | `points` (flat `[x, y, z, …]`), `convexRadius?` |
| `trimesh` | `vertices`, `indices` (both flat) |
| `heightfield` | `heights`, `sampleCount`, `scale`, `blockSize?` |
| `staticCompound` | `children: ShapeDescriptor[]` |
| `mutableCompound` | `children: ShapeDescriptor[]` — editable at runtime, see [above](#mutable-compounds) |
| `scaled` | `child`, `scale` |
| `offsetCenterOfMass` | `child`, `centerOfMass` |
> [!NOTE]
> The tags are `convex` and `trimesh` — there is no `convexHull` or `mesh`. `compound` is accepted
> everywhere too, as a documented alias normalised to `staticCompound` (see [`ShapeType`](#shapetype)).
`offsetCenterOfMass` moves a shape's centre of mass without moving the shape: the classic
self-righting "weeble", and how you stop a vehicle or a character tipping over.
```ts
const weeble = generateShape({
type: 'offsetCenterOfMass',
child: { type: 'sphere', radius: 1 },
centerOfMass: [0, -0.7, 0]
});
```
An unknown tag throws naming the tag, rather than producing a NaN-sized shape.
### Scaling
`BodyState.scale` takes a `THREE.Vector3`, an array **or a plain number**, and wraps the body's
shape in a `ScaledShape`:
```ts
body.scale = 2; // uniform
body.scale = [2, 1, 2]; // wherever Jolt allows it
```
Re-scaling replaces the wrapper rather than stacking another one, and setting the scale it already
has is a no-op. A sphere, capsule or tapered capsule has a single radius and cannot be scaled
non-uniformly; asking for it falls back to a uniform scale of the largest component with a
[`devWarn`](/advanced/memory#debug-output). `` goes through the same path, applied
while the body is created.
### Building shapes yourself
The older entry points still work unchanged and are the shortest route when you already know the
type:
```ts
import { createShapeFromSettings, generateShapeSettings, releaseShape } from '@react-three/jolt';
const settings = generateShapeSettings('sphere', { radius: 0.5 });
const shape = createShapeFromSettings(settings); // frees `settings`, takes a reference for you
// ... later
releaseShape(shape);
```
`createShapeFromSettings` realises the shape, takes a reference on it, destroys the settings, and
throws with Jolt's own error message if creation failed — the manual `settings.Create().Get()`
dance leaks, and silently hands back an invalid shape on error. Bodies created through
`bodySystem` own their shape and release it when the body is removed; a shape you make yourself
is yours to `releaseShape`.
`getShapeSettingsFromGeometry`, `getShapeSettingsFromObject`, `generateShapeSettings` and
`generateCompoundShapeSettings` are all wrappers over the pipeline now, and keep their old
behaviour.
See [Memory & lifecycle](/advanced/memory) for the rules behind that.
]]>
` builds its collision shape from the meshes inside it. When you want the collider
to be something other than what is drawn — a trigger volume with no mesh, a simple box standing
in for a detailed model, several pieces making up one body — you place the colliders yourself.
```tsx
import { BallCollider, CuboidCollider, RigidBody } from '@react-three/jolt';
{/* two cheap primitives instead of a 8k-triangle hull */}
;
```
Every collider is a thin wrapper over [``](/api/shapes#): the same descriptor
pipeline, the same memory ownership, the same [per-sub-shape events](#events). What the wrappers
add is a typed `args` tuple that matches `@react-three/rapier`, so the most-copied snippet in the
r3f physics world works here unchanged.
## `args` are half extents
This is the one thing to hold on to. Rapier's convention — which these components follow — is
**half** extents: `` is a 1×1×1 cube, and the capsule,
cylinder and cone take a half height. ``'s own props keep three.js semantics (`size` is
the full extent, `height` is the full height), and the conversion happens in the collider
component and nowhere else.
```tsx
// the same 1 x 2 x 3 box, twice
```
## The components
| component | `args` | shape |
| --- | --- | --- |
| `` | `[hx, hy, hz]` half extents | box |
| `` | `[radius]` | sphere |
| `` | `[halfHeight, radius]` | capsule — total height `halfHeight * 2 + radius * 2` |
| `` | `[halfHeight, radius]` | cylinder |
| `` | `[halfHeight, radius]` | tapered cylinder with a top radius of `0` |
| `` | `[points]` | convex hull of a flat `[x, y, z, ...]` array |
| `` | `[vertices, indices]` | triangle mesh — **static bodies only** |
| `` | `[samples, size, scale]` | heightfield — **static bodies only** |
Jolt has no cone primitive, so `` is a `taperedCylinder` whose top radius is zero.
That is geometrically the same thing, and it is what the pipeline already knew how to build.
`` on a **dynamic** body is converted to a convex hull with a warning: Jolt has
no mesh-vs-mesh collision, so a dynamic mesh body falls through the world. See
[dynamic trimeshes](/api/shapes#trimeshes).
`` takes `size * size` height samples row
major, `size` samples per edge (Jolt needs `blockSize * 2^n`, and `blockSize` defaults to 2) and
a `[x, y, z]` scale: the distance between samples on x/z, and the height multiplier on y. To
build one from a three.js plane instead, use [``](/api/shapes#heightfields).
```tsx
const size = 64;
const samples = new Float32Array(size * size);
for (let i = 0; i < samples.length; i++) samples[i] = Math.sin(i * 0.1) * 2;
;
```
## Props
Beyond `args`, every collider takes:
| prop | type | notes |
| --- | --- | --- |
| `position` | `number[]` | offset **within the body**, not in the world |
| `rotation` | `[x, y, z]` | euler radians, within the body |
| `name` | `string` | label carried on the descriptor; comes back on a contact |
| `userData` | `number` | 32-bit tag stamped on the shape; comes back on a contact |
| `sensor` | `boolean` | see [sensors](#sensors) — it is a **body** property in Jolt |
| `friction` / `restitution` | `number` | see [materials](#friction-and-restitution) — also body level |
| `onCollisionEnter` etc. | handler | scoped to this collider, see [events](#events) |
Mass and density live on the body: ``. There is no per-collider density —
Jolt derives mass from the shape's own volume and then the body scales it.
A collider's `position`/`rotation` place it inside the body. A body with one collider at the
origin *is* that shape; anything else (an offset, or more than one collider) becomes a compound.
```tsx
{/* a dumbbell: two weights and a bar, one body */}
```
## ``
`colliders` says what to do about the meshes inside the body.
| value | meaning |
| --- | --- |
| *omitted* | every mesh contributes an automatically detected shape (the default, unchanged) |
| `false` | no automatic shape at all — the meshes are decoration, the colliders are the body |
| `'cuboid'` | force a box for the meshes |
| `'ball'` | force a sphere |
| `'hull'` | force a convex hull |
| `'trimesh'` | force a triangle mesh |
The four names are rapier's spelling of this library's [`ShapeType`](/api/shapes#shapetype)
values `box`, `sphere`, `convex` and `trimesh`. `` still works and means
the same thing.
```tsx
// a box collider around a detailed mesh
```
When a body has **both** meshes and colliders, they combine into one compound: the mesh shapes
first, then the colliders in mount order. Use `colliders={false}` when the meshes should not
contribute.
```tsx
// one body: the crate itself, plus a trigger volume sticking out of its lid
```
> [!NOTE]
> `` with no colliders inside it creates no body at all, and warns.
> A Jolt body cannot exist without a shape.
## Sensors
Jolt's sensor flag is a property of the **body** (`Body::SetIsSensor`), not of a sub-shape.
There is no way to have one solid collider and one pass-through collider on the same body, so
the policy is deliberately loud:
- **every** collider on the body is a `sensor`, and no mesh contributes a solid shape → the body
is made a sensor, with a warning telling you to write `` instead;
- **a mix** → a thrown error naming the counts, rather than a body that quietly behaves like one
of the two.
```tsx
// the honest spelling of a trigger volume
```
An explicit `isSensor={false}` wins over the colliders and warns about the conflict.
## Friction and restitution
Jolt only carries a `PhysicsMaterial` per shape for heightfields and meshes, so a box or a sphere
has nowhere per-sub-shape to keep a friction value. `friction`/`restitution` on a collider are
therefore applied to the whole body, with a warning. Prefer saying so directly:
```tsx
```
`` wins when both are set. Per-surface materials do exist for heightfields —
see [heightfield materials](/api/shapes#heightfields).
## Events
A collider's contact handlers fire only for contacts on **its own** sub-shape, which is the whole
point of putting several of them on one body. They are the same handlers `` takes, and the
payload is the same one [``](/api/rigid-body#events) delivers.
```tsx
console.log('left half', event.other.handle)}
/>
console.log('right half', event.other.handle)}
/>
```
`event.targetSubShape` carries the `name` and `userData` of the collider that was hit, so a
single body-level handler can do the routing instead if you prefer.
## Mutable compounds
A compound `` builds out of its colliders is a static one: adding, removing or moving
a collider rebuilds it. When colliders change every frame, wrap them in
[``](/api/shapes#compound-shapes) and leave it as the body's only shape, so the
`MutableCompoundShape` is edited in place instead.
```tsx
type Piece = { id: string; position: [number, number, number] };
const pieces: Piece[] = [];
{pieces.map((piece) => (
))}
;
```
]]>
[!CAUTION]
> **Every query object owns Jolt (WASM) memory and must be destroyed.** The `useRaycaster` /
> `useMulticaster` hooks do it for you on unmount. Anything you get from `physicsSystem.get*()`
> yourself is yours to `destroy()` — see [Lifetime](#lifetime).
## Raycasting
```tsx
import { Raycaster, RaycastHit, useRaycaster } from '@react-three/jolt';
import { useEffect } from 'react';
export function Laser() {
const raycaster: Raycaster = useRaycaster([0, 5, 0], [0, -10, 0]);
useEffect(() => {
raycaster.cast(
(hit: RaycastHit) => {
console.log(hit.position, hit.distance, hit.impactNormal, hit.bodyHandle);
},
() => console.log('no hit')
);
}, [raycaster]);
return null;
}
```
`useRaycaster(origin?, direction?, type?)` creates a caster, applies the arguments, and destroys
it when the component unmounts. **`direction` is not normalised** — its length is the length of
the ray, exactly like `THREE.Raycaster`'s `far`.
### Casting
| call | |
| --- | --- |
| `cast(onHit?, onMiss?)` | cast with the current `origin`/`direction` |
| `castFrom(origin, onHit?, onMiss?)` | move the origin, keep the direction |
| `castTo(destination, onHit?, onMiss?)` | keep the origin, aim at a point |
| `castBetween(origin, destination, onHit?, onMiss?)` | both |
| `set(origin, direction)` | just update, don't cast |
`cast()` also *returns* the hit (or the array of hits in `'all'` mode), so the handlers are
optional.
### Collectors
`setCollector(type)`, or the third argument to `useRaycaster`, chooses what Jolt collects:
| type | |
| --- | --- |
| `'closest'` | the nearest hit (default) |
| `'any'` | the first hit found — cheapest, use for line-of-sight tests |
| `'all'` | every hit along the ray; `hits` is the full array |
`cullBackFaces` (default `true`) skips triangles facing away from the ray.
### `RaycastHit`
| member | |
| --- | --- |
| `position` | world-space hit point (`THREE.Vector3`) |
| `start` / `end` | the ray, as cast |
| `distance` | from `start` to `position` |
| `normal` / `direction` | the ray's own normalised direction |
| `impactNormal` | the **surface** normal at the hit — this is the one you usually want |
| `bodyHandle` | pass to `bodySystem.getBody()` |
| `shapeIdValue` | sub-shape id within a compound |
| `index` | position in the hit list |
### `useMouseRaycaster`
The physics answer to `THREE.Raycaster.setFromCamera()`: it builds a world-space ray from the
pointer and the camera and casts it against Jolt bodies instead of the three.js scene graph.
```tsx
import { useMouseRaycaster } from '@react-three/jolt';
import { useFrame } from '@react-three/fiber';
export function Picker() {
const { hit, raycaster } = useMouseRaycaster({
onHit: (h) => {
if (!Array.isArray(h) && h) console.log(h.bodyHandle);
}
});
useFrame(() => {
const current = hit.current;
if (current && !Array.isArray(current)) highlight(current.bodyHandle);
});
raycaster.isDebugging = false;
return null;
}
```
| option | default | |
| --- | --- | --- |
| `mode` | `'frame'` | rebuild the ray every rendered frame, so it stays right when only the camera moves. `'pointermove'` rebuilds it only on real pointer moves |
| `type` | `'closest'` | collector, as above |
| `length` | `camera.far` | ray length |
| `onHit` | — | called with the latest hit (or `undefined` on a miss) every cast |
| `filter` | — | swap in your own `bpFilter` / `objectFilter` / `bodyFilter` / `shapeFilter` |
It returns `{ raycaster, hit }`, where **`hit` is a mutable ref updated in place** — read
`hit.current` from `useFrame`, an event handler or `onHit`. It is deliberately not React state: a
fast-moving pointer would otherwise re-render every frame.
> [!CAUTION]
> Passing a `filter` **transfers ownership of it** to the raycaster this hook creates: it replaces
> (and frees) the default filter, and will free yours on the next filter change or on unmount.
> Don't share one filter instance across several of these. `filter` participates in the hook's
> dependencies by identity, so memoise it.
Unlike its siblings, this hook destroys its raycaster **when `type` or `filter` change** as well
as on unmount — see [Lifetime](#lifetime).
### Multicaster
One raycaster, many origins or many origin/destination pairs — sweeps, foot probes, spread
patterns:
```tsx
import { useMulticaster } from '@react-three/jolt';
import { useEffect } from 'react';
import * as THREE from 'three';
export function Sweep() {
const multicaster = useMulticaster();
useEffect(() => {
multicaster.positions = [new THREE.Vector3(-5, 5, 0), new THREE.Vector3(5, 5, 0)];
multicaster.direction = new THREE.Vector3(0, -10, 0);
multicaster.cast();
}, [multicaster]);
return null;
}
```
`cast()` walks `positions` with the shared direction; `castRays()` walks `rays`
(`{ origin, destination }[]`). Both fill `hits` (flat) and `results` (grouped per ray), and both
clear them first.
### AdvancedRaycaster
`useAdvancedRaycaster()` exposes Jolt's collector callbacks so you can filter or bail out during
the cast:
```ts
import type Jolt from 'jolt-physics';
const caster = useAdvancedRaycaster();
caster.onBody((body: Jolt.Body) => {
/* inspect each body the ray reaches */
});
caster.addHit((result: Jolt.RayCastResult) => {
/* return truthy to shorten the cast to this hit */
});
caster.onReset(() => {
/* also resets the collector's early-out fraction */
});
```
The callbacks hand you **raw Jolt objects** the collector owns. Read what you need and return —
do not keep them, and never `destroy()` them. They are declared loosely, so annotate the
parameters yourself as above.
`cast()` resets the collector *before* casting (resetting after would wipe the hits it just
collected). In `'all'` mode the success handler is called once per hit.
## Shapecasting
A shapecast sweeps a whole shape along a direction — "will this capsule fit through there", a
thick ground probe, a camera collision test. There is no hook yet; take one from the physics
system and destroy it yourself.
```tsx
import { Shapecaster, ShapecastHit, useJolt } from '@react-three/jolt';
import { useEffect } from 'react';
import * as THREE from 'three';
export function GroundProbe() {
const { physicsSystem } = useJolt();
const shapecaster: Shapecaster = physicsSystem.getShapecaster();
useEffect(() => () => shapecaster.destroy(), [shapecaster]);
useEffect(() => {
shapecaster.origin = new THREE.Vector3(0, 5, 0);
shapecaster.direction = new THREE.Vector3(0, -6, 0);
shapecaster.cast((hit: ShapecastHit) => {
console.log(hit.position, hit.distance, hit.bodyHandle);
});
}, [shapecaster]);
return null;
}
```
It casts a 0.5-radius sphere by default. `shape` takes any `Jolt.Shape` (build one with
[`createShapeFromSettings`](/api/shapes#building-shapes-yourself)); `origin`, `rotation`, `scale`
and `direction` accept three.js values and rebuild the underlying cast for you.
`ignoreBackfaceTriangles` and `ignoreBackfaceConvex` (both `true`) control back-face handling,
and `setCollector('closest' | 'any' | 'all')` works as it does for rays.
`ShapecastHit` carries `position`, `start`, `end`, `distance`, `normal`/`direction`,
`impactNormal`, `bodyHandle`, `shapeIdValue` and `index` — the same surface as `RaycastHit`.
## ShapeCollider
`CollideShape` asks a different question: not "what will I hit if I move", but "what am I
overlapping right now, and by how much". This is what you want for triggers, spawn checks and
push-out resolution.
```tsx
import { ShapeCollider, useJolt } from '@react-three/jolt';
import { useEffect } from 'react';
import * as THREE from 'three';
export function Overlap() {
const { physicsSystem } = useJolt();
const collider: ShapeCollider = physicsSystem.getShapeCollider();
useEffect(() => () => collider.destroy(), [collider]);
useEffect(() => {
collider.position = new THREE.Vector3(0, 1, 0);
collider.setCollector('all');
collider.cast((hits: typeof collider.hits) => {
hits.forEach((hit) => console.log(hit.penetrationDepth, hit.contactNormal));
});
}, [collider]);
return null;
}
```
Set `position`, `rotation` or `matrix` and the world transform is rebuilt **in place** — no
allocation per frame, which is what makes it safe to drive from `useFrame` (the camera rig does).
`cast()` returns the single hit when there is exactly one, the `hits` array when there are more,
and `false` on a miss.
### Shape ownership
`collider.shape` is reference counted, not owned outright:
```ts
const shape = generateShape({ type: 'box', size: [1, 1, 1] });
collider.shape = shape; // the collider takes its own reference (AddRef)
releaseShape(shape); // you can drop yours immediately; the collider still holds one
```
Assigning a new shape `AddRef`s it and `Release`s the previous one; `destroy()` releases the
current one rather than hard-destroying it. So a caller that also holds — and later frees — a
reference to the same shape never ends up with a dangling pointer. Assigning the shape it already
has is a no-op.
`Shapecaster.shape` does **not** do this: it writes the shape straight into the cast, so keep your
own reference alive for as long as the shapecaster uses it.
### `CollisionResult`
| member | |
| --- | --- |
| `contactPointOn1` / `contactPointOn2` | world-space contact points |
| `penetrationAxis` | the raw Jolt axis (not normalised) |
| `contactNormal` | `penetrationAxis`, normalised |
| `penetrationDepth` | how deep the overlap is |
| `bodyHandle` | the other body |
| `shapeMatrix` | the transform the query was run with |
| `subShapeId1` / `subShapeId2` | **raw `Jolt.SubShapeID` objects owned by the collector** — read `.GetValue()`, never destroy |
## Debugging
Casters draw themselves when you ask — they deliberately ignore ``, because a
caster can fire thousands of times a second.
```ts
raycaster.initDebugging(scene); // adds a debug object to the scene
raycaster.drawPoints = true; // start/hit/end points
raycaster.drawMarkers = true; // markers at each hit
raycaster.lineColor = '#68D8D6';
raycaster.stopDebugging();
```
Raycasters, shapecasters and the multicaster's inner raycaster all share this API.
### Markers
A `Raycaster`'s markers are a small ring plus a normal-indicator line, **oriented along the hit
surface normal** (`Quaternion.setFromUnitVectors((0, 1, 0), hit.impactNormal)`) rather than
axis-aligned to the world. A degenerate normal falls back to world up.
The debug objects are **pooled per caster**: one shared material per drawing type, geometry
reused through `setFromPoints`, and one marker group per hit index, so repeated `drawMarker()` /
`cast()` calls update an existing group's transform instead of allocating. Markers past the
current hit count are hidden rather than destroyed. `destroy()` and `clearDebugging()` dispose the
pools.
`Shapecaster` behaves the same way: it has the same pooled drawing and the same normal-oriented
markers, and its `destroy()` / `clearDebugging()` dispose the pools too.
## Shared base
`Raycaster`, `Shapecaster`, `ShapeCollider` and `Multicaster` used to each duplicate the same
filter setup, destroy bookkeeping and (for the ray-like casters) debug-drawing code (issue #217).
They now all extend a small hierarchy instead — useful mainly if you're building a fifth query
type, or want to know exactly what `destroy()` covers:
| class | extends | adds |
| --- | --- | --- |
| `QueryBase` | — | the `joltPhysicsSystem`/`joltInterface`/`bodyInterface` wiring, the four filters (`bpFilter`, `objectFilter`, `bodyFilter`, `shapeFilter`) that make a query cast as if a dynamic object, and an idempotent `destroy()` template |
| `CastQueryBase` | `QueryBase` | the collector lifecycle, the `cast()`/`castFrom()`/`castTo()`/`castBetween()` family, and the pooled debug-drawing machinery described [above](#debugging) |
| `HitBase` | — | the shape every single-cast result shares: a `start`/`end`/`position` triple, a `bodyHandle`/`shapeIdValue` pair, and the `impactNormal` getter that turns those back into a live surface normal |
`Raycaster` and `Shapecaster` extend `CastQueryBase` (and `AdvancedRaycaster` extends
`Raycaster`); `ShapeCollider` and `Multicaster` extend `QueryBase` directly since neither uses the
ray-cast collector family. `RaycastHit` and `ShapecastHit` both extend `HitBase`, which is why
their fields line up exactly (see [`RaycastHit`](#raycasthit) above).
`QueryBase` implements `destroy()` once, as a template: it flips an internal `destroyed` flag and
calls the abstract `releaseResources()` every subclass implements for whatever else it allocated.
**This means `destroy()` is idempotent on every query type now** — `Raycaster`, `Shapecaster`,
`ShapeCollider` and `Multicaster` alike, not just `ShapeCollider` as before #217. Calling it twice
is safe and the second call does nothing, because [`Raw.module.destroy()` on an
already-freed object does not throw — it silently double-frees](/advanced/memory#the-three-rules).
`successHandler`/`failHandler` on `cast()`/`castFrom()`/`castTo()`/`castBetween()` stay untyped
(`any`) rather than a `(hit?: THit | THit[]) => void` alias: TypeScript checks a plain
function-type parameter's parameter list contravariantly, so a caller passing a narrower callback
— `(hit: RaycastHit) => void`, the shape every example above uses — would stop compiling even
though `cast()` never actually calls it with `undefined` or an array unless the hit shape says so.
Annotate your own handler's parameter as shown in the examples on this page.
## Lifetime
Every caster allocates Jolt filters, a collector and settings objects on the WASM heap. They are
freed only by `destroy()`.
| hook | frees its caster |
| --- | --- |
| `useMouseRaycaster()` | on unmount **and** whenever `type` / `filter` change |
| `useRaycaster()` | unmount only — a change to `origin` / `direction` / `type` builds a new one and leaks the old |
| `useMulticaster()` | unmount only, and it frees the inner `Raycaster` rather than calling `Multicaster.destroy()` |
| `useAdvancedRaycaster()` | **never** — call `destroy()` yourself |
`useRaycaster`'s `origin` and `direction` are compared by identity, so passing an inline array or
`Vector3` re-creates the caster on every render. Hoist them, or use `raycaster.set(...)`.
- Anything from `physicsSystem.getShapecaster()` / `getShapeCollider()` / `getRaycaster()` is
yours: pair it with `useEffect(() => () => caster.destroy(), [caster])`.
- `destroy()` is idempotent on every query type — see [Shared base](#shared-base). `ShapeCollider`
goes one step further and also nulls out every field it freed, so a stray call afterwards fails
loudly instead of touching freed memory; the others just no-op on a second call.
- There is no `useShapecaster` / `useShapeCollider` hook yet.
### Objects Jolt hands you
Some query surfaces give you real Jolt objects: `AdvancedRaycaster`'s collector callbacks,
`CollisionResult.subShapeId1/2`, `BodyState.body`, anything reached through `useJolt().jolt`.
> [!CAUTION]
> **Never `destroy()` an object you did not create.** Jolt's Emscripten binding returns
> by-value results as a pointer to *one shared static temporary per function* — freeing it hands
> memory the binder still owns back to the allocator, which reuses it immediately, and the crash
> surfaces somewhere else entirely. The same goes for references into a collector's storage.
> Copy what you need out (`vec3.three(v)`, `id.GetValue()`) and let go.
Full rules in [Memory & lifecycle](/advanced/memory).
]]>
` 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
[``](/api/rigid-body): `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**.
```tsx
```
```tsx
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.
### ``
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`](/api/rigid-body#bodystate):
```ts
body.group = 7;
body.subGroup = 2;
// `collisionGroup` / `collisionSubGroup` are aliases of the same pair
body.collisionSubGroup = 3;
```
Or, creating bodies by hand:
```ts
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:
```ts
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`](/advanced/memory#debug-output) 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 `` 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](/api/rigid-body#motion-sources).
```tsx
```
## 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](/advanced/memory) if you replace one.
]]>
[!WARNING]
> These components work, but they are the roughest corner of the API: several props are not
> wired up yet, and the components are thin wrappers over systems that are much more capable
> than the props suggest. Where a prop is missing, reach for the system.
## CharacterController
A Jolt `CharacterVirtual` — a capsule that walks, runs, crouches, jumps, climbs stairs and
slides — driven by keyboard/gamepad commands out of the box.
```tsx
import { Physics } from '@react-three/jolt';
import { CameraRig, CharacterController } from '@react-three/jolt/controllers';
export function Player() {
return (
);
}
```
| prop | type | default | |
| --- | --- | --- | --- |
| `radius` | `number` | `1` | capsule radius, applied at creation and on every change |
| `height` | `number` | `2` | capsule height, same |
| `debug` | `boolean` | `true` | draws the capsule. Note the default is *on* |
| `position` | `Vector3` \| tuple | | the character's real position — sets it at creation, teleports on change |
| `innerBody` | `boolean` | `false` | give the character a body the world collides with — see below |
| `innerBodyLayer` | `number` | `Layer.MOVING` | object layer for that body |
| `ref` | `Ref` | | hands back the system itself |
Default bindings (from the [commander](/api/addons)): `WASD`/left stick to move, `Space` to
jump, `Shift` to run, `C` to crouch. The move direction is rotated by the camera's horizontal
rotation, so "forward" means forward on screen.
### Being collided with
A `CharacterVirtual` is not part of the simulation. It collides *against* the world — it walks on
floors, it is stopped by walls — but the world does not collide against it. Push a crate into an
NPC built this way and the crate goes straight through.
`innerBody` fixes that. Jolt gives the character a real kinematic body, keeps it glued to the
capsule, and everything else in the world collides with that:
```tsx
```
This is what Jolt's standard (non-virtual) `Character` class is usually reached for. That class
isn't exposed by `jolt-physics`, and its author considers it inferior to `CharacterVirtual` — an
inner body gets you the property it was wanted for without giving up stair walking, crouching or
any of the rest.
It costs one extra body and the narrowphase work that implies, so it is off by default. Turn it
on for characters other things bump into (NPCs, other players) and leave it off for a
first-person player that nothing ever collides with.
> [!NOTE]
> `innerBody` and `innerBodyLayer` are read **once**, when the controller is built: Jolt creates
> the inner body inside the `CharacterVirtual` constructor and has no API to add, remove or
> re-layer one afterwards. Changing either prop remounts the controller. `setCapsule` does keep
> the inner body's *shape* in step, so resizing works normally.
`CharacterControllerSystem` exposes `hasInnerBody` and `innerBodyId` (the Jolt `BodyID`, or
`undefined`). The body belongs to the character — don't remove it through `BodySystem`, and
don't hold the id past `destroy()`.
### `CharacterControllerSystem`
The component creates one and shares it through `CharacterControllerContext`:
```tsx
import {
CharacterControllerContext,
type CharacterControllerSystem
} from '@react-three/jolt/controllers';
import { useContext } from 'react';
function Stamina() {
// the context is untyped today, so annotate it yourself
const { characterSystem } = useContext(CharacterControllerContext) as {
characterSystem?: CharacterControllerSystem;
};
// characterSystem?.isRunning, .isExhausted, .hangtime, ...
return null;
}
```
Movement and state:
| member | |
| --- | --- |
| `move(direction)` | world-space direction; length is ignored, speed comes from the settings |
| `jump()` | respects `jumpLimit`, `allowJumpWhileFalling` and exhaustion |
| `startRunning(speed?)` / `stopRunning()` | |
| `setCrouched(crouched, forceUpdate?)` | swaps to the crouching capsule |
| `position`, `rotation`, `linearVelocity` | three.js types, get/set |
| `isMoving`, `isRunning`, `isCrouched`, `isSliding`, `isGrounded`, `isExhausted`, `isRotating`, `hangtime` | state |
| `anchor` | the `BodyState` other things (like the camera rig) attach to |
| `on(action, callback)` | subscribe to one action; returns an unsubscribe |
| `addActionListener(fn)` | subscribe to *every* action; returns an unsubscribe |
| `destroy()` | idempotent; called for you when the component unmounts |
Actions, and what they carry:
| action | payload |
| --- | --- |
| `'falling'` | `true` on the first frame of hangtime, then the hangtime in seconds |
| `'jump'` | the jump counter |
| `'crouched'` | `boolean` |
| `'running'` | the new speed on start, `0` on stop |
| `'exhausted'` | the exhaustion time limit |
| `'exausted'` | `false` — the spelling is a bug in the source; it is a *different* string from `'exhausted'` |
```ts
import type { CharacterControllerSystem } from '@react-three/jolt/controllers';
function watchJumps(characterSystem: CharacterControllerSystem) {
const off = characterSystem.on('jump', (_action, count) => console.log('jump', count));
off();
}
```
`removeActionListener(fn)` still exists and is deprecated — keep the unsubscribe instead.
Tuning (all plain properties with sensible defaults):
`characterSpeed` (6), `characterSpeedCrouched` (3), `characterSpeedExhausted` (2), `jumpSpeed`
(15), `jumpLimit` (2), `jumpDegradeFactor` (0.5), `characterHeightStanding` (2),
`characterRadiusStanding` (1), `characterHeightCrouching` (1), `characterRadiusCrouching` (0.8),
`allowAirbornControl` (true), `enableCharacterInertia` (true), `enableCharacterRotation` (true),
`enableWalkStairs` (true), `enableStickToFloor` (true), `allowSliding` (false), `allowRunning`
(true), `enableExhaustion` (true), `allowJumpWhileFalling` (true), `runningTimeLimit` (5000 ms),
`exauhstionTimeLimit` (7000 ms, spelled as in the source), `maxRotationSpeed` (0.1 rad/frame),
`lerpFactor` (0.4).
Stair and floor behaviour maps onto Jolt's own settings: `walkStairsStepUp`,
`walkStairsMinStepForward`, `walkStairsStepForwardTest`, `walkStairsStepDownExtra`,
`stickToFloorStepDown`. Ground state is readable through `groundState`, `isSupported`,
`isFalling`, `groundNormal`, `groundPosition`, `groundVelocity`, `groundMaterial` and
`groundBodyHandle`.
The character uses `Layer.RIG`, so it does not collide with other rigs — see
[layers](/api/collision-groups#preset-layers).
### Head and ceiling collision
`ExtendedUpdate`'s own ground/wall/ceiling classification only tells Jolt how to *sweep* the
character; it does nothing to the character's velocity. Without help, a character jumping into a
low overhang keeps its upward velocity until gravity alone brings it back down, which reads as
getting stuck under the ceiling for a beat too long (issue #88). `headAngle` and `onHeadHit` are
the fix:
```tsx
import { CharacterController } from '@react-three/jolt/controllers';
import type { HeadHitInfo } from '@react-three/jolt/controllers';
{
console.log('bonk', info.normal, info.previousVerticalSpeed);
}}
/>;
```
| prop / member | | |
| --- | --- | --- |
| `headAngle` | `number` (radians) | half-angle of the cone around straight-up within which a contact counts as a head/ceiling hit rather than walkable ground or a wall. Default 30°. A contact whose normal satisfies `contactNormal · up < -cos(headAngle)` cancels the character's upward velocity |
| `onHeadHit` | `(info: HeadHitInfo) => void` | called once per **new** contact inside that cone while the character is moving upward — not every step the contact persists, and not for ground/wall contacts |
`HeadHitInfo` carries `normal` (world-space contact normal, pointing away from the obstacle
towards the character) and `previousVerticalSpeed` (the up-axis speed at the moment of the hit,
before it is cancelled). Both are set on the `CharacterControllerSystem` directly too
(`characterSystem.headAngle`, `characterSystem.onHeadHit = ...`) if you're driving the system
without the component.
### Events
`CharacterControllerSystem.events` is an `Emitter` — the same primitive
`bodyState.on()` and the world's `` use. Every movement event is an **edge**, derived
once per pre-step from ground state and velocity relative to whatever is carrying the character,
and every one of them is *also* emitted as an `'action'` under the same name, so the older
`characterSystem.on('jump', fn)` (filtered-action) API and `characterSystem.events.on('jump', fn)`
see the same things.
```tsx
import { useCharacterEvent } from '@react-three/jolt/controllers';
console.log('moving', speed)}
onStop={() => console.log('stopped')}
onSlide={(speed) => console.log('sliding', speed)}
onSlideEnd={() => console.log('slide ended')}
onJump={(count) => console.log('jump', count)}
onLand={(airtime) => console.log('landed after', airtime, 's')}
onGround={() => console.log('grounded')}
onAirborne={() => console.log('airborne')}
onContactAdded={(payload) => console.log('touched', payload.handle)}
/>;
```
`useCharacterEvent(system, type, handler)` is the imperative form the component's `on*` props are
built from — subscribe from your own component once you have a `CharacterControllerSystem` (from
`CharacterControllerContext`, or one you built yourself):
```tsx
import { useCharacterEvent } from '@react-three/jolt/controllers';
import type { CharacterControllerSystem } from '@react-three/jolt/controllers';
function Footsteps({ characterSystem }: { characterSystem?: CharacterControllerSystem }) {
useCharacterEvent(characterSystem, 'move', (speed) => {
if (speed > 4) playRunSfx();
});
return null;
}
```
Like `useBodyEvent`, its dependency is *whether* a handler was passed rather than its identity, so
an inline arrow does not resubscribe every render.
`CharacterEventMap`:
| event | payload | |
| --- | --- | --- |
| `action` | `(name, payload)` | every action, including all the ones below |
| `move` | `speed: number` | started moving under its own power; `speed` is m/s relative to the ground |
| `stop` | — | stopped moving under its own power |
| `slide` | `speed: number` | started sliding down something too steep to stand on |
| `slideEnd` | — | stopped sliding |
| `jump` | `count: number` | a jump was accepted; which jump of the allowed sequence it was |
| `land` | `airtime: number` | touched down, after being unsupported for `airtime` seconds |
| `ground` | — | became supported by something (always paired with `land`) |
| `airborne` | — | stopped being supported by anything |
| `crouch` / `stand` | — | |
| `contactAdded` / `contactPersisted` / `contactRemoved` | `CharacterContactPayload` | a contact with a body, forwarded from Jolt's `CharacterContactListener`. **Pooled** — read it inside the handler, don't keep it. Dispatched after the character update returns, never from inside `ExtendedUpdate` |
`CharacterContactPayload` carries `body`/`object` (`undefined` for a Jolt body `BodySystem` never
registered), `handle`, `subShapeId`, `position` (zeroed for `contactRemoved`, which Jolt gives no
geometry) and `normal` (pointing from the character into the other body).
Component props: `onMove`, `onStop`, `onSlide`, `onSlideEnd`, `onJump`, `onLand`, `onGround`,
`onAirborne`, `onCrouch`, `onStand`, `onContactAdded`, `onContactPersisted`, `onContactRemoved` and
`onAction` mirror the event map one-for-one, plus `moveThreshold` (default 0.5 m/s — the relative
speed above which the character counts as moving) and `slideThreshold` (default 0.5 m/s, along the
surface). Nothing subscribes for a prop you don't pass, so an unused contact stream costs one `&`
per contact and nothing else.
> [!CAUTION]
> Everything in `CharacterEventMap` is dispatched **after** `ExtendedUpdate` returns — Jolt's
> `CharacterContactListener` fires from inside it, where adding or removing a body is illegal and
> the pointers it hands the callback are into memory Jolt reuses. Never call anything that adds,
> removes, or otherwise mutates bodies from inside a listener registered here — by the time your
> handler runs the step has already finished, but the contract still matters for the payload
> itself: it is pooled (one object reused for every contact in a step), so copy what you need and
> return.
## CameraRig
A physics-aware third-person camera: a boom arm on a chain of spaces (`base` → `collar` →
`camera`) that collides with the world instead of clipping through it, and follows an anchor.
```tsx
```
Inside a ``, `` finds the character through context and attaches
itself automatically. Otherwise pass a body:
```tsx
```
Mouse, touch, stick look and wheel zoom are bound through
[`useLookCommand`](/api/addons#uselookcommand); `R` resets the camera behind the anchor.
### Options
Every option below is also a `` prop. They reach the boom **before the rig is first
stepped**, so the camera opens already framed instead of snapping into place on frame one.
Changing one afterwards updates the live rig through `setOptions()` — the manager is memoised and
is never rebuilt for a prop change, and a new `distance` is eased into rather than snapped to.
| option | default | |
| --- | --- | --- |
| `cameraPosition` | `(0, 0, 0)` | where the rig's `main` camera starts, in rig space. A camera anywhere other than the origin **defines the boom's length, pitch and yaw** |
| `distance` | `5` | boom length, metres. An explicit value wins over `cameraPosition` |
| `minDistance` / `maxDistance` | `0.1` / `100` | zoom limits |
| `pitch` | `0` | starting pitch, radians; negative looks down |
| `minPitch` / `maxPitch` | `-1.5` / `0.5` | what a look command may reach |
| `yaw` | `0` | starting yaw about world up |
| `collisionRadius` | — | radius of the camera's collision sphere; left to the shape collider's own default when unset |
| `smoothing` | `0.5` | lerp factor while the boom changes length |
| `lookSpeed` / `zoomSpeed` | `1` / `1` | input multipliers |
| `allowCameraClipping` | `false` | skip the collision, obstruction and shapecast tests entirely |
| `obstructionBuffer` | `0.01` | gap kept between the camera and whatever obstructs it |
| `target` | `(0, 0, 0)` | point the boom frames, relative to the pivot |
| `updateMode` | `'demand'` | `'demand'` recomputes on every command, `'additive'` once per frame |
| `followTarget` | — | `BodyState` to follow, before the first step |
| `anchorOffset` | `(0, 2, 0)` | where on the body the boom hangs |
| `positionUpdateType` | `'distance'` | or `'fixed'` |
| `debug` | `true` | draw the rig's spaces |
```tsx
```
### Follow modes
A PC-style rig only *translates* with its anchor, so running off sideways leaves you looking at
the character's ear. `followMode` decides where the rig points:
| mode | |
| --- | --- |
| `'free'` (default) | the boom only turns when the player turns it |
| `'movement'` | the boom eases round to trail the character's horizontal velocity, so heading off in a new direction swings the camera in behind you |
| `'lookAt'` | the boom eases round so `lookAtTarget` stays framed past the character |
```tsx
```
| option | default | |
| --- | --- | --- |
| `followMode` | `'free'` | |
| `rotationSpeed` | `2` | how fast an automatic mode eases the yaw round, per second. Always takes the short way |
| `movementThreshold` | `0.5` | ground speed, m/s, the character must beat before `'movement'` steers |
| `manualOverrideTimeout` | `1000` | ms a look command parks the automatic modes for |
| `lookAtTarget` | — | `THREE.Object3D \| THREE.Vector3`, for `'lookAt'` |
| `characterSystem` | — | whose velocity drives `'movement'`; set for you inside a `` |
Neither automatic mode fights the player: `CameraBoom.move()` / `rotate()` stamp `lastLookTime`,
so the existing look bindings need no changes. Inside a `` the rig reads the
character's own `linearVelocity` — the anchor it follows is a kinematic stand-in for a
`CharacterVirtual` and does not carry one. Standalone, it falls back to the followed body's
velocity.
### Whiskers
The boom already shape-casts backwards and pulls the camera in when a wall gets between it and the
player, which reads as a snap. With `whiskers` on it also fans short rays out either side of the
boom every step and rotates the yaw away from whatever they touch, so the camera slides around a
corner before the wall ever becomes a problem.
```tsx
```
| option | default | |
| --- | --- | --- |
| `whiskers` | `false` | |
| `whiskerCount` | `5` | fanned evenly across the spread |
| `whiskerSpread` | 60° | half-angle of the fan either side of the boom, radians |
| `whiskerLength` | `3` | metres |
| `whiskerStrength` | `2` | peak yaw rate a fully buried whisker asks for, rad/s |
| `whiskerDamping` | `0.2` | how fast the steering rate catches up; lower is smoother |
`boom.isWhiskerSteering` says whether anything is currently in the way (it is a field on
`CameraBoom`, not an option), and `boom.whiskerYawVelocity` is the current rate. The steering is
spring damped, so it eases in and decays back to zero once the whiskers come clear.
The whisker raycaster is built lazily the first time whiskers are switched on and is freed by
`destroy()` with the boom's other queries. The per-step sweep allocates on neither side of the
WASM boundary.
### `CameraRigManager`
`useCameraRig(options)` gives you the manager directly. It takes over r3f's default camera — and
restores it on unmount — and disables the r3f `controls` while it is mounted.
| member | |
| --- | --- |
| `setOptions(options)` | change options on a live rig; the rig is not rebuilt |
| `attach(body, offset?)` / `detach()` / `reAttach()` | follow a `BodyState` |
| `moveBoom(lookVector)` / `zoom(level)` | what the look command drives |
| `createCamera(name, options?)` / `addCamera(name, camera, space?)` / `setActiveCamera(name)` / `getCamera(name)` / `removeCamera(name)` | multiple cameras, positioned in rig space |
| `onCamera(fn)` | subscribe to camera changes; returns an unsubscribe |
| `controls` | the [`CameraBoom`](#camerarig): collision, obstruction and whiskers |
| `debug` | draw the rig's spaces |
| `destroy()` | **the public teardown** |
> [!IMPORTANT]
> `destroy()` stops the rig being stepped, frees the boom's raycaster, shapecaster and shape
> collider, and removes every body and three.js object the rig created. It is idempotent.
> `useCameraRig` and `` call it on unmount, so mounting and unmounting a rig no longer
> leaks. `attachToLoop()` / `detachFromLoop()` are private on the manager — `destroy()` is what
> you call. (`VehicleSystem.detachFromLoop()` is the one public exception.)
On the boom itself, `initialize(options)` configures it and **snaps** to the resulting pose,
`setOptions(options)` eases into it and only touches the options you passed, and `destroy()` is
likewise idempotent.
Bodies marked [`allowObstruction`](/api/rigid-body#props) let the boom pull the camera in (or
see through them) rather than getting stuck behind them.
## Vehicle
One component for every kind of vehicle: a Jolt vehicle constraint with engine, transmission,
differentials and suspension.
```tsx
import { Vehicle } from '@react-three/jolt/controllers';
;
```
| prop | type | default | |
| --- | --- | --- | --- |
| `type` | `'fourWheel' \| 'twoWheel'` | `'fourWheel'` | a car or a motorcycle |
| `name` | `string` | `'car'` / `'bike'` | the key it is registered under in its `VehicleSystem` |
| `position` | `THREE.Vector3 \| [number, number, number]` | — | reactive |
| `vehicleSettings` | `VehicleSettings` | per type | merged **over** the defaults — see below |
| `bodyObject` | `Object3DSource` | — | your own chassis object |
| `childrenAsChassis` | `boolean` | true when children are given and `bodyObject` is not | |
| `wheels` | `VehicleWheelOptions[]` | — | per-wheel settings, in constraint order |
| `wheelObjects` | `Object3DSource[]` | — | just the objects, in constraint order |
| `debug` | `boolean` | `true` | show the generated stand-in meshes |
| `followCamera` | `boolean` | `true` | translate the r3f camera by the chassis delta each step |
| `onVehicle` | `(vehicle: VehicleManager \| null) => void` | — | called when the manager appears and again with `null` when it goes |
`Object3DSource` is `THREE.Object3D | RefObject | null | undefined`, so a
ref is fine and is resolved after mount.
Driving is bound to the `move` command (stick/WASD) out of the box.
### Injecting your own meshes
```tsx
```
Children become the chassis by default. `bodyObject` does the same with an object or a ref, and
`wheels` / `wheelObjects` do it for the wheels — position *and* rotation, steering included. Wheel
order is `fl, fr, bl, br` for a four-wheeler and `front, back` for a two-wheeler
(`wheelOrderByType`). If both `wheels` and `wheelObjects` are given, `wheels` wins.
**Ownership:** the manager disposes only the meshes it generated itself. An object you injected is
detached on teardown and never disposed.
### `useVehicle`
```tsx
import { useVehicle } from '@react-three/jolt/controllers';
import { useEffect } from 'react';
function Car() {
const vehicle = useVehicle({ type: 'fourWheel', position: [0, 25, 0] });
useEffect(() => vehicle?.onPostStep(() => console.log(vehicle.position)), [vehicle]);
return null;
}
```
`useVehicle(options)` returns the `VehicleManager`, or **`null` on the first render** — it is
created after mount. Options are `name`, `type`, `settings`, `position`, `bodyObject`, `wheels`,
`wheelObjects`, `debug` and `addToScene` (default `true`). Settings are read when the vehicle is
built: changing `name` or `type` rebuilds it, and live changes go through the manager's own
setters. The vehicle, its `VehicleSystem` and everything they own are destroyed on unmount.
### `VehicleManager`
| member | |
| --- | --- |
| `move(direction)` | a `Vector3` or `Vector2` of driver input |
| `setHandBrake(v)` / `setBrake(v)` / `triggerTurbo(extraTime?)` | |
| `setPosition(position)` | |
| `setBodyObject(object \| null)` | `null` puts the generated box back |
| `setWheelObject(index \| name, object \| null)` / `setWheelObjects(objects)` | |
| `getWheel(index \| name)` / `getWheelObject(index \| name)` | |
| `onPreStep(fn)` / `onPostCollide(fn)` / `onPostStep(fn)` | each returns an unsubscribe |
| `onAction(type, fn)` | ditto |
| `position`, `bodyObject`, `wheels`, `wheelOrder`, `settings`, `debug` | |
| `destroy()` | idempotent |
`settings` is a fully resolved, typed `ResolvedVehicleSettings` object rather than `any`.
`FourWheelVehicleManager` and `TwoWheelVehicleManager` are the two concrete classes;
`VehicleSystem` holds them by name (`addVehicle`, `getVehicle`, `removeVehicle`, `setPosition`,
`detachFromLoop`, `destroy`).
### Settings
`vehicleSettings` is a discriminated union on `type`. Shared by both:
`bodyPosition`, `castType` (`'ray' | 'sphere' | 'cylinder'`), `vehicleLength`, `vehicleWidth`,
`vehicleHeight`, `vehicleMass`, `maxPitchRollAngle`, `maxEngineTorque`, `clutchStrength`,
`bodyObject`, `wheelObjects`.
**`fourWheel`** adds `fourWheelDrive`, `frontBackLimitedSlipRatio`, `leftRightLimitedSlipRatio`,
`antiRollbar`, `splitEngineTorqueFront`, `splitEngineTorqueRear`, `frontRollBarStiffness`,
`rearRollBarStiffness` and a `wheels` object with `fl` / `fr` / `bl` / `br` overrides. Defaults:
4.0 × 1.8 × 0.4 m, 1500 kg, cylinder cast, four-wheel drive, 500 N·m of engine torque, 30° of
steering, 0.5 m wheels.
**`twoWheel`** adds `steerSpeed` (radians per second the steering angle may change by),
`casterAngle` and a `wheels` object with `front` / `back` overrides. Defaults: 0.8 × 0.4 × 0.6 m,
250 kg, 30° caster, 0.31 m wheels.
Per-wheel settings cover `radius`, `width`, `position` (the attachment point; when given it
overrides the `wheelOffsetHorizontal` / `wheelOffsetVertical` layout helpers),
`suspensionMinLength` / `suspensionMaxLength` / `suspensionPreloadLength`, `suspensionDirection`,
`suspensionForcePoint`, `suspensionSpring` (`{ frequency, damping }`), `steeringAxis`, `wheelUp`,
`wheelForward`, `inertia`, `angularDamping`, plus `maxSteerAngle` / `maxBrakeTorque` /
`maxHandBrakeTorque` (four-wheel) or `posZ` / `suspensionFreq` / `brakeTorque` (two-wheel).
`defaultFourWheelVehicleSettings`, `defaultTwoWheelVehicleSettings` and
`resolveVehicleSettings(settings)` are exported if you want to inspect or pre-merge them. Your
settings are merged **over** the defaults — a motorcycle built with a custom mass or custom wheels
no longer silently gets the defaults back.
### Secondary physics
Three presentational layers sit on top of the constraint (issue #41) — none of them touch the
simulation, they only read what Jolt solved and drive the three.js objects (and the readouts a
game needs for particles, skid marks and engine audio) from that. All three are **on by default**
and independently configurable through `vehicleSettings`, and each can be turned off with `false`:
```tsx
;
```
**`bodyRoll`** — a spring-damped visual tilt of the chassis *object* (never the physics body),
driven by the chassis' own acceleration rotated into its local frame: a steady turn leans the body
outwards, exactly like a real sprung mass. Because the manager owns that object's local rotation
while this is on, rotate your model inside a wrapper if you need to orient it yourself.
| option | default | |
| --- | --- | --- |
| `maxAngle` | `0.1` rad (~5.7°) | the most the body may lean sideways |
| `maxPitchAngle` | `maxAngle / 2` | the most it may pitch under acceleration/braking |
| `referenceAcceleration` | `9.81` m/s² | the acceleration that produces the full `maxAngle` — lower for floatier, raise for stiffer |
| `stiffness` | `120` | spring constant pulling the tilt towards its target |
| `damping` | `20` | damping of that spring; `2 * sqrt(stiffness)` is critical |
**`wheelSmoothing`** — easing applied to what the wheels *render*, so a wheel doesn't snap between
two suspension lengths or steering angles the solver happens to land on between frames. Wheel spin
is Jolt's own, integrated from its angular velocity, and is never eased.
| option | default | |
| --- | --- | --- |
| `suspension` | `0.04` s | time constant for rendered suspension travel — ~63% of the way to Jolt's value every this many seconds. `0` renders it raw |
| `steering` | `0.05` s | the same, for the rendered steering angle |
**`skid`** — when a wheel counts as skidding, compared against Jolt's own per-wheel slip. The
thresholds are deliberately generous: a longitudinal slip ratio of 3 (the wheel spinning at four
times the speed of the ground under it) is an ordinary standing start in a 500 N·m car.
| option | default | |
| --- | --- | --- |
| `longitudinalSlip` | `1.5` | the \|slipRatio\| a wheel has to exceed to be skidding |
| `lateralSlip` | `0.25` rad (~14°) | the \|lateralSlip\| a wheel has to exceed |
| `minLateralSpeed` | `0.5` m/s | below this speed, lateral slip never counts — Jolt's `atan2(lateral, |longitudinal|)` reports a right angle for a parked, noise-only car |
| `release` | `0.7` | the fraction of those thresholds a skidding wheel has to fall back under |
| `releaseTime` | `0.12` s | how long it has to stay under `release` before `skidEnd` fires |
| `requireContact` | `true` | wheels not touching anything never skid |
Change any of the three live, on the manager: `vehicle.setBodyRoll(settings | false)`,
`vehicle.setWheelSmoothing(settings | false)`, `vehicle.setSkid(settings | false)`. Turning
`bodyRoll` off puts the chassis object back to level; turning `skid` off ends every skid currently
running, so a listener that started a particle effect still gets its `skidEnd`.
### Readouts and events
```tsx
import { useVehicle } from '@react-three/jolt/controllers';
import { useEffect } from 'react';
function Dashboard() {
const vehicle = useVehicle({ type: 'fourWheel' });
useEffect(() => {
if (!vehicle) return;
const offSkid = vehicle.onSkidStart((e) => spawnSkidMark(e.position, e.name));
const offEngine = vehicle.onEngine(({ rpm, gear, throttle }) => {
engineSound.playbackRate = 0.5 + rpm / 6000;
engineSound.volume = 0.2 + 0.8 * throttle;
});
return () => {
offSkid();
offEngine();
};
}, [vehicle]);
return null;
}
```
| member | | |
| --- | --- | --- |
| `rpm` | `number` | the engine's current RPM, `0` with no engine (a destroyed vehicle) |
| `gear` | `number` | `0` in neutral, `1..n` forward, negative in reverse |
| `shifting` | `boolean` | true while the transmission is between gears |
| `clutch` | `number` | clutch friction, 0..1 |
| `throttle` / `brakeInput` | `number` | driver input, 0..1 |
| `speed` / `speedKmh` | `number` | chassis' signed forward speed, m/s / km/h |
| `skidding` | `boolean` | true while any wheel is over the skid thresholds |
| `onSkidStart(fn)` / `onSkidEnd(fn)` | `(event: VehicleSkidEvent) => void` | a wheel crossed (or released) the skid thresholds |
| `onEngine(fn)` | `(state: VehicleEngineState) => void` | the engine readout, once per physics step |
Every readout getter reads a Jolt static temporary or a number — none of it allocates. Both event
payloads are **pooled** (the same object every dispatch): `VehicleSkidEvent` carries `wheel`,
`name` (`'fl' | 'fr' | 'bl' | 'br'` or `'front' | 'back'`), `index`, `slipRatio`, `lateralSlip`,
`position` (world space, also pooled — copy it if you keep it) and `speedKmh`. `VehicleEngineState`
carries `rpm`, `gear`, `throttle`, `brake`, `speed`, `speedKmh`, `shifting`, `clutch` and
`skidding`. `onEngine`'s handler is skipped entirely (no state object is even filled) unless
something is subscribed, so an unused dashboard costs nothing per step.
### `VehicleFourWheel`
> [!WARNING]
> **Deprecated.** `` is exactly `` and now forwards
> to it; the `type` string it used to take is replaced by ``. It keeps
> working for one release and warns once under [`setDebug(true)`](/advanced/memory#debug-output).
> `VehicleFourWheelProps` is likewise a deprecated alias of `VehicleProps`.
The managers were renamed to `FourWheelVehicleManager` / `TwoWheelVehicleManager`; the old
`VehicleFourWheelManager` and `VehicleManagerTwoWheels` names are exported as deprecated aliases.
> [!NOTE]
> `` translates the r3f camera by the chassis delta on every pre-step (and
> retargets the default `controls` if there are any). Pass `followCamera={false}` if you are
> managing the camera yourself — with a [``](#camerarig), for instance.
]]>
{
if (info.isInitial) jump();
},
() => stopJumping(),
{ keys: [' '], rate: 0.1 }
);
return null;
}
```
`useCommand(commandString, onStart?, onEnd?, options?)` registers the command (if it doesn't
already exist), subscribes both callbacks, and unsubscribes on unmount. Registration happens in
an effect, so Strict Mode's double render can't double-register, and the callbacks are read
through refs — inline arrow functions do not re-subscribe on every render.
> [!IMPORTANT]
> Because registration is an effect, the hook **returns `undefined` on the first render** and the
> `Command` from the first effect onwards. Use the returned command inside an effect, or guard it:
>
> ```tsx
> const command = useCommand('jump');
> useEffect(() => command?.setOptions({ rate: 0.2 }), [command]);
> ```
Commands with a name in `commonCommands` pick up their bindings automatically:
`moveForward`, `moveBackward`, `moveLeft`, `moveRight`, `jump`, `crouch`, `run`, `fire`,
`spaceFire`.
### `CommandInfo`
Every callback receives a `CommandInfo` — a real exported type, so `info.isInitial` no longer
needs a `@ts-ignore`:
```ts
type CommandInfo = {
command: Command;
label: string;
method: string;
value: CommandValue;
isInitial: boolean;
startTime: number;
event?: CommandEvent;
duration?: number;
};
```
| field | |
| --- | --- |
| `command` | the `Command` itself |
| `label` | its name |
| `method` | what triggered it — the event's `type`, or `'update'` |
| `value` | `string`, `number`, `boolean` or `{ x, y }` for vector commands (`CommandValue`) |
| `isInitial` | `true` on the first fire of a press — what you check to avoid key repeat |
| `startTime` | ms |
| `duration` | ms; set on the initial down and on every up |
| `event` | the original `KeyboardEvent`, `MouseEvent` or `GamepadInputEvent` (`CommandEvent`) |
### `CommandOptions`
| option | |
| --- | --- |
| `keys` | `KeyboardEvent.key` values, plus `'Mouse0'`-style mouse buttons |
| `buttons` | gamepad button indices |
| `axis` | gamepad axis indices |
| `rate` | minimum seconds between fires — key-repeat throttling |
| `isVariable`, `min`, `max`, `threshold`, `deadzone` | analogue handling |
| `active` | enable/disable without unbinding |
| `asVector` | make it a `VectorCommand` — `value` becomes `{ x, y }` |
| `preset` | a named vector preset: `'move'`, `'look'`, `'race'` |
| `bindings` | overrides merged onto the preset |
| `inverted` | `{ x?, y? }`, flip an axis |
```tsx
useCommand(
'move',
(info) => {
const { x, y } = info.value as { x: number; y: number };
move(x, y);
},
() => stop(),
{ asVector: true, preset: 'move' }
);
```
## `useCommandState`
The flattened state of every live command, as a `useSyncExternalStore` snapshot — for HUDs,
debug overlays and anything that should re-render on input:
```tsx
const state = useCommandState();
// state.jump -> boolean, state.move -> { x, y }
```
For per-frame reading prefer `useCommand`'s callbacks; this one re-renders.
## `useLookCommand`
Look and zoom from **mouse, touch and gamepad** — all three on by default, each registered in its
own effect with a full cleanup.
```tsx
import { useLookCommand } from '@react-three/jolt/addons';
useLookCommand(
(look) => rig.moveBoom(look), // THREE.Vector2 of movement deltas
(zoom) => rig.zoom(zoom),
{
mouse: true,
touch: true,
gamepad: { stick: 'right', deadzone: 0.15 },
sensitivity: { mouse: 1, touch: 1, gamepad: 200 },
invertY: false,
lockPointer: true
}
);
```
Both handlers are positional and required. The zoom handler is a `wheel` listener on the target
element and is independent of the `mouse` option.
| option | default | |
| --- | --- | --- |
| `mouse` | `true` | mouse drag / pointer lock |
| `touch` | `true` | one-finger touch drag |
| `gamepad` | `true` | `false` to disable, or `{ stick: 'left' \| 'right', deadzone?: number }` |
| `sensitivity` | `{ mouse: 1, touch: 1, gamepad: 200 }` | per source |
| `invertY` | `false` | shorthand for `invert.y`; it wins if both are given |
| `invert` | — | `{ x?, y? }` |
| `domElement` | `document.body` | where the listeners go |
| `lockPointer` | `false` | request pointer lock on mouse down |
| `useAccelerated` | `false` | use the OS pointer acceleration curve while locked |
- **Mouse** deltas fire while the pointer is locked **or** the mouse is held down.
- **Touch** uses pointer events filtered on `pointerType === 'touch'`; a second finger (a pinch)
abandons the drag rather than jumping. `touch-action: none` is set on the target element while
mounted and restored on cleanup.
- **Gamepad** samples the stick once per animation frame and scales by the frame delta (so
`sensitivity.gamepad` is "units at full deflection per second"), clamped so a backgrounded tab
can't hand it one enormous delta. The stick defaults to the right one; environments with no
Gamepad API skip it silently.
The look vector handed to your handler is a single reused `THREE.Vector2`, and a zero delta is not
dispatched at all. Both handlers are read through refs, and every listener is removed on unmount.
## `useCommander` and `CommanderProvider`
The commander attaches four `window` listeners and a gamepad polling loop. It is **reference
counted**: the first hook to mount connects it, the last to unmount disconnects it, so a screen
with no command hooks costs nothing.
By default every hook shares one lazily created commander. `` scopes one to a
subtree instead — a `
`, ``, `` and the hooks, you can skip this page —
the library owns that memory and frees it when components unmount. Read on before you touch a
`Jolt.*` object directly, which mostly means `useJolt().jolt`, `body.body`, a caster you created
yourself, or anything reached through `Raw.module`.
## Reaching the module
There is exactly **one jolt-physics module per page** — it owns a single WASM linear memory that
every body, shape and constraint lives in — so the library keeps it in a module level singleton:
```ts
import { free, getJoltModule, JoltModule, initJolt } from '@react-three/jolt';
await initJolt(); // does this for you
const jolt = getJoltModule(); // throws a useful error if it isn't loaded yet
const v = new jolt.Vec3(0, 1, 0);
free(v); // the documented way to destroy something you own
```
`JoltModule.module` is the module itself. `JoltModule` used to be called `Raw`, and `Raw` is
still exported as an alias of the same object, so existing `Raw.module.*` code keeps working.
Each `` builds its own `JoltInterface` on that module and takes an id for it from
`JoltModule.registerInterface(joltInterface)` — a monotonically increasing counter, never reused,
so a stale id can never resolve to somebody else's world. `JoltModule.getInterface(id)` resolves
it back (`undefined` once that world is destroyed), `JoltModule.releaseInterface(id)` drops the
slot in `PhysicsSystem.destroy()` (it frees nothing itself — the caller destroys the interface),
and `JoltModule.interfaceCount` / `JoltModule.liveInterfaceIds()` tell you how many worlds, and
which, are live right now. This replaced a map keyed by React's `useId()` (issue #35): that key
changes across remounts, so an unmount/remount grew the map instead of reusing a slot, and once it
held enough entries a new `` was silently handed an *old* world's interface.
There is no fixed cap on how many worlds you can mount, but each one costs about **20 MB** of the
fixed **128 MB** WASM heap the jolt-physics WASM builds ship with (no growth), so roughly six fit
at once. Past that, `new PhysicsSystem()` throws — rather than letting emscripten abort the module
for the rest of the page — with a message naming how much heap is free, how much is needed, and
how many worlds are already live: `r3/jolt: not enough WASM heap for another physics world - ...`.
See [``: Destroying a world](/api/physics#destroying-a-world) for the deferred-unmount
mechanics that can make that check pass a tick later than you'd expect, and `registerDisposable`
for tying non-body objects to a world's teardown.
## The three rules
1. **If you created it, you free it.** `new Raw.module.Vec3(...)` is a heap allocation; only
`free(v)` / `Raw.module.destroy(v)` releases it. Losing the reference leaks it forever.
2. **If Jolt gave it to you, don't free it.** Anything returned from a Jolt call — a property, a
collector's hit, a "by value" return — belongs to Jolt. Copy the numbers out and let go.
3. **Free it exactly once.** `Raw.module.destroy()` on an already-destroyed object does **not**
throw. It double-frees, and the allocator hands that address straight back out to the next
allocation.
## By-value returns are shared temporaries
This is the rule that catches everyone, and it has no equivalent in JavaScript.
Any C++ function returning **by value** — `Vec3::Normalized()`, `Quat::sIdentity()`,
`RMat44::sRotationTranslation()`, `Shape::GetCenterOfMass()`, `ShapeSettings::Create()`,
`Body::GetWorldSpaceSurfaceNormal()`, `RRayCast::GetPointOnRay()`, `AABox::sBiggest()`, … —
comes back through the binder as a pointer to **one static temporary per function**, shared by
every caller.
That means:
```ts
import { BodyState, Raw, vec3 } from '@react-three/jolt';
function centerOfMass(body: BodyState) {
const joltPosition = body.body.GetCenterOfMassPosition(); // by value: shared temporary
Raw.module.destroy(joltPosition); // ❌ frees memory the binder owns. Corruption, elsewhere.
const kept = joltPosition; // ❌ the next call overwrites it
return vec3.three(joltPosition).clone(); // ✅ copy the components out immediately
}
```
`ShapeSettings::Create()` is the one that bites hardest, because its `ShapeResult` is also a
static temporary — [`createShapeFromSettings`](/api/shapes#building-shapes-yourself) exists so
you never have to get that sequence (check error → `Get()` → `AddRef()` → `Clear()` → destroy
settings) right by hand.
## Arguments are copied
Property assignment and by-value arguments **copy**:
```ts
settings.mPosition = v; // copies v
bodyInterface.SetPosition(id, v, activation); // copies v
list.push_back(v); // copies v
```
So the temporary you built is yours to destroy immediately afterwards — and a single scratch
object can be re-`Set()` for every element of a loop instead of allocating one per element.
## The helpers
### Conversions
`vec3.jolt()`, `vec3.rjolt()` and `quat.jolt()` **always return a new object the caller owns**,
even when given another Jolt object (they clone it). They never hand back their argument, so
destroying the result can never free something Jolt still holds.
```ts
const position = vec3.rjolt([0, 10, 0]); // I own this
bodyInterface.SetPosition(body.BodyID, position, Raw.module.EActivation_Activate);
Raw.module.destroy(position); // ...so I free it
```
`vec3.three()` / `quat.three()` never allocate WASM memory and never destroy their input. A
`THREE.Vector3` argument is passed through unchanged — pass an `out` target or `.clone()` if you
intend to keep it.
> **World-space positions are `RVec3`.** In jolt-physics ≥ 1.0 every world-space position
> argument takes `Jolt.RVec3`, not `Jolt.Vec3` — that's `vec3.rjolt()`. They are interchangeable
> at runtime in the single-precision builds, but not in the types.
### `withJolt` — scoped
Construct, call, destroy, even if the callback throws. The shape most code wants:
```ts
import { Raw, withJolt, withRJolt } from '@react-three/jolt';
const length = withJolt([0, 1, 0], (v) => v.Length());
withRJolt([0, 10, 0], (position) =>
bodyInterface.SetPosition(body.BodyID, position, Raw.module.EActivation_Activate)
);
```
`withQuat()` does the same for quaternions.
### `joltScratch` — shared, allocation-free
For per-frame code, one shared object per flavour, reused forever:
```ts
import { joltScratch } from '@react-three/jolt';
// SetLinearVelocity copies its argument, so one shared vector is enough
bodyInterface.SetLinearVelocity(body.BodyID, joltScratch.vec3(0, 5, 0));
```
> [!CAUTION]
> Scratch rules: **never** `destroy()` the result, **never** store it (it is valid only until the
> next call to the same accessor), and **never** pass it to an API that keeps a pointer to its
> argument rather than copying it. When in doubt, use `vec3.jolt()` or `withJolt()`.
>
> `joltScratch.release()` exists for tearing the module down in tests and HMR; the next accessor
> call rebuilds the objects.
## Bodies, shapes and constraints
These have their own lifetimes and their own rules:
- **Bodies** are never `destroy()`ed. A body is removed *and then* destroyed through the body
interface (`RemoveBody(id)` then `DestroyBody(id)`), in that order — destroying an added body
corrupts the world with no error. `bodySystem.removeBody(handle)` does all of it, and
`` calls that on unmount.
- **Shapes are reference counted.** `createShapeFromSettings()` takes a reference,
`releaseShape()` drops it, and the shape is freed when the last holder lets go. A body owns
its shape and releases it when removed.
- **Constraints are reference counted too**, and must be removed *before* either of their
bodies. `Raw.module.destroy()` on a constraint is a double free that traps in WASM — use
`constraintSystem.removeConstraint()` (which `useConstraint` does on unmount). Removing a
constrained body removes its constraints first.
- **Query objects** (`Raycaster`, `Shapecaster`, `ShapeCollider`, `Multicaster`) own filters, a
collector and settings. Call `destroy()` — see [Queries: lifetime](/api/queries#lifetime).
- **Collector hits** (`mHits[i]`, `mBodyID`, `mSubShapeID2`, …) are references into the
collector's own storage. Read them; never destroy them.
## Component lifecycle
React tears a parent down before its children, so `` frees the Jolt interface *before*
the bodies inside it unmount. Everything that world owned — bodies, constraints, shapes — goes
with the interface, and the systems check `physicsSystem.destroyed` before touching Jolt
afterwards. This is why unmounting a world is safe even though the children clean up "too late".
Practical consequences:
- Changing `` is a clean, complete reset.
- A caster you created from `physicsSystem` outlives nothing: destroy it in your own cleanup,
and don't cast after the world is gone.
- Keeping a `BodyState` (or a handle) past its ``'s lifetime is a use-after-free
waiting to happen. Handles are recycled by Jolt, so a stale one may silently name a *different*
body.
> [!CAUTION]
> **Never touch a `BodyState` after its world is destroyed (issue #227).** `BodyState.dispose()`
> (called from `removeBody`) sets a `disposed` flag and clears its own bookkeeping — contacts,
> listeners, surface materials — but as of this writing it does **not** yet gate every setter, so
> `state.position = ...` on a body whose `` has already unmounted reaches
> `Jolt.Body.SetPosition` on a freed pointer. Because the module is a page-wide singleton with
> immediate pointer reuse, the crash is a wasm trap (`memory access out of bounds` or
> `null function`) that surfaces in whatever *other* world happens to be live at the time, not in
> the code that caused it.
>
> The hazard is not the direct case — nobody writes `state.position = ...` right after
> `physicsSystem.destroy()` — it's anything **asynchronous** that outlives the body: an
> uncancelled `setTimeout`, a promise callback, an animation. The `Launcher` in the
> `OneWayPlatform` demo (`apps/examples/src/examples/OneWayPlatform.tsx`) hit exactly this: a ball
> scheduled its own relaunch with `setTimeout(launch, 700)`, and navigating away before the timer
> fired left it writing `state.position`/`state.velocity` on a body whose world was already gone.
>
> The same applies to character controllers, camera rigs and vehicle managers — none of them throw
> on stale access yet either. `QueryBase.destroyed` (see [Queries: shared
> base](/api/queries#shared-base)) and `PhysicsSystem.destroyed` are the two places this check
> already exists at the library level; treat everything else as your own responsibility until
> #227 closes.
The demo's fix is the pattern to copy until #227 lands a library-level guard: keep the timer
handle so you can `clearTimeout` it on unmount, **and** re-check `physicsSystem.destroyed` inside
the callback itself (a timer can already be in flight the instant its cleanup runs):
```tsx
import type { BodyState } from '@react-three/jolt';
import { useJolt } from '@react-three/jolt';
import { useCallback, useEffect, useRef } from 'react';
import * as THREE from 'three';
function Launcher({ x, body }: { x: number; body: React.MutableRefObject }) {
const { physicsSystem } = useJolt();
const relaunch = useRef | undefined>(undefined);
const launch = useCallback(() => {
relaunch.current = undefined;
const state = body.current;
// the world can go away between the timer being set and it firing
if (!state || physicsSystem.destroyed) return;
state.position = new THREE.Vector3(x, 1, 0);
}, [x, physicsSystem, body]);
useEffect(() => () => clearTimeout(relaunch.current), []);
return null;
}
```
## Debug output
The library's internal `console` output is off by default — a bundled library can't rely on
`NODE_ENV`. Turn it on while you are debugging:
```ts
import { setDebug } from '@react-three/jolt';
setDebug(true);
```
That enables the internal warnings (`devWarn`) for things like dropped simulation time, excess
Jolt interfaces and failed heightmap loads. It is separate from
[``](/api/physics#debug), which draws collision shapes in the scene.
For Jolt's own assertions and its debug renderer, use a debug build of the engine —
[`jolt-physics/debug-wasm-compat`](/getting-started/installation#choosing-a-jolt-build).
## Checking your own code
The repository tests allocation balance by wrapping the module's constructors and asserting the
live-object count is flat across many iterations (`test/jolt-alloc.ts`,
`installAllocTracker(Raw)`). If you write a system that touches Jolt directly, the same trick is
the only reliable way to catch a leak: a leak that grows by one object per frame is invisible
until it isn't, and a double free shows up as the count drifting *negative*.
]]>
` loads a multi-megabyte WebAssembly module before it can do anything. It does that by
**suspending**: on first render it throws a promise, React shows the nearest ``
fallback, and it renders again once Jolt is ready. That single fact drives everything on this
page.
## The `` boundary
```tsx
import { Canvas } from '@react-three/fiber';
import { Physics, RigidBody } from '@react-three/jolt';
import { Suspense } from 'react';
export function Scene() {
return (
);
}
```
Things worth knowing:
- **The boundary has to be above ``, not inside it.** A `` among the children
never sees the throw.
- **Put anything that should stay visible outside it.** Lights, environment, HUD and the camera
belong outside the boundary so they render while physics loads. The example above deliberately
keeps only the physical scene inside.
- **A `null` fallback is fine inside a `
[!NOTE]
> The library is still alpha and was never widely published, so this is a "how the code differs"
> guide rather than a formal breaking-change list. If something you used isn't mentioned, it
> probably didn't change.
## 1. Peer dependencies
All three packages now require:
| package | before | now |
| --- | --- | --- |
| `react` / `react-dom` | 18.2 | `>=19.0.0` (r3f 10 is tested against `19.2.x`) |
| `@react-three/fiber` | 8.16 | `>=10.0.0-0` |
| `three` | 0.163, undeclared | `>=0.185`, now a declared peer |
| `jolt-physics` | `^0.22.0`, a dependency | `>=1.1.0`, now a **peer** |
| node | — | `>=22` |
One of those needs action even if your code doesn't change:
- **`jolt-physics` is a peer now.** It is no longer installed for you — add it to your app. In
exchange you get to pick the [build variant](/getting-started/installation#choosing-a-jolt-build),
and you can no longer end up with two copies of the WASM module in one bundle.
React 19 and r3f 10 have their own migration notes; if you are coming from r3f 8 you are making
two major jumps at once.
## 2. jolt-physics 0.22 → 1.1.0
Two years of engine changes (Jolt C++ v5.6). What surfaces in this library's API:
- **World-space positions are `Jolt.RVec3`.** `BodyState.getPosition(true)` returns an `RVec3`
rather than a `Vec3`. In the single-precision builds the two are interchangeable at runtime,
but not in the types.
- **`vec3.rjolt()` is new** — the `RVec3` counterpart of `vec3.jolt()`. Use it wherever you feed
a position back into Jolt.
- **`generateJoltMatrix()` returns an `RMat44`**, which is what `CollideShape` and `RShapeCast`
take. It also returns a real copy now instead of the binder's shared static temporary — the
old value was silently rewritten by the next caller, and destroying it freed memory Jolt owned.
- **`Shapecaster.shapecast` is a `Jolt.RShapeCast`.**
If you call Jolt directly, note two upstream changes that bit this library:
`BodyInterface.AddForce`/`AddTorque` gained an `EActivation` parameter (0.24), and body user data
went from 64-bit to 32-bit (0.39).
## 3. Breaking changes
Everything that can break a compiling app, by package.
### `@react-three/jolt`
| what | before | now |
| --- | --- | --- |
| **Contact handler signature** | `(handle1, handle2, manifold, settings, count, context)` | one [payload object](/api/rigid-body#payloads). The old arguments handed out Jolt pointers that were freed before the handler could run |
| **Per-body listener arrays** | `BodyState.contactAddedListeners` / `contactRemovedListeners` / `contactPersistedListeners` / `activationListeners` | `BodyState.events`, an [`Emitter`](/api/rigid-body#events) |
| **The 900 ms contact debounce** | `BodyState.contactThreshold`, `contactTimestamps` | **gone.** Enter/persist/exit come from Jolt's sub-shape pairs, which is what the debounce was compensating for |
| **Collision sub-groups** | hard-coded 0/1/2 semantics | a `GroupFilterTable`. Every sub-group pair collides until you call [`disableCollision(a, b)`](/api/collision-groups#the-filter-table) |
| **`BodyEvents` / `WorldEvents` types** | in `types.ts` | deleted. They were unreachable and described signatures that were never dispatched. Use `BodyEventMap` / `WorldEventMap` |
| **`BodyState.getPosition(true)`** | `Jolt.Vec3` | `Jolt.RVec3` |
| **`vec3.jolt()` / `vec3.rjolt()` / `quat.jolt()`** | returned their argument when it already was a Jolt object | always return a **new object the caller owns** |
| **`Raw.joltInterfaces` / `PhysicsSystem.maxInterfaces`** | a fixed three-world cache keyed by React's `useId()` | **gone.** [`JoltModule.registerInterface`/`getInterface`/`releaseInterface`/`interfaceCount`](/advanced/memory#reaching-the-module) replace it, with no fixed cap — see [Destroying a world](/api/physics#destroying-a-world) |
| **`PhysicsSystem`'s constructor argument** | the interface cache key | a debug label only. `destroy(pid)`'s argument is likewise ignored. Both still compile |
| **`jolt-physics`** | a dependency | a peer — add it to your app |
| **Console output** | ~17 unconditional `console.log`s | off unless you call `setDebug(true)` |
``'s `onContactAdded` / `onContactRemoved` / `onContactPersisted` props still work as
deprecated aliases of `onCollisionEnter` / `onCollisionExit` / `onCollisionPersist`, and now
receive the new payload — their declared type never matched what was actually dispatched, so
nothing that worked before breaks.
### `@react-three/jolt/addons`
| what | before | now |
| --- | --- | --- |
| **`useCommand` return value** | the `Command`, from the first render | **`undefined` on the first render**, the command from the first effect onwards. The command is no longer created during render. Use it in an effect, or guard it |
| **`gamepad.js`** | a dependency | removed; gamepads are polled in-house. `GamepadInputEvent` and its payload are unchanged |
| **`Commander.getSnapsot`** | the only spelling | deprecated in favour of `getSnapshot` |
### `@react-three/jolt/controllers`
| what | before | now |
| --- | --- | --- |
| **``** | the vehicle component | deprecated; forwards to [``](/api/controllers#vehicle). Its `type` string is replaced by `` |
| **`VehicleFourWheelManager`** | the class | renamed `FourWheelVehicleManager`; old name kept as a deprecated alias |
| **`VehicleManagerTwoWheels`** | the class | renamed `TwoWheelVehicleManager`; old name kept as a deprecated alias |
| **Two-wheel default settings** | merged **over** your settings | merged **under** them — a motorcycle built with a custom mass or custom wheels no longer silently gets the defaults back |
| **`VehicleManager.settings`** | `any` | a fully resolved, typed `ResolvedVehicleSettings` |
| **`camera-controls`** | a dependency | removed with the dead camera rig that used it |
Manager source files were kebab-cased (`vehicle-manager.ts`, `four-wheel-vehicle-manager.ts`,
`two-wheel-vehicle-manager.ts`, `wheel-state.ts`). If you were deep-importing them, import from
the package root instead.
## 4. Deprecated but still working
Nothing in this list has been removed. Each one has a replacement that returns an unsubscribe,
because removal by function identity can never match the inline arrows callers actually pass.
| deprecated | use |
| --- | --- |
| `BodyState.addContactListener(fn, type)` / `removeContactListener(fn)` | `body.on(type, fn)` |
| `BodyState.addActivationListener(fn)` / `removeActivationListener(fn)` | `body.onSleep(fn)` / `body.onWake(fn)`, which tell the two apart |
| `PhysicsSystem.addPreStepListener(fn)` / `addPostStepListener(fn)` / `removeStepListener(fn)` | `onBeforeStep(fn)` / `onAfterStep(fn)`, or the [hooks](/api/physics#step-hooks) |
| `` | `onCollisionEnter` / `onCollisionExit` / `onCollisionPersist` |
| `CharacterControllerSystem.removeActionListener(fn)` | keep the handle `addActionListener` / `on` returns |
| `Commander.getSnapsot()` | `getSnapshot()` |
| `createMeshForShape` | `createMeshFromShape` (they were byte-identical; both names still work) |
| `VehicleFourWheel`, `VehicleFourWheelProps`, `VehicleFourWheelManager`, `VehicleManagerTwoWheels`, `VehicleFourWheelSettings`, `VehicleTwoWheelSettings` | the [`Vehicle`](/api/controllers#vehicle) equivalents |
`addPreStepListener` / `addPostStepListener` now return an unsubscribe where they used to return
`void`, so that part is source compatible; `removeStepListener(fn)` and `removeContactListener(fn)`
now remove *every* subscription made for that function, where the old `else if` chain removed it
from only the first channel.
## 5. Removed
| gone | instead |
| --- | --- |
| `heightField/heightfieldManager.ts` and its worker scaffold | — (never referenced) |
| `heightField/generators-save.ts`, `utils/heightmap.ts`, `utils/psrddnoise3.ts` | — |
| the `camera-controls`-based camera rig (`camera-rig-system-camera-controls.ts`) | the default [`CameraRigManager`](/api/controllers#camerarigmanager) |
| `use-character-controller.ts` (empty), `tmp.ts` | — |
| the `camera-controls` runtime dependency of `@react-three/jolt/controllers` | — |
| `packages/react-three-jolt/types/gamepad.js.d.ts` | — (the dependency is gone) |
| `apps/examples/src/jolt/` (vendored Jolt builds, ~1 GB) | the `jolt-physics` package |
## 6. New
### `@react-three/jolt`
- **[Events](/api/rigid-body#events).** `onCollisionEnter` / `onCollisionPersist` /
`onCollisionExit` / `onSensorEnter` / `onSensorExit` / `onIntersectionEnter` /
`onIntersectionExit` / `onSleep` / `onWake` / `onContactValidate` on `` and
``; the same names plus `onSettled` and `onActivityChange` on
[``](/api/physics#world-events); `bodyState.on(type, fn)`, `physicsSystem.events`, and
the `useBodyEvent` / `useWorldEvent` hooks. New exported types: `CollisionTarget`,
`CollisionPayload`, `CollisionEnterPayload`, `CollisionExitPayload`, `SensorPayload`,
`ActivationPayload`, `ValidatePayload`, `BodyEventMap`, `WorldEventMap`, `Unsubscribe`,
`Emitter`.
- **[Step hooks](/api/physics#step-hooks).** `useBeforePhysicsStep` / `useAfterPhysicsStep`,
`physicsSystem.onBeforeStep` / `onAfterStep`. The step order is now defined:
`beforeStep → pending actions → Step() → queued events → afterStep`, **per substep**.
- **[Activity accounting](/api/physics#steady-state).** `bodySystem.activeBodyCount`,
`simulatedBodyCount`, `isSettled`.
- **[The shape pipeline](/api/shapes#the-shape-pipeline).** `describeShape` / `generateShape` /
`createShapeSettings` / `descriptorKey` / `stableKey` / `scaleShape` / `validScaleFor`, the
`ShapeDescriptor` union, and explicit descriptors for tapered capsules, cylinders and **tapered
cylinders**.
- **[Mutable compounds](/api/shapes#mutable-compounds).** ``, `addSubShape` /
`removeSubShape` / `modifySubShape` (free functions and `BodyState` methods),
`BodyState.notifyShapeChanged`.
- **[Object scaling](/api/shapes#scaling).** A scaled mesh below a described object produces a
`scaled` descriptor; `describeShape`'s `applyObjectScale`; `offsetCenterOfMass`.
- **[Per-body collision groups](/api/collision-groups).** `bodySystem.setGroupCollision` /
`disableCollision` / `enableCollision` / `isCollisionEnabled` / `subGroupCount`,
`BodyState.collisionGroup` / `collisionSubGroup`, `BodySystem.destroy()`.
- **[`useMouseRaycaster`](/api/queries#usemouseraycaster)**, and normal-oriented, pooled debug
markers on `Raycaster`.
- **[`` props](/api/physics#props).** `timeStep`, `maxSubSteps`, `defaultShape`, a working
`interpolate`, and a bounded accumulator. Plus `PhysicsSystem.accumulator`,
`resetAccumulator()`, `invalidatePoseCache()`, `destroyed`.
- **Memory helpers.** `withJolt()` / `withRJolt()` / `withQuat()`, `joltScratch`,
`createShapeFromSettings()` / `releaseShape()`, `setDebug()`, and the allocation-free
`BodyState.readPose()` / `getInterpolatedPose()` / `resetPoseCache()`.
- **`dynamicMeshStrategy`** on the body options: a trimesh on a dynamic body becomes a convex
hull rather than falling through the world ([#112](/api/shapes#trimeshes)).
- `constraintSystem.constraints`, `removeConstraintsForBody()`, `removeAllConstraints()`, and
typed `ConstraintType` / `ConstraintOptions` / `ConstraintTypeMap`.
- **[`` / `useAttractor()`](/api/physics#).** A point that pulls (or pushes)
every dynamic body within `range`, with rapier's three falloff curves — a
`@react-three/rapier` scene's attractors port across unchanged.
- **[`` collider renderer](/api/physics#debug) / ``.** One wireframe per
body built from its real Jolt shape (so it shows what a dynamic trimesh actually fell back to),
coloured by motion type, plus constraint lines and `showContacts`. Replaces the old per-body
`debug` boolean.
- **[Heightfield generation](/api/shapes#generation) and [surface materials](/api/shapes#surface-materials).**
`` / `` build terrain synchronously with no image or
loader; `generateHeightfield`, `samplesFromGenerator`, `heightfieldToGeometry`,
`validateHeightfieldSize`, `psrdnoise2` / `simplex2` / `fbm2`. `materials` / `materialIndex` give
individual quads their own friction/restitution via a new `SurfaceMaterialTable`.
- **[Named collider components and ``](/api/colliders).** ``,
``, ``, ``, ``,
``, ``, `` — rapier-compatible `args`
(half extents) over ``. ``
picks the auto shape rapier-style, or turns it off entirely.
- **[Sub-shape identity on contacts](/api/rigid-body#which-part-of-a-compound-was-hit).**
`CollisionPayload.targetSubShape` / `otherSubShape` (`{ id, index, userData, descriptor }`), and
per-`` `userData` / `name` / `onCollisionEnter` / `onCollisionPersist` / `onCollisionExit`
/ `onSensorEnter` / `onSensorExit` scoped to that sub-shape.
- **[`activateOnChange`](/api/rigid-body#activation) and [`matrixAutoUpdate`](/api/rigid-body#matrixautoupdate).**
Opt out of a mover setter waking a sleeping body, or of three's own per-object matrix recompute
in the frame sync.
- **[Static bodies can be moved](/api/rigid-body#moving-a-static-body).** `position`/`rotation`
work on `type="static"` now; `BodySystem.movedStatics` drains once a frame so the object stays in
sync — scenes whose statics never move pay nothing.
- **[`setKinematicTarget(position, rotation?)` / `clearKinematicTarget()`](/api/rigid-body#kinematic-platforms).**
A sticky target re-applied at the top of every substep, so a driven platform converges on it
however many substeps a frame runs, instead of `moveKinematic`'s one-shot nudge lurching once per
rendered frame.
- **A shared query base.** `QueryBase` / `CastQueryBase` / `HitBase` — see [Queries: shared
base](/api/queries#shared-base). No public property, method or constructor signature changed;
worth knowing about because `destroy()` is now idempotent on *every* query type, not just
`ShapeCollider`.
- **Real world teardown.** `PhysicsSystem.destroy()` walks the whole world in dependency order
instead of freeing the `JoltInterface` and stopping, `` defers it past the React commit
so children clean up against a live world, and `registerDisposable()` ties a non-body object's
lifetime to the world. `JoltModule` (renamed from `Raw`, which is kept as an alias),
`getJoltModule()` and `free()` are exported from the package root. See [Destroying a
world](/api/physics#destroying-a-world) and [Memory & lifecycle](/advanced/memory).
- **Unlimited concurrent worlds**, gated by the real WASM heap instead of a fixed cap of three —
see [the heap check](/advanced/memory#reaching-the-module).
### `@react-three/jolt/addons`
- **[Gamepads](/api/addons#gamepads).** An in-house poller with `CommanderOptions.gamepad`
(`deadzone`, `axisThreshold`, `buttonThreshold`), the `standardGamepadButtons` /
`gamepadButtonName` / `standardGamepadSticks` helpers, `hasGamepadSupport()` and
`GamepadPoller`. Connect/disconnect is handled, and a yanked controller releases whatever it was
holding.
- **[`useLookCommand` touch and gamepad](/api/addons#uselookcommand)** — `{ mouse, touch, gamepad,
sensitivity, invertY }`.
- **[``](/api/addons#usecommander-and-commanderprovider) / `CommanderContext`**
to scope a commander to a subtree.
- **`CommandInfo` is a real exported type** — callbacks used to be declared as
`(info: CommandCallback) => void`, so `info` was typed as the callback itself and every
`info.isInitial` needed a `@ts-ignore`. **Delete those.** `CommandValue`, `CommandOptions`,
`CommandEvent`, `CommandState` and `VectorBinding` are exported too.
### `@react-three/jolt/controllers`
- **[`` / `useVehicle`](/api/controllers#vehicle).** One component for both vehicle types,
a typed `vehicleSettings` prop, injectable chassis (`bodyObject`, children-as-chassis) and
wheels (`wheels`, `wheelObjects`), and `followCamera`.
- **[Camera rig options](/api/controllers#options).** `cameraPosition`, `minPitch` / `maxPitch`
(the old hard-coded `-1.5` / `0.5` clamp), and everything else set **before** the first step
rather than mutated onto a rig that is already running.
- **[`followMode`](/api/controllers#follow-modes)** — `'free'`, `'movement'`, `'lookAt'`.
- **[Whiskers](/api/controllers#whiskers)** — the boom steers around a corner before the wall
becomes a problem.
- **Real teardown.** `CharacterControllerSystem`, `CameraRigManager` / `CameraBoom`,
`VehicleSystem` / `VehicleManager` and `WheelState` all have idempotent `destroy()` methods, and
each system's pre-step listener is a stored reference so it can actually be removed. The
components release everything on unmount, so mounting and unmounting them no longer leaks.
- **[Character controller events](/api/controllers#events), `isMoving`, `isSliding` and
`isGrounded`.** `CharacterControllerSystem.events` is now public and typed
(`CharacterEventMap`): `move`, `stop`, `slide`, `slideEnd`, `jump`, `land`, `ground`, `airborne`,
`crouch`, `stand`, `contactAdded`, `contactPersisted`, `contactRemoved`, plus the existing
`action`. New `` props for every one of them, `useCharacterEvent(system,
type, handler)`, and `moveThreshold` / `slideThreshold`. `isMoving`/`isSliding` used to be
declared fields nothing ever assigned — they're real getters now, alongside a new `isGrounded`.
- **[`headAngle` / `onHeadHit`](/api/controllers#head-and-ceiling-collision).** A character bumping its
head on a ceiling/overhang no longer keeps its upward velocity until gravity alone brings it
back down — a contact within `headAngle` of straight-up cancels it, once per new contact.
- **[Vehicle secondary physics](/api/controllers#secondary-physics).** `bodyRoll` (spring-damped
chassis tilt), `wheelSmoothing` (eased rendered suspension/steering), and `skid` detection with
`onSkidStart` / `onSkidEnd`, all on by default and independently configurable through
`vehicleSettings` (or `setBodyRoll()` / `setWheelSmoothing()` / `setSkid()` live). Plus
[engine/audio readouts](/api/controllers#readouts-and-events): `rpm`, `gear`, `shifting`,
`clutch`, `throttle`, `brakeInput`, `speed`, `speedKmh`, `skidding`, `onEngine(fn)`. `WheelState`
publishes `slipRatio`, `lateralSlip`, `isSkidding`, `hasContact`, `suspensionLength`,
`spinVelocity` and `spinAngle`.
## 7. Behaviour changes
### ``
- **`interpolate` now actually interpolates.** It was declared but commented out of the
destructure, so it never reached the physics system; with it wired up, bodies are drawn between
the last two steps. The frame loop no longer allocates.
- **`timeStep` is a new prop** — the `"vary"` code path was unreachable from React before.
- **`maxSubSteps` is new, and the accumulator is now bounded.** Simulation time beyond
`maxSubSteps * timeStep` is dropped (with a warning under `setDebug(true)`) instead of queued,
which is what used to turn one long frame into a spiral of death. Negative and `NaN` frame
deltas are dropped too.
- **`defaultShape` is new** — the Jolt answer to rapier's `colliders`.
- `updatePriority` is typed `number` instead of `any`.
- **`module` no longer reinitialises the WASM module.** `initJolt(factory)` used to delete
`Raw.module` and spin up a brand new instance on every call — including the one `` made on every render — with no way to free the old one and every live handle left
dangling. The same factory reference now reuses the module, and swapping to a *different* one
while a world exists is refused with a warning. `` also calls `suspend()`
unconditionally now; toggling `module` used to change the number of hooks between renders.
- **The three-world cap is gone, and unmounting actually frees the world.** Every `PhysicsSystem`
used to share a cache keyed by React's `useId()`, so a remount grew the map instead of reusing a
slot, and once it held three entries a fourth `` was silently handed the *first* world's
interface — two components then shared bodies without either knowing, and the loser's filter
tables were orphaned. `destroy()` now really walks the world (disposables → constraints → bodies
→ events → the interface → the listener objects), `` defers it past the React commit so
children clean up against a live world, and mounting past the real heap limit (~six worlds) is an
actionable error instead of an `abort(OOM)` that kills the module. See [Destroying a
world](/api/physics#destroying-a-world).
- `NUM_OBJECT_LAYERS` was `3` while `Layer` had four members (`Layer.RIG` included), so the
broadphase/object layer pair filter tables were one entry too small and wrote past their own end
— aliasing unrelated layer pairs. It's derived from `Layer` itself now (issue #95).
### Bodies and events
- **``'s listener effect never registered anything.** Its dependency array read
`rigidBodyRef.current`, a mutable ref, which is not reactive — on the render that created the
body the effect had already run with `undefined`. If you concluded that contact props didn't
work, they do now.
- **`` never destroyed its bodies or its `InstancedMesh`.** Both are
released on unmount now, `count` changes add and remove bodies incrementally, and shrinking
`count` no longer copies the old, larger buffer over the new one.
- **`BodyState.color` on a non-instanced body** fell through into `setColorAt`, a method only
`THREE.InstancedMesh` has. It works, sets `instanceColor.needsUpdate` on instanced bodies, and
clones a shared material exactly once instead of mutating it.
- **`BodyState.scale = 2`** used to apply a `NaN` scale (`inScale instanceof Number` is always
false). A plain number works, and non-uniform scale is supported wherever Jolt allows it.
- **``** no longer creates or leaks a stale body when `url` changes (or the component
unmounts) while a previous image is still loading, and an invalid sample grid throws a clear
error naming the count instead of failing silently.
- **`BodyState.mass` reports the mass the simulation actually uses.** The getter used to read the
*shape's* density-derived mass, unrelated to a `mass` option or a later write; it now reads
`1 / MotionProperties.GetInverseMass()`. The setter (`MotionProperties.ScaleToMass`) scales the
inertia tensor with the mass and leaves the body's degrees of freedom alone, where the old path
pushed a fresh `MassProperties` through `SetMassProperties` and quietly unlocked every axis you
had locked. See [Material and mass](/api/rigid-body#material-and-mass).
- **`` mounts and grows to N**, instead of starting broken.
- Setting a static body's `position`/`rotation` used to do nothing visible — it moved the Jolt body
but never synced the three.js object. `BodySystem.movedStatics` fixes that; see [Moving a static
body](/api/rigid-body#moving-a-static-body).
- **`` was declared and read by nothing.** It, `restitution` and the new
`gravityFactor` prop now go through the same reactive effect as `mass` and the damping props, and
that effect is keyed on the body instance rather than a non-reactive ref (so it also reaches a
body created later because it has `` children) and tests `!== undefined` instead of
truthiness, so `friction={0}` and `gravityFactor={0}` are no longer read as "unset".
- **`` is a compound *host* now.** When it has to combine several shapes (sibling
colliders, an offset collider, a collider beside a mesh) its children register plain
`ShapeDescriptor`s and the body composes and owns the compound, instead of every child calling
`setActiveShape` and the last one silently winning. A lone collider with a `position`/`rotation`
becomes a one-child compound now, so its offset is honoured instead of dropped as a root
transform. Nothing changes for the common case (one ``, or meshes only).
### Constraints
`ConstraintSystem.removeConstraint()` was an empty function with its body commented out — and it
is `useConstraint`'s entire cleanup path, so every constraint ever created leaked and stayed in
the world. It works now, which means **constraints actually disappear when your component
unmounts**. If you worked around the old behaviour by not removing bodies, you can stop.
Also: `addConstraint`/`useConstraint`/the options object are typed now (`ConstraintType`,
`ConstraintOptions`, `ConstraintTypeMap`), hinge motors call the methods that exist
(`SetTargetAngularVelocity`/`SetTargetAngle`), `createMotorSettings()` no longer throws a
`ReferenceError`, and `useConstraint` re-creates its constraint when the type, bodies or option
values change. New: `constraintSystem.constraints`, `removeConstraintsForBody()`,
`removeAllConstraints()`.
### Queries
- A `Raycaster` in the default `'closest'` mode never reset its collector between casts, so every
cast after the first returned the **first** hit. Fixed — if your code compensated for stale
hits, remove the workaround.
- `AdvancedRaycaster` used to throw `"a JSImplementation must implement all functions"` on its
first cast. Fixed.
- `Multicaster` gained `destroy()` (it leaked its raycaster) and now clears `results` between
casts instead of growing forever.
- `ShapeCollider.destroy()` was a no-op; it now frees everything it owns and is idempotent.
Setting `position`/`rotation`/`matrix` no longer allocates a transform per call, and
`collider.shape` is properly reference counted.
- `Raycaster.drawMarker()` drew a world-axis-aligned cross whatever it hit; markers now orient
along the hit normal, and the debug objects are pooled instead of a fresh
geometry/material/`Object3D` per cast.
- `ShapecastHit`'s constructor used to `destroy()` the by-value return of `GetPointOnRay()` — the
binder's shared static temporary — and `impactNormal` leaked a `BodyID`, `SubShapeID` and
`RVec3` per read. Both fixed.
- `Shapecaster` never freed its default `SphereShape`, and its `shape` setter's
`RShapeCast.set_mShape()` call was dead code (only a read-only `mShape` getter exists on the
runtime binding) — setting `shape` after construction never took effect. Both fixed: the shape is
reference counted like `ShapeCollider`'s, and the setter rebuilds the live `RShapeCast`.
- `useRaycaster` / `useAdvancedRaycaster` / `useMulticaster` only freed their **last** instance, at
unmount — a dependency change (`origin`/`direction`/`type`) built a fresh raycaster without
destroying the one it replaced. Each hook now destroys its previous instance on every dep change.
- **`Raycaster`, `Shapecaster`, `ShapeCollider` and `Multicaster` now share `QueryBase` /
`CastQueryBase` / `HitBase`** (issue #217) instead of duplicating the same filter setup, destroy
bookkeeping and (for the ray-like casters) ~230 lines of debug-drawing code — see [Queries:
shared base](/api/queries#shared-base). No public signature changed, but `Multicaster.destroy()`
is idempotent now where it previously had no guard at all against being called twice (a
double-free of its owned `Raycaster`'s allocations).
### Shapes
- **`` rebuilt its shape only when `type` changed, and never released anything.** Changing
`size` / `radius` / `height` / `scale` / children now replaces the shape exactly once and
releases the superseded one; unmounting releases everything it owns.
- A `ConeGeometry` used to become a NaN-sized cylinder (three keeps its own `{ radius }`
parameters); it is now inferred as a tapered cylinder.
- Cylinders thinner than the default 0.5 convex radius no longer fail to build.
- `generateShapeSettings('box')` with no options no longer throws, and a numeric `size` means a
cube rather than `(size, NaN, NaN)`.
- `updateScaleShape` was a `console.warn` stub; it wraps the shape in a `ScaledShape`.
- The shape system leaked one WASM object per point, vertex and triangle. Building a mesh collider
from a 2k-triangle sphere left 3078 live Jolt objects behind; it now leaves none.
- The heightfield descriptor's sample spacing was `planeWidth / sampleCount`, where `sampleCount`
samples actually span `sampleCount - 1` segments — so the physics field was one sample wider than
the mesh drawn on top of it. It's `planeWidth / (sampleCount - 1)` now, with
`addHeightfield`'s centring offset derived from the same numbers and the depth of a non-square
plane honoured on z. See [Generation](/api/shapes#generation).
### Input (`@react-three/jolt/addons`)
- **`useCommand` tears itself down.** The commander was a module-level singleton that attached
four `window` listeners and a gamepad poll on first use and never released them. It is
reference counted now, commands register in an effect rather than during render, and
everything is removed on unmount.
- `Command.setOptions` indexed by value (`setOptions({ sensitivity: 2 })` wrote `command[2]`);
fixed.
- `VectorCommand` mutated the shared preset when given `bindings`, leaking bindings into every
other command using that preset; fixed.
- `vectorPresets.look` names its vertical directions `up` / `down`, but `VectorCommand` only
mapped `forward` / `backward` onto `y` — so the whole `look` preset drove yaw with its pitch
bindings. Fixed.
- `Commander.updateState` never removed a command that went inactive, so `useCommandState`
consumers kept acting on an input nobody was giving any more. Fixed.
- `useLookCommand` removes its `mouseout` listener and reads state through refs.
- The commander no longer throws in environments with no Gamepad API (SSR, tests), and the
poller removes every listener it adds — `gamepad.js` used to leave a `window` `error` listener
behind permanently.
### Controllers
- **Step listeners are removable.** All four controller systems subscribed with inline arrows that
`removeStepListener`'s identity match could never find, so a destroyed character kept being
pre-stepped against a freed `CharacterVirtual` and `CameraRigManager.detachFromLoop()` silently
did nothing.
- `CharacterController.on`, `CameraRigManager.onCamera` and `VehicleManager.onPreStep` /
`onPostCollide` / `onPostStep` / `onAction` keep their signatures and are reimplemented on the
shared emitter.
- `createWheelSettings` understands an explicit wheel `position`, vector-valued settings and
`suspensionSpring`, and no longer assigns the library's own layout keys onto the Jolt settings.
- The five character-vs-character `CharacterContactListenerJS` callbacks were dead code and are
removed, with a test that fails if Jolt ever starts calling them.
### Memory
The conversion helpers changed contract: `vec3.jolt()`, `vec3.rjolt()` and `quat.jolt()` used to
return **their argument** when it was already a Jolt object, so the call sites that destroyed the
result were freeing memory Jolt still owned. They now always return a new object the caller owns.
If you wrote code against the old behaviour — passing a Jolt vector in and *not* freeing the
result because it was the same object — you are now leaking one object per call. Review those
call sites.
Also fixed along the way: `vec3.jolt(0, 1, 2)` no longer reads a zero first component as "no
argument"; `quat.jolt(undefined)` returns identity instead of throwing; the shape system no
longer leaks one WASM object per vertex/triangle; `RaycastHit`/`ShapecastHit.impactNormal` no
longer leak a `BodyID`, `SubShapeID` and `RVec3` per read.
`generateJoltMatrix()` returns a real `RMat44` copy — the binder returns a pointer to a single
static temporary from a "by value" return, so the matrix used to be silently rewritten by the next
caller, and destroying it freed memory the binder owns.
See [Memory & lifecycle](/advanced/memory) for the full ownership rules, and
[Contributing](/advanced/contributing#working-on-the-physics-layer) before you touch `systems/`.
### Logging
~17 unconditional `console.log` calls are gone, and the remaining warnings are gated behind
`setDebug(true)` (off by default). If you relied on the library's console output, opt back in.
## 8. Packaging
All three packages declare `"sideEffects": false`, ship `exports` maps with a `./package.json`
entry and a `default` fallback, keep `main`/`module`/`types` for older resolvers, and externalise
their real dependencies properly (`gamepad.js` and `suspend-react` used to be silently inlined
into `dist`). `files` now includes `CHANGELOG.md`, and each package declares
`"type": "commonjs"` and `"engines": { "node": ">=22" }` of its own.
The declaration files changed shape: `exports` nests `types` inside `import` / `require`, so ESM
and CJS consumers each get their own bundled `.d.mts` / `.d.cts` rather than a graph of per-file
declarations. That is what fixes `arethetypeswrong`'s "masquerading as CJS" finding under
`node16 (from ESM)`. `publint` and `@arethetypeswrong/cli --pack` are clean in all four resolution
modes on all three packages. Nothing about how you import them changes.
]]>
[!CAUTION]
> Do **not** run `biome check --write` (and never `--unsafe`) across files you didn't touch. Two
> of its "safe" fixes break this codebase's build: rewriting `@ts-ignore` to `@ts-expect-error`
> (many of those suppress errors that only appear on some type-narrowing paths) and splitting a
> default `React` import into `import type`. Format only your own files:
> `yarn biome format --write `. `LINTING.md` lists the rules currently downgraded to
> `warn` and why.
## Merging
> [!CAUTION]
> **Git merges duplicate methods silently.** Two branches that each add a `destroy()`, a listener
> array or a `dispose()` to the same class merge cleanly into a class with the method twice — no
> conflict, and in JavaScript the *last* definition wins, so the first branch's teardown
> disappears without a single error. Before you add a teardown method, a listener list or an
> `on*` helper to an existing class, **grep for one first**; after a merge, grep the file again.
>
> The same applies to the listener convention: `emitter.on(type, fn)` returns an unsubscribe and
> nothing is ever removed by function identity. If you find yourself writing a
> `removeXListener(fn)`, there is almost certainly one already — and it is almost certainly
> deprecated. `PhysicsSystem.events.listenerCount(type)` is the supported test hook for asserting
> that a subscription was actually dropped.
## Tests
Vitest 5 with happy-dom, and the real Jolt WASM module running in Node — the tests are not
mocked. Component tests use `@react-three/test-renderer`.
If you touch anything that allocates Jolt objects, write an **allocation-counting** test:
`test/jolt-alloc.ts` exposes `installAllocTracker(Raw)`, which wraps the module's constructors
and `destroy` so a test can assert the live-object count is flat across many iterations. It
catches leaks (count grows) and double frees (count drifts negative) — neither of which throws on
its own. Note that the tracker swaps `Raw.module`'s identity, which rebuilds the `joltScratch`
singletons once, so warm them before you start counting.
## Commits and changesets
Conventional commits (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`).
Every change that affects a published package needs a changeset:
```sh
yarn change
```
Pick the packages, pick `patch` for fixes and `minor` for features, and describe **what changed
for a user** — the existing changesets in `.changeset/` are long on purpose and are the
repository's real changelog. A GitHub Action opens the version-bump PR from them.
Docs-only and example-only changes don't need one.
## CI
Every pull request runs `.github/workflows/ci.yml` on Node 22: `yarn install --immutable`,
`yarn lint`, `yarn build`, `yarn test`. Run all four locally before opening a PR. Dependabot
handles grouped weekly dependency bumps, excluding majors of `three`, `react` and
`@react-three/*` — those are deliberate, hand-verified upgrades.
## Working on the physics layer
Three things to read before your first PR into `systems/`:
1. [Memory & lifecycle](/advanced/memory) — ownership, by-value static temporaries, and the
helpers that keep you out of trouble.
2. `packages/react-three-jolt/docs/Memory.md` — the original engine-level notes.
3. [The Jolt docs](https://jrouwe.github.io/JoltPhysics/) — the JS API mirrors the C++ one, and
[`JoltJS.idl`](https://github.com/jrouwe/JoltPhysics.js/blob/main/JoltJS.idl) is the
authoritative list of what is exposed to JavaScript.
The IDL is a *declaration*, not a promise about what Jolt actually calls. Measured against
jolt-physics 1.1.0, a `CharacterVirtual` stepping against bodies invokes six of
`CharacterContactListenerJS`'s eleven declared callbacks (`OnAdjustBodyVelocity`,
`OnContactValidate`, `OnContactAdded`, `OnContactPersisted`, `OnContactRemoved`,
`OnContactSolve`); the five character-vs-character ones only fire once a
`CharacterVsCharacterCollision` is installed. Emscripten's `hasOwnProperty` check is lazy — one
per call site — so the unused ones need no stubs. If you are about to add a no-op callback
"because the IDL says so", write a test that fails when Jolt starts calling it instead
(`packages/react-three-jolt/test/controllers/character-contact-listener.test.ts` is the pattern).
]]>