iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…