Physics with Cannon-es
Cannon-es is the modern successor to the original cannon.js library created by Stefan Hedman. The pmndrs fork repackages it as a proper ES module with TypeScript type definitions, enabling tree shaking in bundlers and better editor support. It handles rigid body simulation, collision detection with several shape types, constraints and springs, material contacts, and basic vehicle physics. For games with up to several hundred active bodies, it delivers solid 60 fps performance without the complexity of a WASM-based engine.
Install Cannon-es and Create a World
Install from npm: npm install cannon-es. Import everything with: import * as CANNON from 'cannon-es'. Create the world with: const world = new CANNON.World(); and set gravity with: world.gravity.set(0, -9.82, 0). That is all the setup needed. No async initialization, no WASM loading, no binary fetching. The world is ready to receive bodies immediately.
Choose a broadphase algorithm next. The default NaiveBroadphase tests every possible pair and works fine for small scenes (under 50 bodies). For larger scenes, switch to SAPBroadphase (sweep-and-prune): world.broadphase = new CANNON.SAPBroadphase(world). SAP works best when most objects are spread along one axis. You can also set world.allowSleep = true so that resting bodies stop consuming CPU cycles. Bodies that have not moved for a few frames automatically enter sleep mode and are excluded from the solver until something wakes them.
Add Bodies with Shapes
Every physics object is a CANNON.Body with at least one shape. Create a dynamic body: const body = new CANNON.Body({ mass: 5, shape: new CANNON.Box(new CANNON.Vec3(1, 1, 1)) }). The mass is in kilograms and the box dimensions are half-extents, so this creates a 2x2x2 meter box weighing 5 kg. Set its initial position with body.position.set(0, 10, 0) and add it to the world with world.addBody(body).
For a static floor, set mass to 0: new CANNON.Body({ mass: 0, shape: new CANNON.Plane() }). A zero-mass body is treated as static and will never move regardless of what hits it. Rotate the plane to face upward: body.quaternion.setFromEuler(-Math.PI / 2, 0, 0) since Cannon-es planes face along positive z by default.
Available shapes include Box, Sphere, Plane (infinite), Cylinder, ConvexPolyhedron (for arbitrary convex meshes), Trimesh (triangle mesh for static geometry), and Heightfield (for terrain). You can add multiple shapes to a single body to build compound colliders: body.addShape(new CANNON.Sphere(0.5), new CANNON.Vec3(0, 1, 0)) adds a sphere offset 1 unit above the body's center. This is useful for approximating complex objects like a table (a box top with four cylinder legs) without using an expensive convex hull.
For characters, a Sphere works well in Cannon-es because it slides along surfaces without catching on edges. A capsule shape is not built in, but you can approximate one with a compound shape: a cylinder for the middle and two spheres at the top and bottom.
Configure Materials and Contact Behavior
Materials control how surfaces interact when they touch. Create materials for different surface types: const iceMaterial = new CANNON.Material('ice') and const rubberMaterial = new CANNON.Material('rubber'). Then define how they interact with a ContactMaterial: world.addContactMaterial(new CANNON.ContactMaterial(iceMaterial, rubberMaterial, { friction: 0.02, restitution: 0.8 })). This means rubber on ice has almost no friction and bounces significantly.
Assign materials to bodies: body.material = iceMaterial. If two colliding bodies both have materials, the engine looks up the ContactMaterial for that pair. If no ContactMaterial is defined, it falls back to the world's default contact material, which you can configure with: world.defaultContactMaterial.friction = 0.3 and world.defaultContactMaterial.restitution = 0.1.
Getting materials right has a big impact on game feel. High friction (0.7 or above) makes surfaces sticky and grippy, good for rubber tires on asphalt. Low friction (below 0.1) creates slippery surfaces like ice or oil. Restitution above 0.5 makes objects visibly bouncy. Restitution at 0 means objects thud to a stop on contact. Most game objects work well with friction around 0.3 and restitution around 0.1 to 0.3.
Add Constraints for Mechanisms
Constraints connect two bodies and restrict their relative movement. Cannon-es provides five constraint types. PointToPointConstraint pins two bodies at a shared point, like a ball-and-socket joint. HingeConstraint allows rotation around one axis, with optional motor and angular limits. LockConstraint freezes all relative motion, welding two bodies together. DistanceConstraint keeps two bodies at a fixed separation. Spring applies a restorative force proportional to displacement.
To build a swinging door, create a hinge between the door body and a static wall body: const hinge = new CANNON.HingeConstraint(doorBody, wallBody, { pivotA: new CANNON.Vec3(-1, 0, 0), pivotB: new CANNON.Vec3(1, 0, 0), axisA: new CANNON.Vec3(0, 1, 0), axisB: new CANNON.Vec3(0, 1, 0) }). Then add it: world.addConstraint(hinge). The pivots define where the hinge attaches on each body, and the axes define the rotation axis.
For breakable structures, add constraints with a check in the postStep event: if the constraint force exceeds a threshold, remove the constraint. Cannon-es does not have a built-in breakable constraint, but you can implement one by listening to the postStep event, reading constraint equation multipliers, and calling world.removeConstraint() when they exceed your limit.
Springs are useful for suspension systems, bouncy platforms, and soft connections. Create one with: const spring = new CANNON.Spring(bodyA, bodyB, { restLength: 2, stiffness: 100, damping: 5 }). Springs are not added to the world directly. Instead, call spring.applyForce() inside the world's preStep event listener. This design gives you full control over when and whether the spring is active.
Step the World and Copy Transforms
In your render loop, call world.step(fixedTimeStep, deltaTime, maxSubSteps). The fixedTimeStep is typically 1/60. The deltaTime is the real elapsed time since the last frame (from your clock or performance.now delta). The maxSubSteps caps how many physics steps run per frame to prevent spiral-of-death slowdowns, usually set to 3 or 5.
After stepping, synchronize each physics body with its visual counterpart. For Three.js: mesh.position.copy(body.position) and mesh.quaternion.copy(body.quaternion). The CANNON.Vec3 and CANNON.Quaternion classes have the same x, y, z (and w) properties as Three.js vectors and quaternions, so .copy() works directly without conversion.
Keep a mapping between physics bodies and visual meshes. A simple approach is an array of objects: [{ body: cannonBody, mesh: threeMesh }, ...]. Loop through it after each step. For larger games, consider a component-based entity system where each entity has a physics component (the body) and a render component (the mesh), and a physics system runs the step and sync.
Use Events and Raycasting for Gameplay
Cannon-es bodies emit collide events when they contact another body. Listen with: body.addEventListener('collide', (event) => { ... }). The event includes the contact object, the other body, and the contact normal and impact velocity. Use this for damage on impact, sound effects on collision, or triggering game events when the player touches a sensor.
Raycasting finds the first body intersected by a line segment. Create a Ray with: const ray = new CANNON.Ray(origin, direction). Call world.rayTest(from, to, result) where result is a CANNON.RaycastResult. After the test, result.hasHit tells you if something was hit, result.hitPointWorld gives the impact position, and result.body gives the body that was hit. Use this for ground checks (cast downward from the player), shooting (cast from the camera through the crosshair), and mouse picking (cast from the camera through the mouse position).
Sleep management improves performance in scenes with many resting objects. With world.allowSleep = true, bodies that have been nearly stationary for a few frames go to sleep automatically. They wake up when another body collides with them or when you call body.wakeUp(). You can tune the sleep threshold with body.sleepSpeedLimit (velocity below which the body is considered stationary) and body.sleepTimeLimit (seconds the body must be stationary before sleeping).
Cannon-es offers the simplest path to 3D physics in a browser game. No WASM, no async init, just import and go. It pairs naturally with Three.js, supports all the common rigid body features, and performs well for games with a few hundred active bodies. For larger simulations, consider Rapier or Havok.