Skip to content

Game physics for web games

Physics simulation brings games to life—falling objects, bouncing balls, ragdolls, vehicles, and destructible environments. This guide covers the major physics libraries available for web games, with real code examples and honest pros/cons.

At a glance

EngineDimensionPerformanceSizeDifficultySoft BodiesVehiclesCCDDeterminism
Rapier2D/3D⭐⭐⭐⭐⭐1.4 MBMedium
Cannon-es3D⭐⭐⭐150 KBEasy
Ammo.js3D⭐⭐⭐⭐⭐1-2 MBHard⚠️
Oimo.js3D⭐⭐⭐100 KBEasy
Matter.js2D⭐⭐⭐80 KBEasy⚠️
Planck.js2D⭐⭐⭐⭐120 KBMedium⚠️
p2-es2D⭐⭐⭐100 KBMedium
Box2D WASM2D⭐⭐⭐⭐⭐300 KBHard

Legend:

  • CCD = Continuous Collision Detection (prevents fast objects tunneling through walls)
  • ⚠️ Determinism = Possible with fixed timestep, but not guaranteed cross-platform

Quick recommendations

Your situationBest choice
Production 3D gameRapier — best performance, modern API, active development
Learning / prototypeCannon-es (3D) or Matter.js (2D) — simple APIs, easy debugging
Vehicle physicsRapier or Ammo.js — both have ray-cast vehicle controllers
Soft bodies, cloth, ropesAmmo.js — only option with these features
Precise 2D platformerPlanck.js — Box2D algorithms, deterministic with fixed timestep
Maximum 2D performanceBox2D WASM — native speed in browser
Smallest bundle sizeOimo.js (3D) or Matter.js (2D)

Detailed comparison

3D Physics Engines

EngineLanguageSizePerformanceBest for
RapierRust/WASM~1.4 MBExcellentProduction games, complex simulations
Cannon-esJavaScript~150 KBGoodPrototypes, simple games
Ammo.jsC++/WASM~1-2 MBExcellentAAA features, soft bodies, vehicles
Oimo.jsJavaScript~100 KBGoodSimple games, quick prototypes

2D Physics Engines

EngineLanguageSizePerformanceBest for
Matter.jsJavaScript~80 KBGoodVisual games, prototypes
Planck.jsJavaScript~120 KBGoodPlatformers, precise physics
p2-esJavaScript~100 KBGoodConstraints, mechanisms
Box2D WASMC++/WASM~300 KBExcellentHigh body counts

3D Physics Engines

Rapier — The modern choice

Rapier is a Rust physics engine with JavaScript/WASM bindings. It's the most performant option for web games in 2025-2026, with 2-5x speed improvements over its 2024 version.

Install:

bash
npm install @dimforge/rapier3d
# Or with SIMD (faster, requires modern browsers):
npm install @dimforge/rapier3d-simd

Basic setup:

js
import RAPIER from '@dimforge/rapier3d'

// Initialize (async required for WASM)
await RAPIER.init()

// Create world with gravity
const gravity = { x: 0, y: -9.81, z: 0 }
const world = new RAPIER.World(gravity)

// Create ground (static body)
const groundDesc = RAPIER.RigidBodyDesc.fixed()
const groundBody = world.createRigidBody(groundDesc)
const groundCollider = RAPIER.ColliderDesc.cuboid(50, 0.1, 50)
world.createCollider(groundCollider, groundBody)

// Create falling box (dynamic body)
const boxDesc = RAPIER.RigidBodyDesc.dynamic()
  .setTranslation(0, 10, 0)
const boxBody = world.createRigidBody(boxDesc)
const boxCollider = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5)
  .setDensity(1.0)
  .setRestitution(0.5)
world.createCollider(boxCollider, boxBody)

Game loop integration:

js
const FIXED_TIMESTEP = 1 / 60

function physicsStep() {
  world.step()
}

function gameLoop() {
  physicsStep()
  
  // Sync render objects with physics
  const position = boxBody.translation()
  const rotation = boxBody.rotation()
  
  // Update your Three.js/Babylon mesh
  mesh.position.set(position.x, position.y, position.z)
  mesh.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w)
  
  requestAnimationFrame(gameLoop)
}

