Physics (Cannon / Rapier)
Three.js handles rendering, but it has no physics. For collisions, gravity, joints, ragdolls, vehicles, and constraint solving, pair it with a physics engine — Rapier (WASM, modern), Cannon-es (smaller, JS), or Ammo.js (Bullet port). Rapier is the recommended default in 2025.
Rapier, bodies, colliders, integration
EXAMPLE
// 1) Install
// npm install @dimforge/rapier3d-compat
import * as THREE from 'three';
import RAPIER from '@dimforge/rapier3d-compat';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
await RAPIER.init(); // load the WASM binary
// 2) Three.js boilerplate
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222a35);
const camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 100);
camera.position.set(4, 4, 8);
new OrbitControls(camera, renderer.domElement);
// 3) Lighting
const sun = new THREE.DirectionalLight(0xffffff, 1.0);
sun.position.set(5, 10, 5);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
scene.add(sun, new THREE.AmbientLight(0xffffff, 0.3));
// 4) Physics world
const gravity = new RAPIER.Vector3(0, -9.81, 0);
const world = new RAPIER.World(gravity);
// 5) Ground — static collider (no rigid body needed, but you can attach one)
const groundMesh = new THREE.Mesh(
new THREE.BoxGeometry(20, 0.2, 20),
new THREE.MeshStandardMaterial({ color: 0x44aa66 }),
);
groundMesh.position.y = -0.1;
groundMesh.receiveShadow = true;
scene.add(groundMesh);
const groundBody = world.createRigidBody(
RAPIER.RigidBodyDesc.fixed().setTranslation(0, -0.1, 0)
);
world.createCollider(
RAPIER.ColliderDesc.cuboid(10, 0.1, 10).setRestitution(0.2).setFriction(0.7),
groundBody,
);
// 6) Spawn a dynamic box
function spawnBox(pos = { x: 0, y: 5, z: 0 }) {
const size = 0.6;
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(size, size, size),
new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff }),
);
mesh.castShadow = true;
scene.add(mesh);
const body = world.createRigidBody(
RAPIER.RigidBodyDesc.dynamic().setTranslation(pos.x, pos.y, pos.z),
);
world.createCollider(
RAPIER.ColliderDesc.cuboid(size/2, size/2, size/2).setRestitution(0.4).setDensity(1.0),
body,
);
return { mesh, body };
}
const dynamics = [];
for (let i = 0; i < 30; i++) {
dynamics.push(spawnBox({ x: (Math.random()-0.5)*4, y: 4 + i*0.6, z: (Math.random()-0.5)*4 }));
}
// 7) Step the world + sync to render
const clock = new THREE.Clock();
function loop() {
const dt = Math.min(clock.getDelta(), 1/30);
world.timestep = dt;
world.step(); // tick physics
for (const { mesh, body } of dynamics) {
const p = body.translation();
const r = body.rotation();
mesh.position.set(p.x, p.y, p.z);
mesh.quaternion.set(r.x, r.y, r.z, r.w);
}
renderer.render(scene, camera);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
// 8) Forces, impulses, velocity
body.applyImpulse({ x: 0, y: 10, z: 0 }, true); // instant momentum change
body.setLinvel({ x: 5, y: 0, z: 0 }, true); // set linear velocity
body.setAngvel({ x: 0, y: 5, z: 0 }, true); // set angular velocity
body.addForce({ x: 0, y: 30, z: 0 }, true); // continuous force (per-step)
// 9) Triggers (sensors) — detect overlap without collision response
const trigger = world.createCollider(
RAPIER.ColliderDesc.cuboid(1, 1, 1).setSensor(true).setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS),
body,
);
const eventQueue = new RAPIER.EventQueue(true);
world.step(eventQueue);
eventQueue.drainCollisionEvents((h1, h2, started) => {
if (started) console.log('overlap', h1, h2);
});
// 10) Raycasting
const ray = new RAPIER.Ray({ x: 0, y: 5, z: 0 }, { x: 0, y: -1, z: 0 });
const hit = world.castRay(ray, 100, true);
if (hit) console.log('hit at distance', hit.toi);
// 11) Joints (constraints)
const joint = RAPIER.JointData.spherical({ x: 0, y: 0, z: 0 }, { x: 0, y: 1, z: 0 });
world.createImpulseJoint(joint, bodyA, bodyB, true);
// Other joints: revolute (hinge), prismatic (slider), fixed.
// 12) Vehicle / character controller
// Rapier has 'KinematicCharacterController' for ground-aware characters
// const controller = world.createCharacterController(0.1);
// controller.computeColliderMovement(collider, { x: 0, y: -1, z: 0 });
// collider.parent().setNextKinematicTranslation(...);
// 13) Performance
// • Run physics at FIXED step (1/60) inside an accumulator — variable dt destabilises
let accumulator = 0;
const STEP = 1/60;
function loopFixed() {
const dt = Math.min(clock.getDelta(), 0.1);
accumulator += dt;
while (accumulator >= STEP) {
world.timestep = STEP;
world.step();
accumulator -= STEP;
}
/* render with interpolation between previous and current state for smoothness */
}
// • Use compound colliders + convex hulls for complex shapes; avoid trimesh for dynamic bodies
// • Sleep static-looking bodies (Rapier auto-sleeps based on velocity threshold)
// • Reuse RAPIER.Vector3 instances rather than allocating per frame
// 14) Alternative libraries
// • cannon-es — pure JS, smaller, slower
// • ammo.js — Bullet port, mature, large WASM bundle
// • Rapier — most modern, deterministic, great docs
// • three-rapier (R3F) — declarative React wrapper
// 15) Common bugs
// • Forgot to sync mesh to body each frame → objects float at origin
// • Used trimesh as a dynamic body → unstable, expensive; use convex hull or compound shapes
// • dt jitter from rAF → use a fixed-step accumulator
// • Massive impulse on body wakes neighbors and tanks FPS — clamp or split events
// • Collider scale != mesh scale → collisions don't match visuals
// • Forgot 'await RAPIER.init()' → 'Cannot read properties of undefined' errors
// • Camera controls + raycast picking → use Three's Raycaster for screen→world, Rapier's for sim queries
Why it matters
Pair Three.js with a physics engine — Rapier is the modern default. Build a separate physics world, mirror each visual mesh with a body + collider, step the world on a fixed timestep, and sync transforms each frame. Reach for triggers (sensors), raycasts, and joints when collisions and constraints replace ad-hoc movement code.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// npm install @dimforge/rapier3d-compat import * as RAPIER from '@dimforge/rapier3d-compat'; await RAPIER.init();Try it Yourself »
Discussion
Loading…