Three.js From Zero · Article s13-04

R3F + Rapier Physics

R3F + Rapier Physics is Article s13-04 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.

← Three.js From ZeroS13-04 · R3F Mastery

Season 13 · Article 04 · R3F Mastery

@react-three/rapier wraps the Rust-WASM Rapier physics engine into JSX. Rigid bodies, joints, sensors, character controllers — production-grade physics with React ergonomics. 200kb WASM, runs at 60fps with thousands of objects.

Code-walkthrough article

Best paired with a fresh project: npm i three @react-three/fiber @react-three/rapier. The snippets here run as-is in a Vite + React app.

The mental model

Every physics object is a <RigidBody> wrapping a regular <mesh>. The rigid body provides collision shape (auto-detected from the mesh), mass, friction, restitution. The mesh provides visuals. Rapier simulates positions; R3F syncs them back to the mesh transforms each frame.

Hello, gravity

import { Physics, RigidBody } from '@react-three/rapier';

<Canvas>
  <Physics gravity={[0, -9.81, 0]}>
    {/* Falling sphere */}
    <RigidBody colliders="ball">
      <mesh position={[0, 5, 0]}>
        <sphereGeometry args={[0.5]} />
        <meshStandardMaterial color="orange" />
      </mesh>
    </RigidBody>

    {/* Static ground */}
    <RigidBody type="fixed" colliders="cuboid">
      <mesh position={[0, -1, 0]} receiveShadow>
        <boxGeometry args={[10, 0.2, 10]} />
        <meshStandardMaterial />
      </mesh>
    </RigidBody>
  </Physics>
</Canvas>

Drop the sphere, it lands, it stops. The ball colliders="ball" tells Rapier to use a sphere collision shape (more efficient than mesh hull); type="fixed" on the ground keeps it from falling.

Forces and impulses

const ballRef = useRef();

return (
  <>
    <RigidBody ref={ballRef} colliders="ball">
      <mesh><sphereGeometry /><meshStandardMaterial /></mesh>
    </RigidBody>
    <button onClick={() => ballRef.current.applyImpulse({ x: 0, y: 10, z: 0 }, true)}>
      Jump
    </button>
  </>
);

The ref points to the Rapier rigid-body instance. applyImpulse = instantaneous velocity change; applyForce = continuous accumulation. The true arg wakes the body if it had gone to sleep.

Joints — connecting bodies

import { useFixedJoint, useSphericalJoint, useRevoluteJoint } from '@react-three/rapier';

function Pendulum() {
  const anchor = useRef();
  const ball = useRef();
  useSphericalJoint(anchor, ball, [[0,0,0], [0,1,0]]);  // joint position on each body

  return (
    <>
      <RigidBody ref={anchor} type="fixed" position={[0, 4, 0]} />
      <RigidBody ref={ball} colliders="ball" position={[1, 3, 0]}>
        <mesh><sphereGeometry /></mesh>
      </RigidBody>
    </>
  );
}

Five joint types: fixed (rigid attach), spherical (free rotation), revolute (single axis hinge), prismatic (slider), generic (custom). Use them for ragdolls (spherical at joints), doors (revolute), pistons (prismatic), or a chain (sequence of spherical).

Sensors — collision events without physics response

<RigidBody type="fixed" sensor onIntersectionEnter={() => pickupCoin()}>
  <mesh><coinGeometry /></mesh>
</RigidBody>

A sensor doesn't block movement — bodies pass through — but fires intersection callbacks. Perfect for game pickups, trigger zones, "exit area" detection.

Character controllers

import { CapsuleCollider, RigidBody, useRapier } from '@react-three/rapier';

function Player() {
  const ref = useRef();
  const { rapier, world } = useRapier();

  useFrame(() => {
    const vel = ref.current.linvel();
    // Read input, apply impulses for movement, etc.
    if (keysPressed.has('w')) {
      ref.current.applyImpulse({ x: 0, y: 0, z: -0.5 });
    }
  });

  return (
    <RigidBody ref={ref} colliders={false} lockRotations>
      <CapsuleCollider args={[0.5, 0.3]} />   {/* half-height, radius */}
      <mesh><capsuleGeometry /></mesh>
    </RigidBody>
  );
}

lockRotations stops the player from tumbling when bumping walls. colliders={false} + manual <CapsuleCollider /> lets you customize the shape independently of the mesh.

Debug rendering

<Physics debug>...</Physics>

One prop. Renders every collider shape as a wireframe overlay so you can see where Rapier thinks your bodies are. Always-on during development.

Common first-time pitfalls

"Bodies fall through the ground." Either no collider on the ground, or you set colliders={false} on a body and forgot to add a manual collider component inside.
"Physics is slow." You're using colliders="trimesh" (mesh hull) for every body. Use "hull", "cuboid", "ball" when possible — they're 10–100× cheaper.
"FPS tanks at 500 bodies." Bodies are not "sleeping." Rapier auto-sleeps quiescent bodies; check your linearDamping and ensure bodies actually come to rest. Otherwise increase Physics step rate or use a coarser collision tolerance.

Exercises

  1. Tower of blocks. 50 cuboid bodies stacked. Click to apply impulse to a random one. Watch them collapse.
  2. Ragdoll. 7 spherical joints between body parts (head, chest, hips, two upper arms, two upper legs). Apply impulse to head; observe.
  3. Coin collector. Sensor rigid bodies for coins; capsule player; collect = increment React state. Game state from physics events.