Collision detection:

js
// Event-based collision detection
world.contactPairsWith(boxCollider, (otherCollider) => {
  console.log('Box is touching:', otherCollider)
})

// Ray casting
const ray = new RAPIER.Ray({ x: 0, y: 10, z: 0 }, { x: 0, y: -1, z: 0 })
const hit = world.castRay(ray, 100, true)
if (hit) {
  const hitPoint = ray.pointAt(hit.timeOfImpact)
  console.log('Hit at:', hitPoint)
}

Joints:

js
// Create a hinge joint (door)
const jointData = RAPIER.JointData.revolute(
  { x: 0, y: 0, z: 0 },  // Anchor on body1
  { x: -1, y: 0, z: 0 }, // Anchor on body2
  { x: 0, y: 1, z: 0 }   // Rotation axis
)
world.createImpulseJoint(jointData, body1, body2, true)

Vehicle controller:

js
// Create chassis rigid body
const chassisDesc = RAPIER.RigidBodyDesc.dynamic()
  .setTranslation(0, 2, 0)
const chassis = world.createRigidBody(chassisDesc)
const chassisCollider = RAPIER.ColliderDesc.cuboid(1, 0.5, 2)
  .setDensity(100)
world.createCollider(chassisCollider, chassis)

// Create vehicle controller
const vehicle = world.createVehicleController(chassis)

// Add wheels (front-left, front-right, rear-left, rear-right)
const suspensionRestLength = 0.3
const wheelRadius = 0.4

