Physics

The Physics world component, its props, and the systems behind it.

<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

proptypedefault
gravitynumber | number[] | THREE.Vector3[0, -9.81, 0]reactive
pausedbooleanfalsereactive
interpolatebooleantruereactive
timeStepnumber | 'vary'1 / 60reactive
maxSubStepsnumber5reactive
updatePrioritynumber0mount only
updateLoop'follow' | 'independent''follow'mount only
debugbooleanfalsereactive
defaultShapeShapeType—reactive
defaultBodySettingsJolt.BodyCreationSettings fields—reactive
defaultDynamicMeshStrategy'convex' | 'decompose' | 'error'—reactive
moduleJolt module initializerjolt-physicsmount 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:

colourmeaning
greystatic
bluekinematic
greendynamic, awake
yellowdynamic, asleep
magentasensor
whiteconstraint 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>;
propdefaultmeaning
colorssee aboveoverride any of the per-category colours
showConstraintstruedraw a line between each constraint's two anchor points
showContactsfalsedraw the contact points and normals of the last step
contactNormalLength0.25length of the drawn normals, in world units
maxContacts1024cap on contact segments drawn per frame
depthTestfalselet the scene occlude the wireframes
updatePriority0useFrame 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).

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).

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.

Warning

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.activeBodyCountawake bodies
bodySystem.simulatedBodyCountdynamic + kinematic bodies
bodySystem.isSettledactiveBodyCount === 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
Note

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;
}
fieldwhat it is
physicsSystemthe PhysicsSystem for this world — gravity, stepping, queries, and every other system hangs off it
bodySystemshortcut for physicsSystem.bodySystem: create, look up and remove bodies
joltInterfacethe live Jolt.JoltInterface. Careful.
eventsthe world Emitter, same object as physicsSystem.events
joltthe raw WASM module (Raw.module). Very careful — see Memory & lifecycle
paused, debugthe world's current flags
stepthe 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, debugthe same knobs as the props
accumulatorsimulation 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
eventsthe 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
constraintSystemconstraints
registerDisposable(disposable)tie something to this world's lifetime — see Destroying a world
destroy()tear the whole world down; idempotent
destroyedtrue 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 <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

propdefaultmeaning
position[0,0,0]local position of the wrapper group
strength1force magnitude, in newtons; negative repels
range10bodies further away than this are untouched
type'static'falloff curve — see below
gravitationalConstant6.673e-11'newtonian' only
mode'force''force' (frame rate independent) or 'impulse' (rapier's behaviour)
enabledtruestop attracting without unmounting
group—only attract bodies with this <RigidBody group> id
filter—(body: BodyState) => boolean, called per body per substep
activatetruewake 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:

typeforcenotes
'static'sconstant 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.

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.