Official API layer for Rapier and @react-three/rapier. Sources: rapier.rs docs and the pmndrs react-three-rapier README. Course-level setup, cannon-es, collider choice basics, kinematic movement, and instancing walkthroughs live in physics.md. This doc is the prop-by-prop and method-by-method reference on top of that.
Physics component props
<Canvas>
<Suspense>
<Physics
gravity={[0, -9.81, 0]}
timeStep={1 / 60}
paused={false}
interpolate
colliders="cuboid"
debug={false}
updateLoop="follow"
updatePriority={undefined}
numSolverIterations={4}
>
{/* bodies */}
</Physics>
</Suspense>
</Canvas>
gravity: world gravity vector, default[0, -9.81, 0]. Changeable at runtimetimeStep: fixed simulation step, default1/60. Set1/30to simulate at 30 steps per second. The special value"vary"steps by each frame's real delta, which follows the display exactly but the docs warn it can destabilize the simulation and it breaks determinismpaused: freezes stepping whiletrue. Bodies keep their state, so this is the correct pause implementation, not unmounting<Physics>interpolate: defaulttrue. With a fixed timestep the physics ticks and render frames do not line up, so bodies are rendered at a pose interpolated between the last two steps. Disable only if you need meshes exactly on the simulated pose each framecolliders: world default for automatic collider generation ("cuboid"by default;"ball","hull","trimesh", orfalse). Per-RigidBodycollidersoverrides itdebug: renders collider wireframes. Dev only, expensive at scaleupdateLoop:"follow"(step inside the frameloop, default) or"independent"(step on its own loop, required for<Canvas frameloop="demand">on-demand rendering)updatePriority: theuseFramepriority the stepping runs at, for ordering against your otheruseFramecallbacksnumSolverIterations: solver iteration count. More iterations, more rigid stacks, more CPU- Wrap
<Physics>in<Suspense>: the Rapier WASM module loads asynchronously
RigidBody prop surface
<RigidBody
type="dynamic"
colliders="hull"
position={[0, 4, 0]}
rotation={[0, 0.5, 0]}
ccd
canSleep={false}
lockRotations
enabledTranslations={[true, true, false]}
linearDamping={0.5}
angularDamping={0.5}
gravityScale={1}
dominanceGroup={0}
restitution={0}
friction={0.7}
density={1}
linearVelocity={[0, 0, 0]}
angularVelocity={[0, 0, 0]}
includeInvisible={false}
userData={{ tag: "crate" }}
>
<mesh />
</RigidBody>
type:"dynamic"(default),"fixed","kinematicPosition","kinematicVelocity". Kinematic bodies are driven by code, push dynamic bodies, and ignore forcesccd: continuous collision detection. Sweeps the shape along its path so fast bodies (bullets, pinballs) cannot tunnel through thin colliders in one step. Costs extra, enable per body that needs it, not globallycanSleep: defaulttrue. Sleeping bodies stop simulating until something wakes them. Setfalsefor bodies you constantly poll or drivelockRotations/lockTranslations: freeze all rotation or translation.lockRotationsis the standard way to keep a capsule-based player uprightenabledRotations/enabledTranslations: per-axis[x, y, z]booleans, the fine-grained version.enabledTranslations={[true, true, false]}makes a 3D body behave 2DlinearDamping/angularDamping: velocity decay per second, 0 by default. Acts like drag; use it to stop bodies drifting or spinning forevergravityScale: multiplier on world gravity for this body.0floats, negative risesdominanceGroup: integer -127 to 127, default 0. A body with higher dominance is immune to forces from lower-dominance bodies while still pushing them, a one-way collision responserestitution/friction: bounce and rub, applied to the auto-generated colliders. Rapier averages both bodies' values on contact by defaultmass/density: mass properties for generated colliders. Without either, mass derives from collider volume at density 1sensor: makes the body's colliders sensors (see events section)includeInvisible: auto-collider generation skipsvisible={false}meshes unless this is setuserData: arbitrary data readable from collision payloads viarigidBody.userDataposition/rotationset the starting pose only. Runtime movement goes through the body API or kinematic types, never by mutating these props (physics.mdcovers why)
Collision groups and solver groups
- Every collider carries two 32-bit masks: 16 membership bits and 16 filter bits. Groups are numbered 0 to 15
- Two colliders interact only on a mutual match: A's filter contains one of B's memberships AND B's filter contains one of A's memberships
collisionGroupsgates whether contacts are detected at all (events included).solverGroupsgates only whether contact forces are solved: a pair outside each other's solver groups still reports events but passes through physically- Default is member of all groups, interacts with all groups
- The
interactionGroups(memberships, filters)helper builds the bitmask. Both args take a group number or array of group numbers; omittingfiltersmeans "interacts with everything"
import { interactionGroups } from "@react-three/rapier";
// Member of group 0, collides with groups 0, 1, 2
<CapsuleCollider collisionGroups={interactionGroups(0, [0, 1, 2])} />
// Member of groups 0 and 5, collides only with group 7
<CapsuleCollider collisionGroups={interactionGroups([0, 5], 7)} />
// Member of group 12, collides with everything
<CapsuleCollider collisionGroups={interactionGroups(12)} />
- Both props also exist on
<RigidBody>, applying to all its auto-generated colliders
Collider components
- Set
colliders={false}on the body (or<Physics>) and compose:CuboidCollider,RoundCuboidCollider,BallCollider,CapsuleCollider,CylinderCollider,ConeCollider,ConvexHullCollider,TrimeshCollider,HeightfieldCollider MeshCollider type="trimesh" | "hull"wraps a mesh child and generates its collider from that specific geometry, useful when one body mixes auto and mesh-derived colliders- Shared collider props:
args,position,rotation(offset within the body),mass,density,restitution,friction,sensor,collisionGroups,solverGroups, and the event handlers below - Argument conventions are half extents for cuboids and half height first for capsule and cylinder (
physics.mdcovers the sizing pitfalls and per-shape cost) - Colliders can also be placed outside any
RigidBody: they become fixed world geometry
Collision events
Handlers go on <RigidBody> or on individual collider components. Body-level handlers fire for any of its colliders; collider-level handlers scope to that one shape.
<RigidBody
onCollisionEnter={({ target, other, manifold, flipped }) => {
console.log("hit", other.rigidBodyObject?.name);
}}
onCollisionExit={() => {}}
onSleep={() => {}}
onWake={() => {}}
>
<mesh />
</RigidBody>
- Payload fields:
target(this side) andother, each aCollisionTargetwithrigidBody(raw Rapier body),rigidBodyObject(the Three object),collider,colliderObject; plusmanifold(contact points and normal) andflipped(whether the manifold is oriented from the other body's perspective) - Sensors: a collider with
sensordetects overlaps but produces no physical response. Sensors fireonIntersectionEnter/onIntersectionExitinstead of the collision pair
<CuboidCollider
args={[5, 5, 1]}
sensor
onIntersectionEnter={() => console.log("Goal!")}
onIntersectionExit={() => console.log("left the zone")}
/>
- Contact force events:
onContactForcefires while contacts exert force, withtotalForce,totalForceMagnitude,maxForceDirection,maxForceMagnitudeplus the usualtarget/other. Threshold ontotalForceMagnitudefor impact sounds and damage instead of reacting to every graze
Physics step and pair-filtering hooks
useBeforePhysicsStep(cb)/useAfterPhysicsStep(cb): run code immediately around each physics step, the right place for per-step forces instead ofuseFrame(which runs per render frame, not per step)useFilterContactPair(cb): programmatic contact filtering; returnSolverFlags.COMPUTE_IMPULSE(1) to solve normally,SolverFlags.EMPTY(0) to skip force computation, ornullfor default behavioruseFilterIntersectionPair(cb): boolean filter for sensor intersection pairs
Rigid body API via ref
The ref is the raw Rapier RigidBody. Plain { x, y, z } objects are accepted as vectors. Mutating methods take a final wakeUp: boolean; pass true or a sleeping body ignores the call.
const body = useRef<RapierRigidBody>(null);
body.current.applyImpulse({ x: 0, y: 10, z: 0 }, true); // one-off kick
body.current.addForce({ x: 0, y: 10, z: 0 }, true); // continuous, persists until resetForces
body.current.applyTorqueImpulse({ x: 0, y: 1, z: 0 }, true);
body.current.addTorque({ x: 0, y: 1, z: 0 }, true);
body.current.resetForces(true);
body.current.resetTorques(true);
body.current.setLinvel({ x: 0, y: 5, z: 0 }, true); // write velocity directly
body.current.setAngvel({ x: 0, y: 2, z: 0 }, true);
const v = body.current.linvel(); // read velocity
const p = body.current.translation(); // read pose
const q = body.current.rotation(); // quaternion
body.current.setTranslation({ x: 0, y: 3, z: 0 }, true); // teleport, use sparingly
body.current.setRotation({ x: 0, y: 0, z: 0, w: 1 }, true);
body.current.mass();
body.current.isSleeping();
body.current.wakeUp();
body.current.sleep();
body.current.setGravityScale(0.5, true);
body.current.setLinearDamping(1);
body.current.setEnabledRotations(false, true, false, true);
setTranslation/setRotationteleport: no collision response along the way, momentum preserved. For scheduled movement use kinematic bodies withsetNextKinematicTranslation/setNextKinematicRotation, which give Rapier a velocity to solve against (physics.mdhas the pattern)
Joints
Joint hooks connect two body refs and return a RefObject to the raw Rapier impulse joint. Anchor points are local to each body.
import {
useFixedJoint, useSphericalJoint, useRevoluteJoint,
usePrismaticJoint, useRopeJoint, useSpringJoint,
} from "@react-three/rapier";
// Weld: [anchorA, orientationA, anchorB, orientationB]
useFixedJoint(bodyA, bodyB, [[0, 0, 0], [0, 0, 0, 1], [0, -2, 0], [0, 0, 0, 1]]);
// Ball socket, all rotation free: [anchorA, anchorB]
useSphericalJoint(bodyA, bodyB, [[0, -1, 0], [0, 1, 0]]);
// Hinge around one axis: [anchorA, anchorB, axis]
const hinge = useRevoluteJoint(bodyA, bodyB, [[0, 0, 0], [0, 0, 2], [0, 1, 0]]);
// Slider along one axis: [anchorA, anchorB, axis]
const piston = usePrismaticJoint(bodyA, bodyB, [[0, 0, 0], [0, 2, 0], [0, 1, 0]]);
// Max-distance tether: [anchorA, anchorB, maxDistance]
useRopeJoint(bodyA, bodyB, [[0, 0, 0], [0, 0, 0], 3]);
// Spring: [anchorA, anchorB, restLength, stiffness, damping]
useSpringJoint(bodyA, bodyB, [[0, 0, 0], [0, 0, 0], 1, 50, 5]);
- Motors on revolute and prismatic joints:
hinge.current.configureMotorVelocity(velocity, factor)for constant speed,configureMotorPosition(target, stiffness, damping)for a servo. Access any raw joint config through.current - Chains of spherical joints make ropes and ragdolls; revolute chains make robot arms
useRapier and the raw world
const { world, rapier, step, isPaused, setWorld } = useRapier();
world.setGravity({ x: 0, y: -1, z: 0 });
world.bodies.forEach((b) => {});
const snapshot = world.takeSnapshot(); // Uint8Array
const restored = rapier.World.restoreSnapshot(snapshot);
setWorld(restored); // swap the live world
// Manual stepping: valid only when <Physics paused>
step(1 / 60);
worldis the actualRapier.Worldsingleton (since v1; the oldworld.raw()proxy is gone),rapieris the raw module for constructingRay, descs, and static helpers- Snapshot plus
setWorldis the official rewind / replay / determinism-testing path
Raycasting the physics world
Physics raycasts hit colliders, not visual meshes, so they respect collision groups and cost far less than THREE.Raycaster against high-poly geometry.
const { world, rapier } = useRapier();
const ray = new rapier.Ray({ x: 0, y: 10, z: 0 }, { x: 0, y: -1, z: 0 });
const hit = world.castRay(ray, 20, true); // maxToi, solid
if (hit) {
const point = ray.pointAt(hit.timeOfImpact); // origin + dir * toi
const body = hit.collider.parent(); // owning rigid body
}
maxToilimits length: the ray reachesdir.norm() * maxToi, so with a unit direction it is the distancesolid: truemeans a ray starting inside a shape hits at time 0;falsetreats shapes as hollow and hits the boundary from inside- Optional trailing args filter hits:
filterFlags(e.g.QueryFilterFlags.EXCLUDE_SENSORS),filterGroups(aninteractionGroupsmask),filterExcludeCollider,filterExcludeRigidBody(exclude the caster so it does not hit itself),filterPredicate world.castRayAndGetNormal(...)also returns the surface normal;world.intersectionsWithRay(ray, maxToi, solid, callback)reports every hit until the callback returnsfalse- Same query family:
world.castShape(thick raycast),world.intersectionsWithShape,world.projectPoint - Classic use: ground check for jumping, cast down from the player and compare
timeOfImpactagainst a small threshold
Character controller (KinematicCharacterController)
Rapier's collide-and-slide controller for player movement. Not a component; drive it manually against a kinematic body.
const { world } = useRapier();
const bodyRef = useRef<RapierRigidBody>(null);
const controller = useMemo(() => {
const c = world.createCharacterController(0.01); // offset gap, keep small but nonzero
c.setUp({ x: 0, y: 1, z: 0 });
c.setMaxSlopeClimbAngle((45 * Math.PI) / 180);
c.setMinSlopeSlideAngle((30 * Math.PI) / 180);
c.enableAutostep(0.5, 0.2, true); // maxHeight, minWidth, includeDynamic
c.enableSnapToGround(0.5); // stick to ground on downslopes
c.setApplyImpulsesToDynamicBodies(true); // push crates around
return c;
}, [world]);
useFrame((_, delta) => {
const body = bodyRef.current;
if (!body) return;
const collider = body.collider(0);
const desired = { x: input.x * speed * delta, y: -9.81 * delta, z: input.z * speed * delta };
controller.computeColliderMovement(collider, desired);
const move = controller.computedMovement(); // corrected by obstacles
const pos = body.translation();
body.setNextKinematicTranslation({ x: pos.x + move.x, y: pos.y + move.y, z: pos.z + move.z });
const grounded = controller.computedGrounded(); // gate jumping on this
});
- The body is
type="kinematicPosition"with a capsule collider and the flow is always: desired movement in,computeColliderMovement, readcomputedMovement, add to current translation,setNextKinematicTranslation - Gravity is not applied for you: include a downward component in the desired movement every frame
- The controller handles translation only, no rotational movement; rotate the visual or body yourself
- Autostep climbs stairs up to
maxHeightifminWidthof space exists on top; snap-to-ground keeps the character glued when walking down slopes within the threshold numComputedCollisions()/computedCollision(i)enumerate what was hit during the movesetCharacterMass(m)tunes how hard impulses push dynamic bodies; unset, it uses the attached body's mass- Clean up with
world.removeCharacterController(controller)on unmount - The pmndrs
ecctrlpackage packages this pattern as a ready-made component if you do not need custom behavior
InstancedRigidBodies API notes
physics.md covers the setup. API surface beyond that:
instancesentries areInstancedRigidBodyProps: fullRigidBodyPropsplus a required stablekey, so per-instancetype,collisionGroups, oruserDataare all legal- The ref is
RapierRigidBody[]: index into it (rigidBodies.current[40].applyImpulse(...)) or iterate to drive every instance colliderson the wrapper sets one automatic collider shape for all instances; collider component children apply per instance
Performance guidance
- Keep the fixed
timeStep, and keepinterpolateon: cheap smoothness, and"vary"trades determinism and stability for nothing you usually need - Leave sleeping enabled (
canSleep). A resting pile costs almost nothing;canSleep={false}bodies simulate forever - Collider cost ordering (cheap to expensive): cuboid and ball, capsule and cylinder, convex hull, trimesh. Prefer primitives, reserve trimesh for fixed geometry (dynamic trimesh also tunnels, see
physics.md) - Fewer, simpler colliders beat exact ones; compound a few primitives before reaching for a hull
- CCD only on the specific fast bodies that need it
- Collision groups prune broad-phase pairs: partitioning debris so it ignores other debris removes most contact work in dense scenes
debugwireframes are usually the bottleneck in stress tests, not the physics. Turn them off before measuring- On-demand rendering:
updateLoop="independent"withframeloop="demand"keeps physics stepping while renders happen only when needed
Common pitfalls (official-docs layer)
- Missing
<Suspense>around<Physics>, so the WASM load suspends the whole canvas - Forgetting the
wakeUpargument onapplyImpulse/setLinvel: a sleeping body silently ignores the call - Using
addForcefor a jump: forces integrate over time, impulses change velocity now. AndaddForcepersists untilresetForces, it is not a one-frame push - Expecting
solverGroupsto stop collision events: onlycollisionGroupsgates detection, solver groups gate forces - Setting collision groups on one side only: the bitmask test is mutual, both colliders' filters must accept the other's membership
- Raycasting without excluding the caster's own body, so every ray hits yourself at
timeOfImpact0 - Applying
computedMovement()as an absolute position instead of adding it to the current translation - Forgetting gravity in the character controller's desired movement, so the character floats over ledges
- Reading physics state in
useFrameand expecting per-step frequency: useuseBeforePhysicsStepfor step-rate logic - Setting a non-zero character controller offset of 0, or changing it after creation, both called out by the docs as numerically unstable
- Treating
manifolddata as always oriented from your body's perspective: checkflipped - Reacting to
onContactForcewithout thresholdingtotalForceMagnitude, firing on every resting contact