Physics
<Physics> is the entry point to the simulation. Everything physical has to be inside one, the
way everything three.js has to be inside a <Canvas>.
import { Physics, RigidBody } from '@react-three/jolt';
<Physics gravity={20} debug={false} paused={false}>
<RigidBody>
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
</RigidBody>
</Physics>;
It suspends while the Jolt WASM module loads, so it needs a <Suspense> boundary above it —
see SSR & Suspense.
Each <Physics> 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 | — | reactive |
defaultBodySettings | Jolt.BodyCreationSettings fields | — | reactive |
defaultDynamicMeshStrategy | 'convex' | 'decompose' | 'error' | — | reactive |
module | Jolt module initializer | jolt-physics | mount only |
Plus the world event props: onCollisionEnter, onCollisionPersist,
onCollisionExit, onSensorEnter, onSensorExit, onIntersectionEnter, onIntersectionExit,
onSleep, onWake, onSettled and onActivityChange.
<Physics
gravity={20}
paused={paused}
interpolate
timeStep={1 / 120}
maxSubSteps={8}
updateLoop="follow"
updatePriority={0}
debug={false}
defaultShape="box"
defaultBodySettings={{ mRestitution: 0.5 }}
>
{children}
</Physics>
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 <RigidBody debug> 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> yourself instead of (or as
well as) setting debug.
<Debug>
import { Debug, Physics } from '@react-three/jolt';
<Physics>
<Debug showContacts depthTest />
</Physics>;
| 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.
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).
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).
defaultShape
The collision shape used for bodies that don't ask for one, instead of guessing from each
geometry. <Physics defaultShape="box"> is the Jolt equivalent of rapier's colliders prop.
Individual <RigidBody shape="..."> props still win. See Shapes.
defaultDynamicMeshStrategy
World wide fallback for <RigidBody dynamicMeshStrategy> (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.
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.
This injects directly into the Jolt settings pipeline, so the keys are Jolt's own names — which
almost always start with m.
const defaultBodySettings = { mRestitution: 0.5 };
<Physics defaultBodySettings={defaultBodySettings}>
<RigidBody position={[0, 20, 3]}>
<mesh>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="hotpink" />
</mesh>
</RigidBody>
</Physics>;
module
A jolt-physics module initializer to use instead of the bundled default — how you pick a
different build variant, including the
debug-wasm-compat build whose JoltInterface.sGetTotalMemory() / sGetFreeMemory() let you
profile the WASM heap.
import InitJolt from 'jolt-physics/wasm';
<Physics module={InitJolt}>{null}</Physics>;
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 <Physics> world exists is refused (every live body,
shape and constraint points into the old module's heap): it warns under
setDebug(true) and keeps the active module. Initialise the
variant you want before any <Physics> 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 <Physics on*> is the cheap path and the per-body
<RigidBody on*> props 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.
<Physics
onCollisionEnter={(e) => console.log(e.target.handle, e.other.handle)}
onSensorEnter={(e) => console.log('entered a sensor')}
onSleep={(e) => console.log(e.handle, 'asleep')}
>
{null}
</Physics>
Every name, payload shape, ordering rule and the "don't retain the payload" contract is the same
as for a body — see RigidBody → Events. onIntersectionEnter /
onIntersectionExit are the rapier-compatible aliases of the sensor props.
Steady state
<Physics
onSettled={() => console.log('everything is asleep')}
onActivityChange={(active, total) => console.log(`${active}/${total} awake`)}
>
{null}
</Physics>
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
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.
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
There are no onBeforeStep / onAfterStep props on <Physics>. The hooks are the React
surface; they run per substep, which a prop on the world component would not make obvious.
useJolt()
Inside <Physics>, useJolt() gives you the systems behind the components.
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, same object as physicsSystem.events |
jolt | the raw WASM module (Raw.module). Very careful — see Memory & lifecycle |
paused, debug | the world's current flags |
step | the step function the frame loop calls |
Calling useJolt() outside a <Physics> 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 |
onBeforeStep(fn) / onAfterStep(fn) | 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 |
constraintSystem | constraints |
registerDisposable(disposable) | tie something to this world's lifetime — see Destroying a world |
destroy() | tear the whole world down; idempotent |
destroyed | true once the world has been torn down |
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 <Physics> 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 for what happens when there isn't room, and Memory &
lifecycle for the heap numbers.
To restart a world from scratch, change its key:
<Physics key={resetCount}>{scene}</Physics>
That unmounts the old PhysicsSystem (freeing its bodies and Jolt allocations) and builds a new
one.
Destroying a world
<Physics> 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 <Physics>'s
own unmount cleanup runs before <RigidBody> / useConstraint / controller cleanups
underneath it. Destroying the Jolt interface right there would leave every child cleaning up
against an already-dead world. Instead <Physics> 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 <Physics key={...}>
churn and StrictMode's double-invoke both work out safely.
registerDisposable(disposable) ties an object's lifetime to the world's, for anything a
<RigidBody> doesn't already cover — a character controller, a camera rig, a vehicle system, a
raycaster you built by hand:
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
<Physics> does this for you) before creating another one.
If you hit this in a test suite or a page that mounts/unmounts <Physics> 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.
<Attractor>
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 <Attractor>,
so a scene can be ported across unchanged.
import { Attractor, Physics, RigidBody } from '@react-three/jolt';
<Physics gravity={0}>
<Attractor position={[0, 4, 0]} range={12} strength={40} type="linear">
<mesh>
<sphereGeometry args={[0.4]} />
</mesh>
</Attractor>
<RigidBody position={[8, 4, 0]}>
<mesh>
<sphereGeometry args={[0.5]} />
</mesh>
</RigidBody>
</Physics>;
It renders a <group>, 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 <RigidBody group> 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. |
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.
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.