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. ![React Three Jolt](../logo.png) ```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 client-only: ```tsx 'use client'; import { Canvas } from '@react-three/fiber'; import { Physics } from '@react-three/jolt'; ``` See [SSR & Suspense](/advanced/ssr-and-suspense) for the full server-rendering story. ### The webpack workaround > [!WARNING] > Only needed on the **legacy webpack bundler** (`next dev --webpack` / `next build --webpack`). > Turbopack, the default since Next 16, does not need any of this. `jolt-physics` does a Node-only `await import("node:module")` that webpack still tries to resolve statically for the browser target, which fails the build with: ``` UnhandledSchemeError: Reading from "node:module" is not handled by plugins (Unhandled scheme). Import trace: node:module → ./node_modules/jolt-physics/dist/jolt-physics.wasm-compat.js ``` Rewrite the `node:` scheme away and stub the Node builtins for the browser bundle (`next.config.ts`): ```ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { webpack: (config, { isServer, webpack }) => { if (!isServer) { config.plugins.push( new webpack.NormalModuleReplacementPlugin(/^node:/, (resource: { request: string }) => { resource.request = resource.request.replace(/^node:/, ''); }) ); config.resolve.fallback = { ...config.resolve.fallback, module: false, fs: false, path: false, url: false }; } return config; } }; export default nextConfig; ``` No `experiments.asyncWebAssembly` flag is needed: the `.wasm` is never imported as a module, it is decoded at runtime. ([#111](https://github.com/pmndrs/react-three-jolt/issues/111)) ## Choosing a Jolt build `jolt-physics` ships several flavours. Pass an initializer to `` to use one other than the default: | entry point | what it is | | --- | --- | | `jolt-physics` / `jolt-physics/wasm-compat` | **default** — WASM embedded as base64 in the JS bundle | | `jolt-physics/wasm` | WASM as a separate `.wasm` file — smaller JS, one more asset to serve | | `jolt-physics/debug-wasm-compat` | the same, with Jolt's assertions and debug renderer enabled | | `jolt-physics/wasm-multithread` | multi-threaded; needs cross-origin isolation (COOP/COEP headers) | | `jolt-physics/wasm-compat-multithread` | multi-threaded, embedded | | `jolt-physics/debug-wasm-compat-multithread` | multi-threaded, embedded, debug | | `jolt-physics/asm` | asm.js fallback for environments without WASM | ```tsx import { Physics } from '@react-three/jolt'; import InitJolt from 'jolt-physics/wasm'; export function MultithreadedWorld({ children }: { children: React.ReactNode }) { return {children}; } ``` ### The `/wasm` recipe The `wasm` flavour fetches `jolt-physics.wasm.wasm` at runtime, so it has to be able to find it. With Vite, import the file as a URL and hand Jolt a `locateFile`: ```tsx import { Physics } from '@react-three/jolt'; import initJolt from 'jolt-physics/wasm'; import joltWasmUrl from 'jolt-physics/jolt-physics.wasm.wasm?url'; // `module` is called with no arguments, so wrap the initializer to pass options through const loadJolt = () => initJolt({ locateFile: () => joltWasmUrl }); export function World({ children }: { children: React.ReactNode }) { return {children}; } ``` > [!IMPORTANT] > Define the wrapper **outside** the component (or in a `useConst`/`useMemo`). `` keys > its `suspend()` cache on the value you pass, so a new function identity every render > re-initialises the engine. Other bundlers have their own asset-URL syntax; the requirement is only that `locateFile` returns a URL the browser can fetch. If you would rather not think about it, stay on the default `wasm-compat` build. ## Next steps - [Physics](/api/physics) — the world and its props - [RigidBody](/api/rigid-body) — bodies and `BodyState` - [Contributing](/advanced/contributing) — running this repo locally ]]> ` is the entry point to the simulation. Everything physical has to be inside one, the way everything three.js has to be inside a ``. ```tsx import { Physics, RigidBody } from '@react-three/jolt'; ; ``` It suspends while the Jolt WASM module loads, so it needs a `` boundary above it — see [SSR & Suspense](/advanced/ssr-and-suspense). Each `` creates its own `PhysicsSystem` (and Jolt interface) and destroys it on unmount. Most props are reactive: changing them writes straight through to the running system on the next frame rather than rebuilding the world. ## Props | prop | type | default | | | --- | --- | --- | --- | | `gravity` | `number \| number[] \| THREE.Vector3` | `[0, -9.81, 0]` | reactive | | `paused` | `boolean` | `false` | reactive | | `interpolate` | `boolean` | `true` | reactive | | `timeStep` | `number \| 'vary'` | `1 / 60` | reactive | | `maxSubSteps` | `number` | `5` | reactive | | `updatePriority` | `number` | `0` | mount only | | `updateLoop` | `'follow' \| 'independent'` | `'follow'` | mount only | | `debug` | `boolean` | `false` | reactive | | `defaultShape` | [`ShapeType`](/api/shapes#shapetype) | — | reactive | | `defaultBodySettings` | `Jolt.BodyCreationSettings` fields | — | reactive | | `defaultDynamicMeshStrategy` | `'convex' \| 'decompose' \| 'error'` | — | reactive | | `module` | Jolt module initializer | `jolt-physics` | mount only | Plus the world [event props](#world-events): `onCollisionEnter`, `onCollisionPersist`, `onCollisionExit`, `onSensorEnter`, `onSensorExit`, `onIntersectionEnter`, `onIntersectionExit`, `onSleep`, `onWake`, `onSettled` and `onActivityChange`. ```tsx {children} ``` ### `gravity` World gravity. A tuple or `THREE.Vector3` is used as-is; a plain **number is read as a downward magnitude**, so `gravity={20}` means `[0, -20, 0]`. `0` is a valid value (zero-g). Changing it calls `physicsSystem.setGravity()` at runtime. Gravity is compared by value, not by identity, so an inline `gravity={[0, -9.81, 0]}` does not reallocate a Jolt vector on every parent render. ### `paused` Blocks the physics step. Rendering, shaders and your `useFrame` callbacks carry on, the scene stays interactive, queries still work, and unpausing resumes from exactly where it stopped — this is not a "freeze the page" switch, only a "stop time" one. ### `interpolate` The simulation runs on its own fixed clock, which almost never lines up with your render rate, so without interpolation objects visibly stutter whenever a frame lands between two steps. With it on, each object is drawn on the path between the last two steps instead of snapped to the most recent one. It only changes what is *drawn* — the bodies themselves are untouched — and it is ignored when `timeStep="vary"`, because then every step already lands on a frame. ### `timeStep` The length of one physics step, in seconds. A fixed step is what makes a simulation reproducible: the same inputs give the same result regardless of frame rate. `"vary"` steps with the render delta instead (clamped to 0.5s, 1–2 substeps). It never falls behind, but it is not deterministic and it disables interpolation. ### `maxSubSteps` The most fixed steps a single frame is allowed to run. When a frame takes far longer than `timeStep` — a backgrounded tab, a debugger pause, a large asset decode — simulation time beyond `maxSubSteps * timeStep` is **dropped** rather than queued. Without that cap each slow frame makes the next one slower still, until the app locks up (the "spiral of death"). Turn on `debug` to log when time is being dropped. ### `updateLoop` / `updatePriority` `"follow"` (the default) steps from r3f's `useFrame`, in sync with rendering. `"independent"` steps from its own `requestAnimationFrame` loop. `updatePriority` is passed straight to `useFrame`; as in r3f, **any non-zero priority means you take over rendering yourself**. It only applies to `"follow"`. ### `debug` Turns on debugging globally: the world draws a **wireframe of every collider in it**, and the systems log lifecycle information. Individual [``](/api/rigid-body#props) props can opt in without switching the whole world on. The overlay is one mesh per body, built from the body's *actual* Jolt shape — so it shows the convex hull a trimesh fell back to, the compound you assembled and the scale it was really given, not the three.js geometry you handed in. Wireframes are coloured by motion type: | colour | meaning | | --- | --- | | grey | static | | blue | kinematic | | green | dynamic, awake | | yellow | dynamic, asleep | | magenta | sensor | | white | constraint anchors | Turning it on for a running scene backfills the bodies that already exist; turning it off removes every wireframe and all of the per-frame work with it. Geometry is cached per Jolt shape, so a thousand boxes sharing one shape are triangulated once. For the overlay's own options, mount [``](#debug-component) yourself instead of (or as well as) setting `debug`. #### `` ```tsx import { Debug, Physics } from '@react-three/jolt'; ; ``` | prop | default | meaning | | --- | --- | --- | | `colors` | see above | override any of the per-category colours | | `showConstraints` | `true` | draw a line between each constraint's two anchor points | | `showContacts` | `false` | draw the contact points and normals of the last step | | `contactNormalLength` | `0.25` | length of the drawn normals, in world units | | `maxContacts` | `1024` | cap on contact segments drawn per frame | | `depthTest` | `false` | let the scene occlude the wireframes | | `updatePriority` | `0` | `useFrame` priority for the overlay update | `showContacts` is off by default because it subscribes to `collisionPersist`, which makes Jolt report (and the library wrap) every manifold of every touching pair, every step. The overlay is render-only: it reads body poses and shapes and writes three.js matrices, never the other way round, so having it mounted cannot change the simulation. It draws the same interpolated pose the bodies' own meshes get, so wireframes never lead their meshes. > [!NOTE] > This is built from the library's own shape triangulation, not Jolt's `DebugRenderer` — that > class only exists in jolt-physics' debug builds and has no JS binding in 1.1.0. Raycasters deliberately do **not** follow this flag — they can fire thousands of times a second and you rarely want the screen filled with lasers. Turn debugging on per caster instead (see [Queries](/api/queries#debugging)). > [!NOTE] > `debug` controls in-scene visualisation. The library's internal `console` output is separate > and off by default — enable it with `setDebug(true)` (see [Memory & lifecycle](/advanced/memory#debug-output)). ### `defaultShape` The collision shape used for bodies that don't ask for one, instead of guessing from each geometry. `` is the Jolt equivalent of rapier's `colliders` prop. Individual `` props still win. See [Shapes](/api/shapes). ### `defaultDynamicMeshStrategy` World wide fallback for `` (issue #211): what a **dynamic** body does with a trimesh shape when it doesn't say for itself. A per-body `dynamicMeshStrategy` always wins. See [Trimeshes](/api/shapes#trimeshes). ### `defaultBodySettings` Jolt `BodyCreationSettings` fields merged into **every** body this world creates. It is applied before any body exists, so it covers the first frame too. > [!WARNING] > This injects directly into the Jolt settings pipeline, so the keys are Jolt's own names — which > almost always start with `m`. ```tsx const defaultBodySettings = { mRestitution: 0.5 }; ; ``` ### `module` A `jolt-physics` module initializer to use instead of the bundled default — how you pick a different [build variant](/getting-started/installation#choosing-a-jolt-build), including the `debug-wasm-compat` build whose `JoltInterface.sGetTotalMemory()` / `sGetFreeMemory()` let you profile the WASM heap. ```tsx import InitJolt from 'jolt-physics/wasm'; {null}; ``` Keep its identity stable across renders — a module-level import like the one above, not an inline arrow. Calling it again with the *same* factory reuses the module that is already running. Switching to a **different** factory while a `` world exists is refused (every live body, shape and constraint points into the old module's heap): it warns under [`setDebug(true)`](/advanced/memory#debug-output) and keeps the active module. Initialise the variant you want before any `` mounts. The prop is a single unconditional `suspend()` keyed on `module ?? 'default'`, so toggling it does not change the number of hooks between renders. ## World events Jolt's contact listener is global, so `` is the **cheap** path and the per-body [`` props](/api/rigid-body#events) are the fan-out. A world handler fires **once per pair**, with `target` set to the body with the lower `handle`, so a world-wide counter is right without dividing by two. ```tsx console.log(e.target.handle, e.other.handle)} onSensorEnter={(e) => console.log('entered a sensor')} onSleep={(e) => console.log(e.handle, 'asleep')} > {null} ``` Every name, payload shape, ordering rule and the "don't retain the payload" contract is the same as for a body — see [RigidBody → Events](/api/rigid-body#events). `onIntersectionEnter` / `onIntersectionExit` are the rapier-compatible aliases of the sensor props. ### Steady state ```tsx console.log('everything is asleep')} onActivityChange={(active, total) => console.log(`${active}/${total} awake`)} > {null} ``` `onSettled` is **edge triggered**: it fires on the step where the last awake body goes to sleep, and not again until something wakes up. A world that was never active does not announce itself settled, and removing the last awake body settles the world (Jolt's `RemoveBody` deactivates synchronously). Both are driven by a count the activation listener maintains, so they cost one comparison per step rather than a per-frame scan of every body. The same numbers are readable at any time: | member | | | --- | --- | | `bodySystem.activeBodyCount` | awake bodies | | `bodySystem.simulatedBodyCount` | dynamic + kinematic bodies | | `bodySystem.isSettled` | `activeBodyCount === 0` | `activityChange` is emitted after that step's sleep/wake events, so a handler that counts those agrees with the totals. ### Subscribing imperatively ```tsx import { useJolt, useWorldEvent } from '@react-three/jolt'; import { useEffect } from 'react'; function Watcher() { const { physicsSystem } = useJolt(); // either the hook… useWorldEvent('collisionEnter', (e) => console.log(e.other.handle)); // …or the emitter directly; `on` returns the unsubscribe useEffect( () => physicsSystem.events.on('settled', () => console.log('settled')), [physicsSystem] ); return null; } ``` `JoltContext` also exposes the world emitter directly as `useJolt().events`. ## Step hooks The physics step is where a force means a fixed amount of momentum: a rendered frame may run zero, one or five substeps, so a force applied from `useFrame` is frame-rate dependent and one applied from a step hook is not. ```tsx import { useAfterPhysicsStep, useBeforePhysicsStep } from '@react-three/jolt'; function Thruster() { useBeforePhysicsStep((deltaTime, subframe) => { // runs before pending body actions and before Step() }); useAfterPhysicsStep((deltaTime) => { // runs after Step() and after that step's contact/sensor/sleep events were dispatched }); return null; } ``` Both hold the callback in a ref, so an inline arrow does not resubscribe on every render, and both unsubscribe on unmount. The imperative equivalents are `physicsSystem.onBeforeStep(fn)` and `physicsSystem.onAfterStep(fn)`, each returning its unsubscribe. The order, per substep, is: ``` beforeStep → pending body actions → joltInterface.Step() → queued events → afterStep ``` > [!NOTE] > There are no `onBeforeStep` / `onAfterStep` props on ``. The hooks are the React > surface; they run per substep, which a prop on the world component would not make obvious. ## `useJolt()` Inside ``, `useJolt()` gives you the systems behind the components. ```tsx import { useJolt } from '@react-three/jolt'; function Inspector() { const { jolt, physicsSystem, bodySystem, joltInterface, events, paused, debug, step } = useJolt(); // ... return null; } ``` | field | what it is | | --- | --- | | `physicsSystem` | the `PhysicsSystem` for this world — gravity, stepping, queries, and every other system hangs off it | | `bodySystem` | shortcut for `physicsSystem.bodySystem`: create, look up and remove bodies | | `joltInterface` | the live `Jolt.JoltInterface`. Careful. | | `events` | the world [`Emitter`](#world-events), same object as `physicsSystem.events` | | `jolt` | the raw WASM module (`Raw.module`). **Very** careful — see [Memory & lifecycle](/advanced/memory) | | `paused`, `debug` | the world's current flags | | `step` | the step function the frame loop calls | Calling `useJolt()` outside a `` throws. ## `PhysicsSystem` The object most of the world's behaviour lives on. Beyond the props above: | member | | | --- | --- | | `setGravity(gravity)` | accepts a number, tuple or `THREE.Vector3` | | `paused`, `interpolate`, `timeStep`, `maxSubSteps`, `debug` | the same knobs as the props | | `accumulator` | simulation time not yet consumed by a fixed step, in seconds | | `resetAccumulator()` | drop it (e.g. after a long stall you handled yourself) | | `invalidatePoseCache()` | forget every body's cached poses; frames render live poses until it refills | | `events` | the world [`Emitter`](#world-events) | | `onBeforeStep(fn)` / `onAfterStep(fn)` | [step hooks](#step-hooks), called `(deltaTime, subframe)`; each returns its unsubscribe | | `addPreStepListener(fn)` / `addPostStepListener(fn)` / `removeStepListener(fn)` | **deprecated** aliases of the above | | `getRaycaster()` / `getAdvancedRaycaster()` / `getMulticaster()` / `getShapecaster()` / `getShapeCollider()` | [queries](/api/queries) | | `constraintSystem` | [constraints](/api/rigid-body#constraints) | | `registerDisposable(disposable)` | tie something to this world's lifetime — see [Destroying a world](#destroying-a-world) | | `destroy()` | tear the whole world down; idempotent | | `destroyed` | true once the world has been torn down | > [!NOTE] > `addPreStepListener` / `addPostStepListener` now return an unsubscribe too (they used to return > `void`, so this is source compatible), and `removeStepListener(fn)` removes *every* subscription > made for that function. Both are deprecated because removal by identity cannot work for the > inline arrows callers actually pass — keep the handle `onBeforeStep` gives you instead. ## Stepping and the frame loop One `` per world; multiple worlds in one app are supported, each with its own Jolt interface. There is no fixed cap on how many can be live at once — instead every new world is checked against the real WASM heap before it is built. See [Destroying a world](#destroying-a-world) for what happens when there isn't room, and [Memory & lifecycle](/advanced/memory#reaching-the-module) for the heap numbers. To restart a world from scratch, change its `key`: ```tsx {scene} ``` That unmounts the old `PhysicsSystem` (freeing its bodies and Jolt allocations) and builds a new one. ## Destroying a world `` calls `physicsSystem.destroy()` for you on unmount — you only need this section if you hold a `PhysicsSystem` outside the component (from `useJolt()`, kept in a ref) or you're writing something (a controller, a camera rig, a vehicle system, a query) whose lifetime should track the world's. **Deferred unmount.** React tears a parent's effects down before its children's, so ``'s own unmount cleanup runs *before* `` / `useConstraint` / controller cleanups underneath it. Destroying the Jolt interface right there would leave every child cleaning up against an already-dead world. Instead `` schedules the real `destroy()` through a microtask queued during React's commit, which only runs once the whole commit — including every child's own cleanup — has finished. You don't do anything for this; it's why `` churn and StrictMode's double-invoke both work out safely. **`registerDisposable(disposable)`** ties an object's lifetime to the world's, for anything a `` doesn't already cover — a character controller, a camera rig, a vehicle system, a raycaster you built by hand: ```ts const unregister = physicsSystem.registerDisposable(() => raycaster.destroy()); // or an object: registerDisposable(vehicleManager) calls vehicleManager.destroy() // later, if you tear it down yourself first: unregister(); ``` `destroy()` calls every registered disposable — while the world is still live, so each can still remove its own bodies and listeners normally — before it frees anything else. It is a safety net, not a substitute for your own cleanup: something that unmounts on its own should still call its own `destroy()`, which is expected to unregister itself. Registering after the world is already destroyed is a no-op (there's nothing left to tear down against). **The heap check.** The jolt-physics WASM builds ship a fixed **128 MB** heap with no growth, and one `JoltInterface` costs about **20 MB** of it — so roughly six worlds fit at once. Past that, building another one throws instead of letting emscripten abort the module for the rest of the page: ``` r3/jolt: not enough WASM heap for another physics world - 4.2MB free, about 21.0MB needed, 6 world(s) already live. Call `destroy()` on a PhysicsSystem you no longer need (unmounting its does this for you) before creating another one. ``` If you hit this in a test suite or a page that mounts/unmounts `` quickly, the deferred destroy above may just not have run yet — flushing it (which the heap check itself does before giving up) is exactly what buys back the memory. ## `` A point that pulls (or, with a negative `strength`, pushes) every dynamic body within `range` of it, once per **physics substep** — not once per rendered frame, which is what makes it frame rate independent. The API mirrors [@react-three/rapier's ``](https://github.com/pmndrs/react-three-rapier), so a scene can be ported across unchanged. ```tsx import { Attractor, Physics, RigidBody } from '@react-three/jolt'; ; ``` It renders a ``, so it can be nested, animated or parented to a moving object and the attraction follows: its **world** position is re-read every substep. The step subscription is removed on unmount. ### Props | prop | default | meaning | | --- | --- | --- | | `position` | `[0,0,0]` | local position of the wrapper group | | `strength` | `1` | force magnitude, in newtons; negative repels | | `range` | `10` | bodies further away than this are untouched | | `type` | `'static'` | falloff curve — see below | | `gravitationalConstant` | `6.673e-11` | `'newtonian'` only | | `mode` | `'force'` | `'force'` (frame rate independent) or `'impulse'` (rapier's behaviour) | | `enabled` | `true` | stop attracting without unmounting | | `group` | — | only attract bodies with this [``](/api/collision-groups) id | | `filter` | — | `(body: BodyState) => boolean`, called per body per substep | | `activate` | `true` | wake sleeping bodies that come into range | ### Falloff types Given strength `s`, range `r`, distance `d`, the attracted body's mass `m` and the gravitational constant `G`: | `type` | force | notes | | --- | --- | --- | | `'static'` | `s` | constant inside `range`, independent of mass — every body accelerates the same. The easiest to tune. | | `'linear'` | `s * (d / r)` | ramps *up* with distance, so it is gentlest at the centre. This is rapier's curve, kept identical for parity. | | `'newtonian'` | `G * s * m / d²` | real gravity: inverse square and proportional to mass. Needs a very large `strength` to do anything with the default `G`. | > [!NOTE] > `activate` exists because Jolt never clears the force accumulated on a *sleeping* body. Left > off, an attractor could neither start a body moving nor stop its pull going off all at once the > moment something else woke it. ### `useAttractor()` The imperative half. Takes the same options plus `target` (a ref to any `Object3D`, whose world position is used) or a plain `position`, and returns the live world position the attraction is being applied from. ```tsx const origin = useAttractor({ target: planetRef, strength: 60, range: 20 }); ``` Both forms allocate nothing on the Jolt heap per step: the body list is walked with a hoisted callback and the force goes through the library's shared scratch vector. ]]> ` 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 ``, a `` world, one scene of a game: ```tsx import { CommanderProvider } from '@react-three/jolt/addons'; ; ``` Pass your own instance with `` if you want to configure it up front. The provider destroys the commander it created when it unmounts. `useCommander()` returns whichever commander applies, for `addCommand` / `getCommand` / `addListener` / `removeListener` / `subscribe` / `getSnapshot`. > [!NOTE] > `Commander.getSnapsot` (one `h` short) is deprecated in favour of `getSnapshot`. And there is no > `release()` method — the release function is what `retain()` returns. Environments with no gamepad API (SSR, tests, some browsers) are handled — gamepad input is simply skipped. ## Gamepads Gamepad support is in-house now; the `gamepad.js` dependency is gone. `Commander` diffs `navigator.getGamepads()` inside a `requestAnimationFrame` loop that only runs while at least one consumer retains the commander, and stops when the last one lets go. ```tsx import { Commander, CommanderProvider } from '@react-three/jolt/addons'; const commander = new Commander({ gamepad: { deadzone: 0.2, axisThreshold: 0.02, buttonThreshold: 0.01 } }); {null}; ``` `CommanderOptions` is `{ debug?: boolean; gamepad?: GamepadPollerOptions }`, and `GamepadPollerOptions` is: | option | default | | | --- | --- | --- | | `deadzone` | `0.15` | axis values at or below this magnitude report as `0` | | `axisThreshold` | `0.01` | an axis only emits once it has moved this far from its last emitted value | | `buttonThreshold` | `0.01` | the same for a button's analogue value; a `pressed` change always emits | | `debug` | `false` | | Events keep the payload shape they always had — `{ type: 'gamepad:button' | 'gamepad:axis', detail: { index, button | axis, value, pressed } }` — so anything written against `GamepadInputEvent` is unchanged. The button detail gained an additive optional `name`: the W3C standard-mapping name, which is what `commonCommands` binds its indices against. ```ts import { gamepadButtonName, standardGamepadButtons, standardGamepadSticks } from '@react-three/jolt/addons'; standardGamepadButtons; // ['A', 'B', 'X', 'Y', 'LeftBumper', …, 'DPadRight', 'Home'] as const gamepadButtonName(12); // 'DPadUp' standardGamepadSticks; // { left: [0, 1], right: [2, 3] } — the axis indices of each stick ``` `gamepadconnected` / `gamepaddisconnected` are handled, several pads are tracked by index, and a disconnect **releases whatever that pad was holding**, so a yanked controller can't leave a command stuck down. `hasGamepadSupport()` tells you whether any of this will do anything, and `GamepadPoller` is exported if you want to drive `poll()` from your own loop. ## `useGamepadForCameraControls` Drives a drei `CameraControls` instance from a gamepad stick: ```tsx import { CameraControls } from '@react-three/drei'; import { useGamepadForCameraControls } from '@react-three/jolt/addons'; function Rig({ controls }: { controls: CameraControls }) { useGamepadForCameraControls('look', controls, { sensitivity: 0.03 }); return null; } ``` drei is not a dependency of this package. The `controls` parameter is typed as `CameraControlsLike` — anything with a `rotate(azimuth, polar)` method satisfies it, and drei's `CameraControls` does so without a cast. ## `` A static box floor, because every scene needs one: ```tsx import { Floor } from '@react-three/jolt/addons'; ; ``` Takes `size`, `position`, `rotation` and any `` props. It is a plain static [``](/api/rigid-body) underneath — read it if you want a template for your own helper component. ]]> `, ``, `` 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 ``,** but a three.js loading indicator (or drei's ``/``) is friendlier for a first visit — this is a several-hundred-millisecond wait on a cold cache. - **The module is loaded once per page.** Later `` mounts (a second world, a remount after a `key` change) resolve from the cache and never suspend again. - **After the boundary resolves, `` renders `null` for one more render** while it builds its system. Children mount on the render after that, so a child effect never sees a half-built world. ### Preloading To pay the cost earlier — during a menu, say — warm the cache before the world mounts. The key is `['jolt']`, or `['jolt', module]` if you pass a [custom build](/getting-started/installation#choosing-a-jolt-build): ```ts import { initJolt } from '@react-three/jolt'; import { preload } from 'suspend-react'; preload(() => initJolt(), ['jolt']); ``` `suspend-react` is a dependency of `@react-three/jolt`; add it to your own `package.json` if you import from it directly. ## Server rendering Physics is client-only. Not "works badly on the server" — a `` needs a DOM and a WebGL context, and Jolt needs WebAssembly and a frame loop. In the Next.js App Router, mark the file that renders the canvas as a client component: ```tsx 'use client'; import { Canvas } from '@react-three/fiber'; import { Physics } from '@react-three/jolt'; import { Suspense } from 'react'; export function Scene({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` That is usually enough: the component is skipped during the server pass and hydrates on the client. If your setup does try to render it on the server (or a dependency touches `window` at import time), keep it out of the server bundle entirely: ```tsx import dynamic from 'next/dynamic'; const Scene = dynamic(() => import('./scene').then((m) => m.Scene), { ssr: false }); ``` > [!NOTE] > Nothing in `@react-three/jolt` runs at import time — no `window` access, no module > instantiation — so importing it on the server is harmless. It is the rendering that has to be > client-side. See [Installation](/getting-started/installation#bundlers) for the bundler configuration Next.js needs (nothing for Turbopack; a small `webpack` shim for the legacy bundler). ## When the WASM module fails to load A failed fetch, a blocked CDN, an environment without WebAssembly: `initJolt()` rejects, and on the next render `` **throws that error** instead of a promise. A `` boundary does not catch errors — you need an error boundary. ```tsx import { Physics } from '@react-three/jolt'; import { Component, type ErrorInfo, type ReactNode, Suspense } from 'react'; class PhysicsErrorBoundary extends Component< { fallback: ReactNode; children: ReactNode }, { failed: boolean } > { state = { failed: false }; static getDerivedStateFromError() { return { failed: true }; } componentDidCatch(error: Error, info: ErrorInfo) { console.error('jolt failed to load', error, info); } render() { return this.state.failed ? this.props.fallback : this.props.children; } } export function SafeWorld({ children }: { children: ReactNode }) { return ( }> {children} ); } ``` Error boundaries have to be class components; [`react-error-boundary`](https://github.com/bvaughn/react-error-boundary) wraps that up if you prefer. Two properties of the underlying cache matter here: - **The failure is cached.** Once the load has rejected, every subsequent `` throws the same error immediately — remounting alone does not retry. - **To retry, clear the cache entry first:** ```ts import { clear } from 'suspend-react'; clear(['jolt']); // then re-mount ``` A sensible fallback is a non-physical version of the scene — static meshes, animations, a message — rather than a blank canvas. ### Other failure modes | symptom | cause | | --- | --- | | Hangs on the fallback forever | the `.wasm` file is 404ing with the `wasm` build. Check your `locateFile` — or use the default `wasm-compat` build, which has no separate file | | `UnhandledSchemeError: node:module` at build time | webpack; see [the workaround](/getting-started/installation#the-webpack-workaround) | | Works in dev, blank in production | the WASM asset isn't being copied into the deploy output | | Multi-threaded build never starts | `wasm-multithread` needs cross-origin isolation (`COOP`/`COEP` headers) | | `useJolt must be used within a JoltProvider` | a hook is outside ``, or in a sibling of it rather than a child | Turn on [`setDebug(true)`](/advanced/memory#debug-output) to see the library's own warnings while diagnosing any of these. ]]> [!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). ]]>