The core classes of cannon-es — the maintained fork of Cannon.js — for building rigid-body physics in the browser: World, Body, Shape, Material, Constraint, collision filtering, contact events, and the exact pattern for syncing bodies to Three.js meshes every frame.
CANNON.World is the top-level container: gravity,
broadphase, solver, and the list of bodies/constraints/contact
materials all live on it. Call world.step(dt) once
per animation frame to advance the simulation.
| Property / Method | Type | Purpose |
|---|---|---|
gravity |
Vec3 | Constant acceleration applied to all dynamic bodies |
broadphase |
NaiveBroadphase | SAPBroadphase | Pair culling strategy before narrow phase |
solver.iterations |
number | PGS solver iterations per step (default 10) |
defaultContactMaterial |
ContactMaterial | Friction/restitution used when no pair-specific material set |
world.addBody(body) |
method | Register a Body with the world |
world.addConstraint(c) |
method | Register a Constraint (joint) |
world.step(dt, time, maxSubSteps) |
method | Advance simulation by dt seconds (fixed timestep + interpolation) |
import * as CANNON from 'cannon-es';
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.82, 0),
});
world.broadphase = new CANNON.SAPBroadphase(world);
world.solver.iterations = 10;
// Fixed-timestep step call in the render loop
const fixedTimeStep = 1 / 60;
function animate() {
requestAnimationFrame(animate);
world.step(fixedTimeStep);
renderer.render(scene, camera);
}
CANNON.Body represents a single rigid body: mass,
position, quaternion (orientation), velocity, angular velocity,
and one or more attached shapes. A body with mass: 0
is static — infinite mass, never moves, but
participates in collisions.
| Property | Type | Notes |
|---|---|---|
mass |
number | 0 = static, >0 = dynamic |
position |
Vec3 | World-space center of mass |
quaternion |
Quaternion | Orientation — copy directly into mesh.quaternion |
velocity / angularVelocity |
Vec3 | Linear / angular velocity in world space |
type |
Body.DYNAMIC | STATIC | KINEMATIC | KINEMATIC bodies are moved by script, unaffected by forces |
linearDamping / angularDamping |
number 0–1 | Per-step velocity decay (air resistance approximation) |
fixedRotation |
boolean | Locks orientation — useful for characters/props that shouldn't tip over |
allowSleep / sleepSpeedLimit |
boolean / number | Bodies at rest go to sleep — skipped by the solver until disturbed |
ccdSpeedThreshold |
number | Enables continuous collision detection above this speed (prevents tunneling) |
const body = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(0, 5, 0),
shape: new CANNON.Sphere(0.5),
linearDamping: 0.01,
});
body.applyForce(new CANNON.Vec3(0, 0, 50), body.position);
body.applyImpulse(new CANNON.Vec3(5, 0, 0), body.position);
world.addBody(body);
A Body can carry multiple shapes (compound bodies),
each with an optional local offset and
orientation via
body.addShape(shape, offset, quaternion).
| Shape | Constructor | Use case |
|---|---|---|
| Box | new Box(new Vec3(hx,hy,hz)) | Half-extents — crates, walls, platforms |
| Sphere | new Sphere(radius) | Cheapest narrow-phase test — balls, particles |
| Cylinder | new Cylinder(rTop, rBottom, h, segments) | Barrels, wheels (pair with HingeConstraint) |
| ConvexPolyhedron | new ConvexPolyhedron({vertices, faces}) | Custom convex meshes — import from a Three.js BufferGeometry |
| Trimesh | new Trimesh(vertices, indices) | Static concave geometry only — terrain, level meshes |
| Heightfield | new Heightfield(matrix, {elementSize}) | Grid-based terrain — cheaper than Trimesh for landscapes |
| Plane | new Plane() | Infinite static plane — ground, invisible walls |
| Particle | new Particle() | Zero-size point mass — cloth/rope nodes |
Trimesh and Heightfield can only be
attached to mass: 0 bodies — cannon-es' SAT-based
narrow phase requires convexity for dynamic bodies. For a
dynamic concave object, decompose it into multiple convex
shapes on one compound body (e.g. with a convex-hull
decomposition tool).
A Material is just a named tag on a shape/body; the
actual friction and restitution numbers live on a
ContactMaterial that pairs two materials together.
const ice = new CANNON.Material('ice');
const rubber = new CANNON.Material('rubber');
const iceRubberContact = new CANNON.ContactMaterial(ice, rubber, {
friction: 0.02, // low — slides freely
restitution: 0.3, // 0 = no bounce, 1 = perfectly elastic
contactEquationStiffness: 1e8,
contactEquationRelaxation: 3,
});
world.addContactMaterial(iceRubberContact);
const puck = new CANNON.Body({ mass: 1, material: rubber, shape: new CANNON.Cylinder(0.4,0.4,0.1,16) });
const rink = new CANNON.Body({ mass: 0, material: ice, shape: new CANNON.Plane() });
Constraints (joints) restrict the relative motion of two bodies.
All constraints are added to the world with
world.addConstraint(c).
| Constraint | Behaviour |
|---|---|
| PointToPointConstraint | Pins a local point on A to a local point on B — ball-and-socket, ropes |
| HingeConstraint | One rotational DOF around a shared axis — doors, wheels, pendulum arms |
| DistanceConstraint | Fixed distance between two bodies — rigid rope segments |
| LockConstraint | Fully welds two bodies together (no relative motion) |
| ConeTwistConstraint | Ball joint with a cone angle limit — ragdoll shoulders/hips |
// Pendulum: hinge a box to a fixed anchor point
const anchor = new CANNON.Body({ mass: 0 });
anchor.position.set(0, 5, 0);
world.addBody(anchor);
const hinge = new CANNON.HingeConstraint(anchor, bobBody, {
pivotA: new CANNON.Vec3(0, 0, 0),
pivotB: new CANNON.Vec3(0, 1.5, 0),
axisA: new CANNON.Vec3(0, 0, 1),
axisB: new CANNON.Vec3(0, 0, 1),
});
world.addConstraint(hinge);
Every shape carries a collisionFilterGroup (which
group it belongs to) and collisionFilterMask (which
groups it should test against) — both bitmasks. Two shapes
collide only if (A.group & B.mask) and
(B.group & A.mask) are both non-zero.
const GROUP_PLAYER = 1; // 0b0001
const GROUP_ENEMY = 2; // 0b0010
const GROUP_TERRAIN = 4; // 0b0100
const GROUP_TRIGGER = 8; // 0b1000 — no physical response, events only
playerBody.collisionFilterGroup = GROUP_PLAYER;
playerBody.collisionFilterMask = GROUP_ENEMY | GROUP_TERRAIN | GROUP_TRIGGER;
// Enemies collide with terrain but pass through each other
enemyBody.collisionFilterGroup = GROUP_ENEMY;
enemyBody.collisionFilterMask = GROUP_PLAYER | GROUP_TERRAIN;
| Event | Fired on | Payload |
|---|---|---|
'collide' |
body | { body, target, contact } |
'sleep' / 'wakeup' |
body | Body entered/left the sleeping state |
'postStep' |
world | Fired after every world.step() — good place to read updated positions |
playerBody.addEventListener('collide', (e) => {
const impactSpeed = e.contact.getImpactVelocityAlongNormal();
if (impactSpeed > 5) playLandingSound(impactSpeed);
});
Cannon-es has no rendering concept of its own — every frame, copy
each body's position and quaternion
directly into the matching Three.js mesh. Both
libraries use the same axis convention and quaternion layout, so
no conversion is needed.
const bodies = []; // { body: CANNON.Body, mesh: THREE.Mesh }
function addBox(size, mass, position) {
const shape = new CANNON.Box(new CANNON.Vec3(size.x/2, size.y/2, size.z/2));
const body = new CANNON.Body({ mass, shape, position: new CANNON.Vec3(...position) });
world.addBody(body);
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(size.x, size.y, size.z),
new THREE.MeshStandardMaterial({ color: 0x60a5fa }),
);
scene.add(mesh);
bodies.push({ body, mesh });
return body;
}
function animate() {
requestAnimationFrame(animate);
world.step(1/60);
// Sync every mesh to its body's physics state
for (const { body, mesh } of bodies) {
mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);
}
renderer.render(scene, camera);
}
world.step(dt, deltaTime, maxSubSteps) accepts the
actual elapsed time as its second argument and will run extra
fixed sub-steps internally to catch up — call it as
world.step(1/60, realDeltaSeconds, 5) so physics
stays deterministic even if the display refresh rate varies
(60Hz vs 144Hz vs a stalled tab).
mass: 0.
applyForce accumulate and are cleared automatically
each world.step() — but manual integration loops
that skip step() will leak force indefinitely.
world.solver.iterations, enable
ccdSpeedThreshold/ccdSweptSphereRadius,
or substep the simulation (see the
physics engine tutorial).
world.allowSleep, and check that
contactEquationStiffness isn't set too high for
your timestep — extremely stiff contacts can overshoot and
oscillate.
scale property — bake the
scale into the shape's dimensions when you create it
(e.g. multiply Box half-extents), not via the mesh transform.