Three.js Physics: Cannon.js vs Rapier vs Ammo.js

Three.js has no built-in physics engine. You pair it with a separate library and manually sync each physics body's position and rotation onto its corresponding mesh every frame. Cannon.js (or its maintained fork cannon-es) is the simplest to set up, Rapier (WebAssembly, from the Rapier/Bevy ecosystem) is currently the best performance choice, and Ammo.js (a Bullet Physics port) is the most feature-complete but heaviest.

Last updated . Verified against three.js r181.

Read the FAQJump to code

Why Three.js has no built-in physics

Three.js is deliberately a rendering library, not a game engine. Physics, like input handling and audio, is left to specialized libraries you compose yourself. This means more setup than an all-in-one engine, but you get to pick the physics library that fits your project's needs (2D vs 3D, performance profile, feature set) rather than being locked into whatever ships in the box.

Cannon.js vs Rapier vs Ammo.js

  • Cannon.js / cannon-es: pure JavaScript, small, easy to reason about, good documentation and a large body of Three.js integration examples. The most common starting point for prototypes and games that don't need extreme physics performance. The original cannon.js is unmaintained; use the community fork cannon-es instead.
  • Rapier: compiled to WebAssembly, built by the team behind the Bevy game engine's physics. Significantly faster than pure-JS solutions for scenes with many active bodies, with a modern API. The current best choice when physics performance actually matters (many objects, continuous collision detection).
  • Ammo.js: a WebAssembly port of Bullet Physics (the engine used in many native games and Blender). The most feature-complete of the three (soft bodies, vehicle physics, constraints), but has the heaviest API and largest bundle size. Reach for it specifically when you need a feature the other two don't have.

For React Three Fiber projects specifically, @react-three/rapier and @react-three/cannon wrap these with hooks that remove most of the manual sync boilerplate described below.

The core pattern: syncing bodies to meshes

Regardless of which library you choose, the integration pattern is the same: for every mesh that should be physically simulated, create a matching physics body (with its own shape, mass and material), step the physics world forward each frame, then copy each body's resulting position and rotation onto its mesh.

This coupling is manual because Three.js's scene graph and the physics engine's world are two independent systems that know nothing about each other. The sync loop is the only thing connecting them, so a body created without a matching mesh update (or vice versa) is the most common physics bug: objects "falling through the floor" usually means the ground's physics body and its visual mesh drifted out of sync, not that gravity is broken.

Tools and libraries

See it in production

Browse the full showcase →

Code

Cannon-es sync loop (r181)
1import * as CANNON from 'cannon-es'
2
3const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) })
4
5const groundBody = new CANNON.Body({ mass: 0, shape: new CANNON.Plane() })
6groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
7world.addBody(groundBody)
8
9const boxBody = new CANNON.Body({ mass: 1, shape: new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)) })
10boxBody.position.set(0, 5, 0)
11world.addBody(boxBody)
12
13const boxMesh = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshStandardMaterial())
14scene.add(boxMesh)
15
16function animate() {
17 world.step(1 / 60)
18 boxMesh.position.copy(boxBody.position)
19 boxMesh.quaternion.copy(boxBody.quaternion)
20 renderer.render(scene, camera)
21}
Rapier sync loop (WASM, r181)
1import RAPIER from '@dimforge/rapier3d-compat'
2await RAPIER.init()
3
4const world = new RAPIER.World({ x: 0, y: -9.82, z: 0 })
5
6const groundBody = world.createRigidBody(RAPIER.RigidBodyDesc.fixed())
7world.createCollider(RAPIER.ColliderDesc.cuboid(10, 0.1, 10), groundBody)
8
9const boxBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 5, 0))
10world.createCollider(RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5), boxBody)
11
12function animate() {
13 world.step()
14 const t = boxBody.translation()
15 boxMesh.position.set(t.x, t.y, t.z)
16 const r = boxBody.rotation()
17 boxMesh.quaternion.set(r.x, r.y, r.z, r.w)
18 renderer.render(scene, camera)
19}

Learn this properly

Learn Practical TSL

Your First Node Material

Have a mesh and material ready before wiring it up to a physics body.

Start the lesson (6 minutes)

Frequently asked questions

Does Three.js have built-in physics?

No. Three.js is a rendering library only. You add a separate physics engine (Cannon.js/cannon-es, Rapier or Ammo.js are the common choices) and manually sync each physics body's position and rotation onto its mesh every frame.

Which physics engine is fastest for Three.js?

Rapier, a WebAssembly physics engine, generally outperforms pure-JavaScript Cannon.js for scenes with many active bodies. Ammo.js (also WASM) is more feature-complete but has a heavier API and larger bundle.

How do I use physics with React Three Fiber?

@react-three/rapier and @react-three/cannon wrap Rapier and Cannon.js respectively with React hooks and components, removing most of the manual body-to-mesh sync code you'd otherwise write by hand.

Keep reading