// Front wheels (steering)
vehicle.addWheel(
  { x: -0.8, y: 0, z: 1.2 },   // Connection point
  { x: 0, y: -1, z: 0 },       // Suspension direction
  { x: -1, y: 0, z: 0 },       // Axle direction
  suspensionRestLength,
  wheelRadius
)
vehicle.addWheel(
  { x: 0.8, y: 0, z: 1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)

// Rear wheels (drive)
vehicle.addWheel(
  { x: -0.8, y: 0, z: -1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)
vehicle.addWheel(
  { x: 0.8, y: 0, z: -1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)

// Configure suspension for all wheels
for (let i = 0; i < 4; i++) {
  vehicle.setWheelSuspensionStiffness(i, 30)
  vehicle.setWheelSuspensionCompression(i, 4.4)
  vehicle.setWheelSuspensionRelaxation(i, 2.3)
  vehicle.setWheelMaxSuspensionTravel(i, 0.5)
  vehicle.setWheelFrictionSlip(i, 2)
}

// In game loop
function updateVehicle(steering, engineForce, brakeForce) {
  // Steering (front wheels only)
  vehicle.setWheelSteering(0, steering)
  vehicle.setWheelSteering(1, steering)
  
  // Engine (rear wheels)
  vehicle.setWheelEngineForce(2, engineForce)
  vehicle.setWheelEngineForce(3, engineForce)
  
  // Brakes (all wheels)
  for (let i = 0; i < 4; i++) {
    vehicle.setWheelBrake(i, brakeForce)
  }
  
  // Update vehicle physics
  vehicle.updateVehicle(world.timestep)
}

Pros:

  • Best performance (WASM + SIMD)
  • Excellent collision detection accuracy
  • Cross-platform determinism (same inputs = same outputs)
  • Active development, modern API
  • Continuous collision detection (no tunneling)
  • Character controller and vehicle controller built-in

Cons:

  • Larger bundle size (~1.4 MB)
  • Async initialization required
  • More complex API than pure JS alternatives
  • WASM debugging can be tricky

Cannon-es — Simple and effective

Cannon-es is the maintained fork of Cannon.js. Pure JavaScript, easy to understand, great for learning and prototypes.

Install:

bash
npm install cannon-es

Basic setup:

js
import * as CANNON from 'cannon-es'

// Create world
const world = new CANNON.World({
  gravity: new CANNON.Vec3(0, -9.81, 0)
})

// Ground
const groundBody = new CANNON.Body({
  type: CANNON.Body.STATIC,
  shape: new CANNON.Plane()
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
world.addBody(groundBody)

// Falling sphere
const sphereBody = new CANNON.Body({
  mass: 1,
  shape: new CANNON.Sphere(0.5),
  position: new CANNON.Vec3(0, 10, 0)
})
sphereBody.linearDamping = 0.1
world.addBody(sphereBody)

Game loop:

js
const TIMESTEP = 1 / 60

function animate() {
  world.step(TIMESTEP)
  
  // Sync with Three.js
  mesh.position.copy(sphereBody.position)
  mesh.quaternion.copy(sphereBody.quaternion)
  
  requestAnimationFrame(animate)
}

Collision events:

js
sphereBody.addEventListener('collide', (event) => {
  const contact = event.contact
  const impactVelocity = contact.getImpactVelocityAlongNormal()
  
  if (Math.abs(impactVelocity) > 5) {
    console.log('Hard impact!')
  }
})

Constraints:

js
// Distance constraint (rope-like)
const constraint = new CANNON.DistanceConstraint(
  bodyA, bodyB, 
  2 // distance
)
world.addConstraint(constraint)

// Hinge constraint
const hinge = new CANNON.HingeConstraint(bodyA, bodyB, {
  pivotA: new CANNON.Vec3(1, 0, 0),
  axisA: new CANNON.Vec3(0, 1, 0),
  pivotB: new CANNON.Vec3(-1, 0, 0),
  axisB: new CANNON.Vec3(0, 1, 0)
})
world.addConstraint(hinge)

Pros:

  • Pure JavaScript, no WASM complexity
  • Small bundle size (~150 KB)
  • Easy to learn and debug
  • Works everywhere
  • Great Three.js integration
  • Good documentation

Cons:

  • Slower than WASM alternatives
  • Struggles with many bodies (>100)
  • Limited trimesh support
  • No built-in CCD (tunneling possible)
  • Less active development

Ammo.js — Full Bullet Physics power

Ammo.js is the Bullet Physics engine compiled to WebAssembly. Maximum features, including soft bodies, vehicles, and advanced constraints.

Install:

bash
npm install ammo.js
# Or use from CDN

Basic setup:

js
import Ammo from 'ammo.js'

let physicsWorld

async function initPhysics() {
  await Ammo()
  
  const collisionConfig = new Ammo.btDefaultCollisionConfiguration()
  const dispatcher = new Ammo.btCollisionDispatcher(collisionConfig)
  const broadphase = new Ammo.btDbvtBroadphase()
  const solver = new Ammo.btSequentialImpulseConstraintSolver()
  
  physicsWorld = new Ammo.btDiscreteDynamicsWorld(
    dispatcher, broadphase, solver, collisionConfig
  )
  physicsWorld.setGravity(new Ammo.btVector3(0, -9.81, 0))
}

function createBox(mass, width, height, depth, x, y, z) {
  const transform = new Ammo.btTransform()
  transform.setIdentity()
  transform.setOrigin(new Ammo.btVector3(x, y, z))
  
  const motionState = new Ammo.btDefaultMotionState(transform)
  const shape = new Ammo.btBoxShape(
    new Ammo.btVector3(width / 2, height / 2, depth / 2)
  )
  
  const localInertia = new Ammo.btVector3(0, 0, 0)
  if (mass > 0) {
    shape.calculateLocalInertia(mass, localInertia)
  }
  
  const rbInfo = new Ammo.btRigidBodyConstructionInfo(
    mass, motionState, shape, localInertia
  )
  const body = new Ammo.btRigidBody(rbInfo)
  
  physicsWorld.addRigidBody(body)
  return body
}

Soft bodies (cloth):

js
function createCloth(width, height, segments) {
  const softBodyHelpers = new Ammo.btSoftBodyHelpers()
  
  const corner00 = new Ammo.btVector3(-width/2, height, 0)
  const corner10 = new Ammo.btVector3(width/2, height, 0)
  const corner01 = new Ammo.btVector3(-width/2, 0, 0)
  const corner11 = new Ammo.btVector3(width/2, 0, 0)
  
  const softBody = softBodyHelpers.CreatePatch(
    physicsWorld.getWorldInfo(),
    corner00, corner10, corner01, corner11,
    segments, segments,
    0, true
  )
  
  const sbConfig = softBody.get_m_cfg()
  sbConfig.set_viterations(10)
  sbConfig.set_piterations(10)
  
  softBody.setTotalMass(0.9, false)
  Ammo.castObject(softBody, Ammo.btCollisionObject)
    .getCollisionShape().setMargin(0.05)
  
  physicsWorld.addSoftBody(softBody, 1, -1)
  return softBody
}

Vehicle physics:

js
function createVehicle(chassisBody) {
  const tuning = new Ammo.btVehicleTuning()
  const rayCaster = new Ammo.btDefaultVehicleRaycaster(physicsWorld)
  const vehicle = new Ammo.btRaycastVehicle(tuning, chassisBody, rayCaster)
  
  vehicle.setCoordinateSystem(0, 1, 2)
  physicsWorld.addAction(vehicle)
  
  // Add wheels
  const wheelRadius = 0.4
  const wheelWidth = 0.3
  const suspensionRestLength = 0.3
  
  const wheelDirectionCS = new Ammo.btVector3(0, -1, 0)
  const wheelAxleCS = new Ammo.btVector3(-1, 0, 0)
  
  function addWheel(isFront, pos) {
    const wheelInfo = vehicle.addWheel(
      pos, wheelDirectionCS, wheelAxleCS,
      suspensionRestLength, wheelRadius, tuning, isFront
    )
    wheelInfo.set_m_suspensionStiffness(20)
    wheelInfo.set_m_wheelsDampingRelaxation(2.3)
    wheelInfo.set_m_wheelsDampingCompression(4.4)
    wheelInfo.set_m_frictionSlip(1000)
    wheelInfo.set_m_rollInfluence(0.1)
  }
  
  addWheel(true, new Ammo.btVector3(1, 0, 1.5))   // Front left
  addWheel(true, new Ammo.btVector3(-1, 0, 1.5))  // Front right
  addWheel(false, new Ammo.btVector3(1, 0, -1.5)) // Rear left
  addWheel(false, new Ammo.btVector3(-1, 0, -1.5))// Rear right
  
  return vehicle
}

Pros:

  • Most complete feature set
  • Soft bodies, cloth, ropes
  • Advanced vehicle physics
  • Battle-tested (used in many AAA games)
  • Highly configurable

Cons:

  • Complex, verbose API
  • Large bundle size
  • Memory management required (destroy objects)
  • Steep learning curve
  • Documentation scattered
  • Not cross-platform deterministic (same-device only, with careful config)

Oimo.js — Lightweight and fast

Oimo.js is a lightweight 3D physics engine. Great for simple games where you don't need advanced features.

Install:

bash
npm install oimo

Basic setup:

js
import * as OIMO from 'oimo'

const world = new OIMO.World({
  timestep: 1/60,
  iterations: 8,
  broadphase: 2, // 1: brute, 2: sweep & prune, 3: volume tree
  worldscale: 1,
  random: true,
  gravity: [0, -9.8, 0]
})

// Create ground
world.add({
  type: 'box',
  size: [100, 1, 100],
  pos: [0, -0.5, 0],
  move: false
})

// Create falling sphere
const sphere = world.add({
  type: 'sphere',
  size: [1],
  pos: [0, 10, 0],
  move: true,
  density: 1,
  friction: 0.4,
  restitution: 0.2
})

Game loop:

js
function animate() {
  world.step()
  
  // Get position/rotation
  const pos = sphere.getPosition()
  const rot = sphere.getQuaternion()
  
  mesh.position.set(pos.x, pos.y, pos.z)
  mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w)
  
  requestAnimationFrame(animate)
}

Pros:

  • Very small (~100 KB)
  • Simple API
  • Good performance for basic scenes
  • Built-in Babylon.js support

Cons:

  • Limited shapes (primitives only)
  • No soft bodies
  • Sparse documentation
  • Less active development
  • Limited joint options

2D Physics Engines

Matter.js — Beautiful and intuitive

Matter.js is a feature-rich 2D physics engine with excellent rendering and debug tools.

Install:

bash
npm install matter-js

Basic setup:

js
import Matter from 'matter-js'

const { Engine, Render, World, Bodies, Runner } = Matter

// Create engine
const engine = Engine.create()

// Create renderer (optional, great for debugging)
const render = Render.create({
  element: document.body,
  engine: engine,
  options: {
    width: 800,
    height: 600,
    wireframes: false
  }
})

// Create bodies
const ground = Bodies.rectangle(400, 580, 810, 60, { 
  isStatic: true 
})

const box = Bodies.rectangle(400, 200, 80, 80, {
  restitution: 0.8,
  friction: 0.5
})

const circle = Bodies.circle(300, 100, 40, {
  restitution: 0.9
})

// Add to world
World.add(engine.world, [ground, box, circle])

// Run
Render.run(render)
Runner.run(Runner.create(), engine)

Collision events:

js
Matter.Events.on(engine, 'collisionStart', (event) => {
  event.pairs.forEach(pair => {
    console.log('Collision between:', pair.bodyA.label, pair.bodyB.label)
  })
})

Constraints:

js
// Pin constraint
const pin = Matter.Constraint.create({
  pointA: { x: 400, y: 100 },
  bodyB: box,
  stiffness: 0.9
})

// Spring between bodies
const spring = Matter.Constraint.create({
  bodyA: box,
  bodyB: circle,
  stiffness: 0.01,
  length: 100
})

World.add(engine.world, [pin, spring])

Mouse interaction:

js
const mouse = Matter.Mouse.create(render.canvas)
const mouseConstraint = Matter.MouseConstraint.create(engine, {
  mouse: mouse,
  constraint: {
    stiffness: 0.2,
    render: { visible: false }
  }
})
World.add(engine.world, mouseConstraint)

Pros:

  • Beautiful default rendering
  • Great for learning/prototyping
  • Intuitive API
  • Good documentation
  • Active community
  • Deterministic with fixed timestep (isFixed: true)

Cons:

  • No CCD (fast objects can tunnel — use substepping to mitigate)
  • Performance issues with many bodies
  • No WASM option
  • Limited precision for complex simulations

Planck.js — Box2D for JavaScript

Planck.js is a complete rewrite of Box2D in JavaScript. Battle-tested physics, deterministic when using fixed timestep.

Install:

bash
npm install planck

Basic setup:

js
import { World, Vec2, Box, Circle, Edge } from 'planck'

// Create world
const world = new World({
  gravity: Vec2(0, -10)
})

// Create ground
const ground = world.createBody()
ground.createFixture({
  shape: Edge(Vec2(-40, 0), Vec2(40, 0))
})

// Create dynamic box
const box = world.createBody({
  type: 'dynamic',
  position: Vec2(0, 10)
})
box.createFixture({
  shape: Box(1, 1),
  density: 1,
  friction: 0.3,
  restitution: 0.5
})

Fixed timestep game loop:

js
const TIMESTEP = 1 / 60
const VELOCITY_ITERATIONS = 8
const POSITION_ITERATIONS = 3

function gameLoop() {
  world.step(TIMESTEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS)
  
  // Iterate all bodies
  for (let body = world.getBodyList(); body; body = body.getNext()) {
    const pos = body.getPosition()
    const angle = body.getAngle()
    // Update your sprites...
  }
  
  requestAnimationFrame(gameLoop)
}

Collision callbacks:

js
world.on('begin-contact', (contact) => {
  const fixtureA = contact.getFixtureA()
  const fixtureB = contact.getFixtureB()
  console.log('Contact started')
})

world.on('end-contact', (contact) => {
  console.log('Contact ended')
})

world.on('pre-solve', (contact, oldManifold) => {
  // Can disable contact here
  // contact.setEnabled(false)
})

Joints:

js
import { RevoluteJoint, DistanceJoint, PrismaticJoint } from 'planck'

// Revolute joint (hinge)
const joint = world.createJoint(RevoluteJoint({
  bodyA: ground,
  bodyB: box,
  localAnchorA: Vec2(0, 5),
  localAnchorB: Vec2(-1, 0),
  enableMotor: true,
  maxMotorTorque: 1000,
  motorSpeed: 2
}))

// Distance joint (spring)
world.createJoint(DistanceJoint({
  bodyA: boxA,
  bodyB: boxB,
  localAnchorA: Vec2(0, 0),
  localAnchorB: Vec2(0, 0),
  length: 5,
  stiffness: 10,
  damping: 0.5
}))

Pros:

  • Deterministic with fixed timestep
  • Well-documented (Box2D docs apply)
  • Good performance
  • TypeScript support
  • Small bundle size

Cons:

  • No CCD (fast objects can tunnel through walls)
  • Steeper learning curve than Matter.js
  • No built-in renderer
  • Box2D quirks (unit scale matters)

p2-es — Flexible 2D physics

p2-es is the maintained fork of p2.js. Great for games needing complex constraints and mechanisms.

Install:

bash
npm install p2-es

Basic setup:

js
import * as p2 from 'p2-es'

const world = new p2.World({
  gravity: [0, -9.81]
})

// Ground
const groundBody = new p2.Body({
  mass: 0, // static
  position: [0, -1]
})
groundBody.addShape(new p2.Plane())
world.addBody(groundBody)

// Dynamic circle
const circleBody = new p2.Body({
  mass: 1,
  position: [0, 5]
})
circleBody.addShape(new p2.Circle({ radius: 0.5 }))
world.addBody(circleBody)

Advanced constraints:

js
// Gear constraint
const gear = new p2.GearConstraint(bodyA, bodyB, {
  ratio: 2 // bodyB rotates twice as fast
})
world.addConstraint(gear)

// Prismatic constraint (slider)
const prismatic = new p2.PrismaticConstraint(bodyA, bodyB, {
  localAnchorA: [0, 0],
  localAnchorB: [0, 0],
  localAxisA: [1, 0],
  disableRotationalLock: false
})
world.addConstraint(prismatic)

// Lock constraint (weld)
const lock = new p2.LockConstraint(bodyA, bodyB)
world.addConstraint(lock)

Contact materials:

js
const ice = new p2.Material()
const rubber = new p2.Material()

const iceRubber = new p2.ContactMaterial(ice, rubber, {
  friction: 0.1,
  restitution: 0.9
})
world.addContactMaterial(iceRubber)

// Apply to shapes
iceBody.shapes[0].material = ice
rubberBody.shapes[0].material = rubber

Pros:

  • Rich constraint system
  • Contact materials
  • Good for mechanisms/machines
  • Sleeping bodies (performance)
  • ES modules, tree-shakable

Cons:

  • No built-in renderer
  • Some shape pairs unsupported
  • Less active than alternatives
  • Documentation gaps

Performance tips

1. Use a fixed timestep

js
const TIMESTEP = 1 / 60
let accumulator = 0

function gameLoop(deltaTime) {
  accumulator += deltaTime
  
  while (accumulator >= TIMESTEP) {
    world.step(TIMESTEP)
    accumulator -= TIMESTEP
  }
  
  // Interpolate for smooth rendering
  const alpha = accumulator / TIMESTEP
  // lerp(previousState, currentState, alpha)
}

2. Sleep inactive bodies

Most engines support sleeping. Enable it:

js
// Cannon-es
world.allowSleep = true
body.allowSleep = true
body.sleepSpeedLimit = 0.1
body.sleepTimeLimit = 1

// Rapier
bodyDesc.setCanSleep(true)

3. Use simple collision shapes

js
// GOOD: Simple primitives
const sphere = new CANNON.Sphere(1)
const box = new CANNON.Box(new CANNON.Vec3(1, 1, 1))

// AVOID: Complex trimesh for dynamic bodies
const trimesh = new CANNON.Trimesh(vertices, indices) // Slow!

4. Reduce solver iterations (carefully)

js
// Cannon-es
world.solver.iterations = 5 // Default 10

// Rapier - configured at world creation

5. Run physics in a Web Worker

js
// main.js
const worker = new Worker('physics-worker.js')

worker.postMessage({ type: 'step', deltaTime: 1/60 })
worker.onmessage = (e) => {
  const { positions, rotations } = e.data
  // Update render objects
}

// physics-worker.js
importScripts('cannon-es.js')
const world = new CANNON.World()

onmessage = (e) => {
  if (e.data.type === 'step') {
    world.step(e.data.deltaTime)
    postMessage({
      positions: bodies.map(b => b.position.toArray()),
      rotations: bodies.map(b => b.quaternion.toArray())
    })
  }
}

When to use what

ScenarioRecommendedWhy
Production 3D gameRapierBest performance, modern API, vehicles
3D game prototypeCannon-esSimple, debuggable
Soft bodies, cloth, ropesAmmo.jsOnly option with these features
Simple 3D browser gameOimo.jsTiny, sufficient
2D game jamMatter.jsFast setup, built-in rendering
Precise platformerPlanck.jsFixed timestep determinism, well-documented
Complex 2D mechanismsp2-esRich constraint system
Performance-critical 2DBox2D WASMFastest 2D option, true CCD

Integration with renderers

Three.js + Rapier

js
import * as THREE from 'three'
import RAPIER from '@dimforge/rapier3d'

await RAPIER.init()

const scene = new THREE.Scene()
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 })

const bodies = new Map() // physics body -> mesh

function createPhysicsBox(x, y, z) {
  // Physics
  const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y, z)
  const body = world.createRigidBody(bodyDesc)
  const collider = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5)
  world.createCollider(collider, body)
  
  // Render
  const mesh = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({ color: 0x00ff00 })
  )
  scene.add(mesh)
  
  bodies.set(body, mesh)
  return body
}

