Skip to content

Rapier Physics Reference

A Three.js guide for coding agents. Also covers rapier api reference, react three rapier props, Physics component props, timeStep vary, paused physics, physics interpolation, and 36 more.

Show all 42 aliases

rapier api reference, react three rapier props, Physics component props, timeStep vary, paused physics, physics interpolation, RigidBody props, ccd continuous collision detection, canSleep, lockRotations, lockTranslations, enabledRotations, linearDamping, angularDamping, dominanceGroup, gravityScale, collisionGroups, solverGroups, interactionGroups bitmask, sensor collider, onIntersectionEnter, onContactForce, contact force events, rapier joints, useSphericalJoint, useRevoluteJoint, usePrismaticJoint, useFixedJoint, rope joint spring joint, joint motor configureMotorVelocity, useRapier, world.castRay, raycast physics world, KinematicCharacterController, character controller autostep, snap to ground, computeColliderMovement, setLinvel, setTranslation teleport, wakeUp sleeping body, useBeforePhysicsStep, physics world snapshot

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 runtime
  • timeStep: fixed simulation step, default 1/60. Set 1/30 to 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 determinism
  • paused: freezes stepping while true. Bodies keep their state, so this is the correct pause implementation, not unmounting <Physics>
  • interpolate: default true. 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 frame
  • colliders: world default for automatic collider generation ("cuboid" by default; "ball", "hull", "trimesh", or false). Per-RigidBody colliders overrides it
  • debug: renders collider wireframes. Dev only, expensive at scale
  • updateLoop: "follow" (step inside the frameloop, default) or "independent" (step on its own loop, required for <Canvas frameloop="demand"> on-demand rendering)
  • updatePriority: the useFrame priority the stepping runs at, for ordering against your other useFrame callbacks
  • numSolverIterations: 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 forces
  • ccd: 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 globally
  • canSleep: default true. Sleeping bodies stop simulating until something wakes them. Set false for bodies you constantly poll or drive
  • lockRotations / lockTranslations: freeze all rotation or translation. lockRotations is the standard way to keep a capsule-based player upright
  • enabledRotations / enabledTranslations: per-axis [x, y, z] booleans, the fine-grained version. enabledTranslations={[true, true, false]} makes a 3D body behave 2D
  • linearDamping / angularDamping: velocity decay per second, 0 by default. Acts like drag; use it to stop bodies drifting or spinning forever
  • gravityScale: multiplier on world gravity for this body. 0 floats, negative rises
  • dominanceGroup: 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 response
  • restitution / friction: bounce and rub, applied to the auto-generated colliders. Rapier averages both bodies' values on contact by default
  • mass / density: mass properties for generated colliders. Without either, mass derives from collider volume at density 1
  • sensor: makes the body's colliders sensors (see events section)
  • includeInvisible: auto-collider generation skips visible={false} meshes unless this is set
  • userData: arbitrary data readable from collision payloads via rigidBody.userData
  • position / rotation set the starting pose only. Runtime movement goes through the body API or kinematic types, never by mutating these props (physics.md covers 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
  • collisionGroups gates whether contacts are detected at all (events included). solverGroups gates 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; omitting filters means "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.md covers 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) and other, each a CollisionTarget with rigidBody (raw Rapier body), rigidBodyObject (the Three object), collider, colliderObject; plus manifold (contact points and normal) and flipped (whether the manifold is oriented from the other body's perspective)
  • Sensors: a collider with sensor detects overlaps but produces no physical response. Sensors fire onIntersectionEnter / onIntersectionExit instead of the collision pair
<CuboidCollider
  args={[5, 5, 1]}
  sensor
  onIntersectionEnter={() => console.log("Goal!")}
  onIntersectionExit={() => console.log("left the zone")}
/>
  • Contact force events: onContactForce fires while contacts exert force, with totalForce, totalForceMagnitude, maxForceDirection, maxForceMagnitude plus the usual target / other. Threshold on totalForceMagnitude for 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 of useFrame (which runs per render frame, not per step)
  • useFilterContactPair(cb): programmatic contact filtering; return SolverFlags.COMPUTE_IMPULSE (1) to solve normally, SolverFlags.EMPTY (0) to skip force computation, or null for default behavior
  • useFilterIntersectionPair(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 / setRotation teleport: no collision response along the way, momentum preserved. For scheduled movement use kinematic bodies with setNextKinematicTranslation / setNextKinematicRotation, which give Rapier a velocity to solve against (physics.md has 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);
  • world is the actual Rapier.World singleton (since v1; the old world.raw() proxy is gone), rapier is the raw module for constructing Ray, descs, and static helpers
  • Snapshot plus setWorld is 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
}
  • maxToi limits length: the ray reaches dir.norm() * maxToi, so with a unit direction it is the distance
  • solid: true means a ray starting inside a shape hits at time 0; false treats shapes as hollow and hits the boundary from inside
  • Optional trailing args filter hits: filterFlags (e.g. QueryFilterFlags.EXCLUDE_SENSORS), filterGroups (an interactionGroups mask), 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 returns false
  • Same query family: world.castShape (thick raycast), world.intersectionsWithShape, world.projectPoint
  • Classic use: ground check for jumping, cast down from the player and compare timeOfImpact against 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, read computedMovement, 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 maxHeight if minWidth of 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 move
  • setCharacterMass(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 ecctrl package 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:

  • instances entries are InstancedRigidBodyProps: full RigidBodyProps plus a required stable key, so per-instance type, collisionGroups, or userData are all legal
  • The ref is RapierRigidBody[]: index into it (rigidBodies.current[40].applyImpulse(...)) or iterate to drive every instance
  • colliders on the wrapper sets one automatic collider shape for all instances; collider component children apply per instance

Performance guidance

  • Keep the fixed timeStep, and keep interpolate on: 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
  • debug wireframes are usually the bottleneck in stress tests, not the physics. Turn them off before measuring
  • On-demand rendering: updateLoop="independent" with frameloop="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 wakeUp argument on applyImpulse / setLinvel: a sleeping body silently ignores the call
  • Using addForce for a jump: forces integrate over time, impulses change velocity now. And addForce persists until resetForces, it is not a one-frame push
  • Expecting solverGroups to stop collision events: only collisionGroups gates 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 timeOfImpact 0
  • 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 useFrame and expecting per-step frequency: use useBeforePhysicsStep for 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 manifold data as always oriented from your body's perspective: check flipped
  • Reacting to onContactForce without thresholding totalForceMagnitude, firing on every resting contact

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-three-js-guide({ topic: "rapier-physics-reference" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems