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
| Engine | Dimension | Performance | Size | Difficulty | Soft Bodies | Vehicles | CCD | Determinism |
|---|---|---|---|---|---|---|---|---|
| Rapier | 2D/3D | ⭐⭐⭐⭐⭐ | 1.4 MB | Medium | — | ✅ | ✅ | ✅ |
| Cannon-es | 3D | ⭐⭐⭐ | 150 KB | Easy | — | — | — | — |
| Ammo.js | 3D | ⭐⭐⭐⭐⭐ | 1-2 MB | Hard | ✅ | ✅ | ✅ | ⚠️ |
| Oimo.js | 3D | ⭐⭐⭐ | 100 KB | Easy | — | — | — | — |
| Matter.js | 2D | ⭐⭐⭐ | 80 KB | Easy | — | — | — | ⚠️ |
| Planck.js | 2D | ⭐⭐⭐⭐ | 120 KB | Medium | — | — | — | ⚠️ |
| p2-es | 2D | ⭐⭐⭐ | 100 KB | Medium | — | — | — | — |
| Box2D WASM | 2D | ⭐⭐⭐⭐⭐ | 300 KB | Hard | — | — | ✅ | ✅ |
Legend:
- CCD = Continuous Collision Detection (prevents fast objects tunneling through walls)
- ⚠️ Determinism = Possible with fixed timestep, but not guaranteed cross-platform
Quick recommendations
| Your situation | Best choice |
|---|---|
| Production 3D game | Rapier — best performance, modern API, active development |
| Learning / prototype | Cannon-es (3D) or Matter.js (2D) — simple APIs, easy debugging |
| Vehicle physics | Rapier or Ammo.js — both have ray-cast vehicle controllers |
| Soft bodies, cloth, ropes | Ammo.js — only option with these features |
| Precise 2D platformer | Planck.js — Box2D algorithms, deterministic with fixed timestep |
| Maximum 2D performance | Box2D WASM — native speed in browser |
| Smallest bundle size | Oimo.js (3D) or Matter.js (2D) |
Detailed comparison
3D Physics Engines
| Engine | Language | Size | Performance | Best for |
|---|---|---|---|---|
| Rapier | Rust/WASM | ~1.4 MB | Excellent | Production games, complex simulations |
| Cannon-es | JavaScript | ~150 KB | Good | Prototypes, simple games |
| Ammo.js | C++/WASM | ~1-2 MB | Excellent | AAA features, soft bodies, vehicles |
| Oimo.js | JavaScript | ~100 KB | Good | Simple games, quick prototypes |
2D Physics Engines
| Engine | Language | Size | Performance | Best for |
|---|---|---|---|---|
| Matter.js | JavaScript | ~80 KB | Good | Visual games, prototypes |
| Planck.js | JavaScript | ~120 KB | Good | Platformers, precise physics |
| p2-es | JavaScript | ~100 KB | Good | Constraints, mechanisms |
| Box2D WASM | C++/WASM | ~300 KB | Excellent | High 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:
npm install @dimforge/rapier3d
# Or with SIMD (faster, requires modern browsers):
npm install @dimforge/rapier3d-simdBasic setup:
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:
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:
// 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:
// 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:
// 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:
npm install cannon-esBasic setup:
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:
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:
sphereBody.addEventListener('collide', (event) => {
const contact = event.contact
const impactVelocity = contact.getImpactVelocityAlongNormal()
if (Math.abs(impactVelocity) > 5) {
console.log('Hard impact!')
}
})Constraints:
// 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:
npm install ammo.js
# Or use from CDNBasic setup:
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):
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:
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:
npm install oimoBasic setup:
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:
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:
npm install matter-jsBasic setup:
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:
Matter.Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach(pair => {
console.log('Collision between:', pair.bodyA.label, pair.bodyB.label)
})
})Constraints:
// 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:
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:
npm install planckBasic setup:
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:
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:
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:
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:
npm install p2-esBasic setup:
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:
// 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:
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 = rubberPros:
- 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
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:
// Cannon-es
world.allowSleep = true
body.allowSleep = true
body.sleepSpeedLimit = 0.1
body.sleepTimeLimit = 1
// Rapier
bodyDesc.setCanSleep(true)3. Use simple collision shapes
// 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)
// Cannon-es
world.solver.iterations = 5 // Default 10
// Rapier - configured at world creation5. Run physics in a Web Worker
// 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
| Scenario | Recommended | Why |
|---|---|---|
| Production 3D game | Rapier | Best performance, modern API, vehicles |
| 3D game prototype | Cannon-es | Simple, debuggable |
| Soft bodies, cloth, ropes | Ammo.js | Only option with these features |
| Simple 3D browser game | Oimo.js | Tiny, sufficient |
| 2D game jam | Matter.js | Fast setup, built-in rendering |
| Precise platformer | Planck.js | Fixed timestep determinism, well-documented |
| Complex 2D mechanisms | p2-es | Rich constraint system |
| Performance-critical 2D | Box2D WASM | Fastest 2D option, true CCD |
Integration with renderers
Three.js + Rapier
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
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.
// 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 renderingMemory leaks with WASM engines
Always clean up bodies when removing them:
// 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:
// 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