function syncPhysics() {
  bodies.forEach((mesh, body) => {
    const pos = body.translation()
    const rot = body.rotation()
    mesh.position.set(pos.x, pos.y, pos.z)
    mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w)
  })
}

PixiJS + Matter.js

js
import * as PIXI from 'pixi.js'
import Matter from 'matter-js'

const app = new PIXI.Application({ width: 800, height: 600 })
document.body.appendChild(app.view)

const engine = Matter.Engine.create()
const bodies = new Map()

function createPhysicsSprite(texture, x, y, width, height) {
  // Physics
  const body = Matter.Bodies.rectangle(x, y, width, height)
  Matter.World.add(engine.world, body)
  
  // Render
  const sprite = new PIXI.Sprite(texture)
  sprite.anchor.set(0.5)
  app.stage.addChild(sprite)
  
  bodies.set(body, sprite)
  return body
}

app.ticker.add(() => {
  Matter.Engine.update(engine, 1000 / 60)
  
  bodies.forEach((sprite, body) => {
    sprite.position.set(body.position.x, body.position.y)
    sprite.rotation = body.angle
  })
})

Common gotchas

Scale matters

Physics engines work best with real-world scale (1 unit = 1 meter). Don't use pixel coordinates directly.

js
// BAD: Using pixel positions
const body = Bodies.circle(400, 300, 50) // 50 pixel radius?

// GOOD: Use a scale factor
const SCALE = 50 // 50 pixels per meter
const body = Bodies.circle(8, 6, 1) // 1 meter radius
// Then multiply by SCALE when rendering

Memory leaks with WASM engines

Always clean up bodies when removing them:

js
// Rapier
world.removeRigidBody(body)

// Ammo.js
physicsWorld.removeRigidBody(body)
Ammo.destroy(body)
Ammo.destroy(shape)
Ammo.destroy(motionState)

Tunneling (objects passing through each other)

Fast-moving objects can tunnel through thin walls. Solutions:

js
// Rapier: Enable CCD
const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setCcdEnabled(true)

// Cannon-es: Use smaller timesteps or thicker walls
world.step(1/120) // 120 Hz instead of 60