Article Physics & Mechanics · ≈ ⏱ 11 min read

Cannon-es: architecture of a physics engine

Cannon-es is the lightweight JavaScript rigid-body engine behind several simulations on this site — car suspension, fracture, ropes, gears. Here's how its internals actually fit together: World, Body, Shape, Broadphase, Solver and Constraint.

TL;DR: Cannon-es organizes a simulation into a World holding Bodies with collision Shapes; a broadphase (usually sweep-and-prune) cheaply discards non-touching pairs before an iterative Gauss-Seidel solver resolves contacts and constraints each frame using semi-implicit Euler integration, while idle bodies are put to sleep to save CPU.

1. Why Cannon-es

Cannon-es is a maintained TypeScript fork of the original cannon.js library by Stefan Hedman. It's a pure-JS (no WASM) rigid-body physics engine designed to run comfortably inside a browser tab alongside a Three.js renderer. It trades some raw performance and feature completeness against engines like Rapier or Ammo.js (Bullet compiled to WASM) for a much smaller footprint and a simple, readable object model — which is exactly why it shows up so often in educational and small-scale interactive simulations.

Understanding its architecture matters even if you never read the source: every rigid-body engine — Cannon-es, Rapier, Bullet, Havok — is built from the same handful of building blocks. Learn them once here and the same mental model transfers everywhere.

2. The World object

Everything in Cannon-es lives inside a single World instance. It's the top-level container that owns the list of bodies, the gravity vector, the broadphase implementation, the solver, and the list of active constraints and contact materials.

Minimal world setup world = new CANNON.World()
world.gravity.set(0, -9.82, 0)
world.broadphase = new CANNON.SAPBroadphase(world)
world.solver.iterations = 10

Calling world.step(dt) once per animation frame advances every body: it runs the broadphase, narrow-phase contact generation, the constraint solver, and finally integrates positions and velocities forward by dt.

3. Body and Shape

A Body is a rigid object with mass, position, quaternion orientation, linear and angular velocity, damping, and a material. A body owns one or more Shape instances (box, sphere, cylinder, plane, convex polyhedron, trimesh, or a compound of several shapes) that define its collision geometry — deliberately kept separate from any visual mesh, so the same invisible collision box can drive a much more detailed Three.js model.

Body + Shape const body = new CANNON.Body({ mass: 1 })
body.addShape(new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)))
body.position.set(0, 5, 0)
world.addBody(body)

A mass of 0 marks a body as static — infinite effective inertia, never moved by forces or collisions (the ground plane, a wall). Every dynamic body computes its own inertia tensor from its shapes so that torques produce the correct angular acceleration.

4. Broadphase

With N bodies, testing every pair for collision is O(N²) — fine for a dozen bodies, ruinous for a thousand. The broadphase's job is to cheaply discard pairs that obviously cannot be touching (their bounding boxes don't overlap) before the expensive narrow-phase math ever runs. Cannon-es ships three broadphase strategies:

ClassApproachBest for
NaiveBroadphaseBrute-force O(N²)< ~50 bodies, debugging
GridBroadphaseUniform spatial grid bucketsEvenly distributed bodies
SAPBroadphaseSweep-and-prune on sorted AABBsGeneral purpose, default choice

For the full picture on how sweep-and-prune, grids and BVH trees actually work — and how narrow-phase SAT/GJK/EPA algorithms take over from there — see Broad-phase and narrow-phase collision detection.

5. Solver

Once contact pairs are known, Cannon-es generates equations — one per contact point, plus friction equations, plus one per active constraint. The default GSSolver (Gauss-Seidel) is an iterative solver: it walks the equation list repeatedly, adjusting each body's velocity a little at a time so that all constraints are satisfied simultaneously, rather than solving one giant linear system exactly.

Why iterative, not exact?

An exact solve of hundreds of simultaneous contact constraints is expensive and only valid for a single instant — the contact set changes every frame anyway. A handful of Gauss-Seidel iterations (the default is 10) gets visually convincing results for a fraction of the cost, which is the standard trade-off in every real-time engine, not just Cannon-es.

More iterations mean stiffer, more accurate stacks and joints at a higher CPU cost per frame — a direct dial between quality and performance exposed as world.solver.iterations.

6. Constraints

Beyond contacts, Cannon-es exposes explicit joint types that bolt two bodies together with restricted relative motion:

  • PointToPointConstraint — ball-and-socket, pins one local point on each body together (rope segments, ragdoll hips).
  • HingeConstraint — one rotational degree of freedom (doors, wheels, gears — see the Mechanisms simulation).
  • DistanceConstraint — keeps two bodies a fixed distance apart, cheaper than a full point-to-point pin.
  • ConeTwistConstraint — a hinge with a limited swing cone, used for shoulder/hip-like ragdoll joints.
  • LockConstraint — fuses two bodies rigidly, useful for temporarily merging fragments (see the fracture pipeline below).

Each constraint is turned into one or more equations for the same GSSolver described above — architecturally, a joint is just a contact that never breaks.

7. The step() loop

Every call to world.step(dt) runs the same five phases in order:

// Simplified Cannon-es world.step() pipeline
function step(dt) {
  applyForces(bodies);           // gravity, user forces, springs
  const pairs = broadphase.collisionPairs(world);
  const contacts = narrowphase(pairs);   // exact contact points + normals
  solver.solve(dt, contacts, constraints); // Gauss-Seidel iterations
  integrate(bodies, dt);         // semi-implicit (symplectic) Euler
}

The final integration step uses semi-implicit Euler: velocity is updated from the (now solved) forces first, then position is updated using the new velocity. It's first-order accurate but symplectic — it doesn't leak energy the way explicit Euler does. See Verlet, Leapfrog and RK4 for why that distinction matters for long-running simulations.

8. Sleeping bodies

Simulating a body that hasn't moved in twenty frames wastes CPU. Cannon-es tracks each body's kinetic energy over a short window; if it stays below sleepSpeedLimit for longer than sleepTimeLimit, the body is marked SLEEPING and skipped entirely by integration and the solver — until a moving body touches it again, which wakes it up. This is essential for scenes with dozens of resting crates, rubble piles, or fracture debris that eventually come to rest.

9. Common pitfalls

Tunneling at high speed

Cannon-es' default narrow phase is discrete: it checks positions at the start and end of a frame but not in between. A fast projectile (a car at 40 m/s against a thin wall) can pass straight through a thin collider between two frames. Increase body iterations, thicken thin colliders, or shrink dt using a fixed sub-stepped timestep.

Compound shapes vs. trimesh

Trimesh colliders only support narrow-phase contact generation against a limited set of shapes and are far more expensive than a compound of convex primitives (boxes, spheres, cylinders). Prefer decomposing complex geometry into a handful of convex shapes wherever possible — exactly what the Solid Body Fracture simulation does when it Voronoi-splits a mesh into convex chunks.

▶ Live Demo

See Cannon-es in action

Car suspension, solid-body fracture, chain & rope and Maxwell's wheel on this site all run on Cannon-es under the hood.

🚗 Car Physics 🧱 Solid Body Fracture

🔗 Related Simulations

🚗Car Physics 🧱Fracture 🔗Chain & Rope ⚙️Mechanisms