JavaScript Physics Engines: The Complete Guide to Matter.js, Cannon.js, Rapier, and Interactive Web Physics
Executive Summary
Physics engines transform static web experiences into dynamic, interactive environments where objects respond realistically to gravity, collisions, forces, and constraints. From simple bouncing balls to complex ragdoll simulations, from product configurators to browser-based games, JavaScript physics engines enable developers to create immersive experiences that feel tangible and responsive. Three engines dominate the modern landscape: Matter.js for 2D physics with elegant APIs and rich ecosystem, Cannon.js for lightweight 3D physics integrated with Three.js, and Rapier for high-performance physics compiled from Rust to WebAssembly.
Matter.js has established itself as the gold standard for 2D web physics since its 2014 release, offering an intuitive API that makes physics accessible to developers without extensive mathematics backgrounds. The library handles rigid body dynamics, collision detection, constraint systems, and composite bodies through a clean, well-documented interface. Matter.js powers everything from creative portfolio animations to educational physics simulations to mobile games, with performance characteristics that enable hundreds of active bodies at 60 FPS on modern devices. The ecosystem includes visual debugging tools, physics editors, and extensive community examples that accelerate development from concept to deployment.
Cannon.js brings 3D physics to web applications with a focus on Three.js integration, enabling developers to add realistic motion and interactions to 3D scenes. Originally created by Stefan Hedman in 2012 and actively maintained through community forks like cannon-es, the library provides rigid body dynamics, collision detection across primitive and complex geometries, constraint systems for joints and motors, and raycast capabilities for interaction and AI. While more complex than 2D alternatives due to three-dimensional mathematics, Cannon.js abstracts the computational physics behind approachable APIs that developers can master through practical examples and patterns.
Rapier represents the cutting edge of web physics through its Rust-compiled WebAssembly foundation, delivering performance that often exceeds native JavaScript implementations by 3-10x. Released in 2021, Rapier supports both 2D and 3D physics through a unified API, handles thousands of active bodies simultaneously, and provides advanced features like continuous collision detection (CCD) for fast-moving objects, kinematic character controllers, and multibody joints. The WASM architecture enables desktop-class physics simulation in browsers, opening possibilities previously limited to native applications: real-time multiplayer physics, procedural physics-based level generation, and complex mechanical simulations.
The strategic choice between these engines depends on project requirements:
Choose Matter.js for 2D projects prioritizing developer experience, rapid prototyping, and ecosystem maturity. The gentle learning curve and comprehensive documentation enable teams to implement physics features quickly without specialized knowledge. Portfolio sites with interactive elements, educational platforms demonstrating physics concepts, and 2D browser games benefit from Matter.js's stability and community support.
Choose Cannon.js for 3D applications built with Three.js where physics adds realism to existing 3D scenes. Product configurators with object manipulation, architectural walkthroughs with interactive elements, and browser-based 3D games leverage Cannon.js's established Three.js integration patterns. The library suits projects where physics enhances experience rather than defining it—complementing visual presentation with realistic motion.
Choose Rapier for performance-critical applications requiring maximum physics fidelity or handling large numbers of simultaneous bodies. Multiplayer physics simulations, procedural content generation, vehicle simulators, and complex mechanical systems benefit from Rapier's computational efficiency. The performance headroom enables features impossible with JavaScript implementations while maintaining cross-platform web deployment.
This comprehensive guide explores practical implementation patterns, performance optimization strategies, and real-world use cases across all three engines. Whether creating subtle animations for landing pages, building interactive educational tools, or developing full-featured browser games, the techniques and code examples below provide the foundation for effective physics-driven web development.
Understanding Web Physics Fundamentals
The Physics Simulation Loop
Physics engines operate through iterative simulation loops that approximate continuous physical motion through discrete time steps. Understanding this fundamental pattern clarifies how engines work and informs optimization decisions.
Basic Simulation Cycle:
- 1. Apply Forces: External forces (gravity, user input, wind) apply to bodiesApply Forces: External forces (gravity, user input, wind) apply to bodies
- 2. Update Velocities: Forces modify velocities based on mass (F = ma)Update Velocities: Forces modify velocities based on mass (F = ma)
- 3. Detect Collisions: Broad-phase and narrow-phase algorithms identify intersecting bodiesDetect Collisions: Broad-phase and narrow-phase algorithms identify intersecting bodies
- 4. Resolve Collisions: Collision responses generate impulses that modify velocitiesResolve Collisions: Collision responses generate impulses that modify velocities
- 5. Apply Constraints: Joints and constraints enforce relationships between bodiesApply Constraints: Joints and constraints enforce relationships between bodies
- 6. Integrate Positions: Velocities update positions for the time stepIntegrate Positions: Velocities update positions for the time step
- 7. Render: Visual representation syncs with physics stateRender: Visual representation syncs with physics state
This cycle executes at fixed intervals (typically 60 Hz for 16.67ms time steps) independent of rendering frame rate to ensure simulation stability and determinism.
Fixed Time Step vs. Variable Time Step:
Fixed time steps maintain simulation accuracy and determinism—critical for multiplayer games or recorded playback. Variable time steps tie physics updates to rendering frame rate, creating inconsistent behavior across devices with different performance characteristics.
// Fixed time step pattern (recommended)
const fixedTimeStep = 1000 / 60; // 60 FPS = 16.67ms per step
let accumulator = 0;
let lastTime = performance.now();
function update() {
const currentTime = performance.now();
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
accumulator += deltaTime;
// Execute fixed time steps
while (accumulator >= fixedTimeStep) {
physicsEngine.step(fixedTimeStep / 1000); // Convert ms to seconds
accumulator -= fixedTimeStep;
}
// Render with interpolation for smooth visuals
const interpolation = accumulator / fixedTimeStep;
render(interpolation);
requestAnimationFrame(update);
}
update();
This pattern maintains stable physics at 60 Hz while rendering as fast as possible, interpolating visual positions between physics states for smooth motion.
Collision Detection: Broad Phase and Narrow Phase
Collision detection represents the most computationally expensive physics operation. Naive approaches checking every body against every other body scale at O(n²)—with 100 bodies requiring 10,000 checks per frame. Production engines employ two-phase detection strategies.
Broad Phase: Quickly eliminate body pairs that definitely aren't colliding using spatial partitioning (grid, quadtree, bounding volume hierarchy) or sweep-and-prune algorithms. This reduces potential collisions from O(n²) to O(n log n) or better.
Narrow Phase: Precisely calculate collision points, normals, and penetration depths for body pairs identified by broad phase using geometry-specific algorithms (SAT for polygons, GJK for convex shapes).
Most engines handle this optimization transparently, but understanding the concept informs decisions about body counts and scene structure.
Matter.js: 2D Physics with Elegant APIs
Getting Started with Matter.js
Matter.js excels at making physics approachable through clean APIs that abstract mathematical complexity without sacrificing control.
Installation and Basic Setup:
npm install matter-js
import Matter from 'matter-js';
// Create engine
const engine = Matter.Engine.create();
const world = engine.world;
// Create renderer
const render = Matter.Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false, // Show styled bodies instead of wireframes
background: '#1a1a2e'
}
});
// Create ground
const ground = Matter.Bodies.rectangle(400, 580, 810, 60, {
isStatic: true,
render: { fillStyle: '#16213e' }
});
// Create falling boxes
const boxA = Matter.Bodies.rectangle(400, 200, 80, 80, {
restitution: 0.5, // Bounciness
render: { fillStyle: '#0f3460' }
});
const boxB = Matter.Bodies.rectangle(450, 50, 80, 80, {
restitution: 0.8,
render: { fillStyle: '#e94560' }
});
// Add all bodies to world
Matter.World.add(world, [ground, boxA, boxB]);
// Run the engine
Matter.Engine.run(engine);
// Run the renderer
Matter.Render.run(render);
This minimal example creates a physics world with gravity, ground, and falling boxes that bounce realistically—demonstrating Matter.js's approachable API.
Creating Interactive Physics Objects
Real applications require mouse interaction, dynamic body creation, and event handling:
// Add mouse control
const mouse = Matter.Mouse.create(render.canvas);
const mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: { visible: false }
}
});
Matter.World.add(world, mouseConstraint);
// Keep mouse in sync with rendering
render.mouse = mouse;
// Create variety of shapes
function createRandomBody(x, y) {
const shapeType = Math.random();
let body;
if (shapeType < 0.33) {
// Circle
const radius = 20 + Math.random() * 40;
body = Matter.Bodies.circle(x, y, radius, {
restitution: 0.6,
friction: 0.01,
render: {
fillStyle: hsl(${Math.random() * 360}, 70%, 50%)
}
});
} else if (shapeType < 0.66) {
// Rectangle
const width = 40 + Math.random() * 60;
const height = 40 + Math.random() * 60;
body = Matter.Bodies.rectangle(x, y, width, height, {
restitution: 0.4,
friction: 0.1,
render: {
fillStyle: hsl(${Math.random() * 360}, 70%, 50%)
}
});
} else {
// Polygon
const sides = Math.floor(Math.random() * 4) + 5; // 5-8 sides
const radius = 30 + Math.random() * 30;
body = Matter.Bodies.polygon(x, y, sides, radius, {
restitution: 0.5,
friction: 0.05,
render: {
fillStyle: hsl(${Math.random() * 360}, 70%, 50%)
}
});
}
return body;
}
// Click to spawn bodies
render.canvas.addEventListener('click', (event) => {
const rect = render.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const body = createRandomBody(x, y);
Matter.World.add(world, body);
});
// Collision events
Matter.Events.on(engine, 'collisionStart', (event) => {
const pairs = event.pairs;
pairs.forEach(pair => {
const { bodyA, bodyB } = pair;
// Flash bodies on collision
bodyA.render.fillStyle = '#ffffff';
bodyB.render.fillStyle = '#ffffff';
setTimeout(() => {
bodyA.render.fillStyle = bodyA.render.originalColor || '#0f3460';
bodyB.render.fillStyle = bodyB.render.originalColor || '#e94560';
}, 100);
// Play collision sound based on impact velocity
const impactVelocity = Matter.Vector.magnitude(
Matter.Vector.sub(bodyA.velocity, bodyB.velocity)
);
if (impactVelocity > 2) {
playCollisionSound(impactVelocity);
}
});
});
Building Composite Bodies and Constraints
Complex objects require multiple bodies connected through constraints:
// Ragdoll character
function createRagdoll(x, y, scale = 1) {
const headRadius = 25 * scale;
const limbWidth = 15 * scale;
const limbHeight = 40 * scale;
// Create body parts
const head = Matter.Bodies.circle(x, y, headRadius, {
density: 0.001,
render: { fillStyle: '#ffa07a' }
});
const torso = Matter.Bodies.rectangle(x, y + 50 * scale, 30 * scale, 50 * scale, {
density: 0.001,
render: { fillStyle: '#4a90e2' }
});
const leftArm = Matter.Bodies.rectangle(x - 30 * scale, y + 40 * scale, limbWidth, limbHeight, {
density: 0.001,
render: { fillStyle: '#ffa07a' }
});
const rightArm = Matter.Bodies.rectangle(x + 30 * scale, y + 40 * scale, limbWidth, limbHeight, {
density: 0.001,
render: { fillStyle: '#ffa07a' }
});
const leftLeg = Matter.Bodies.rectangle(x - 10 * scale, y + 95 * scale, limbWidth, limbHeight * 1.2, {
density: 0.001,
render: { fillStyle: '#4a90e2' }
});
const rightLeg = Matter.Bodies.rectangle(x + 10 * scale, y + 95 * scale, limbWidth, limbHeight * 1.2, {
density: 0.001,
render: { fillStyle: '#4a90e2' }
});
// Create constraints (joints)
const neckConstraint = Matter.Constraint.create({
bodyA: head,
bodyB: torso,
pointA: { x: 0, y: headRadius },
pointB: { x: 0, y: -25 * scale },
stiffness: 0.6,
length: 5 * scale
});
const leftShoulderConstraint = Matter.Constraint.create({
bodyA: torso,
bodyB: leftArm,
pointA: { x: -15 * scale, y: -20 * scale },
pointB: { x: 0, y: -limbHeight / 2 },
stiffness: 0.4
});
const rightShoulderConstraint = Matter.Constraint.create({
bodyA: torso,
bodyB: rightArm,
pointA: { x: 15 * scale, y: -20 * scale },
pointB: { x: 0, y: -limbHeight / 2 },
stiffness: 0.4
});
const leftHipConstraint = Matter.Constraint.create({
bodyA: torso,
bodyB: leftLeg,
pointA: { x: -10 * scale, y: 25 * scale },
pointB: { x: 0, y: -(limbHeight * 1.2) / 2 },
stiffness: 0.5
});
const rightHipConstraint = Matter.Constraint.create({
bodyA: torso,
bodyB: rightLeg,
pointA: { x: 10 * scale, y: 25 * scale },
pointB: { x: 0, y: -(limbHeight * 1.2) / 2 },
stiffness: 0.5
});
// Combine into composite
const ragdoll = Matter.Composite.create();
Matter.Composite.add(ragdoll, [
head, torso, leftArm, rightArm, leftLeg, rightLeg,
neckConstraint, leftShoulderConstraint, rightShoulderConstraint,
leftHipConstraint, rightHipConstraint
]);
return ragdoll;
}
// Add ragdoll to world
const ragdoll = createRagdoll(400, 100);
Matter.World.add(world, ragdoll);
// Apply force to ragdoll
Matter.Body.applyForce(
ragdoll.bodies[0], // Apply to head
ragdoll.bodies[0].position,
{ x: 0.05, y: -0.1 } // Force vector
);
Advanced Matter.js Techniques
Soft Bodies with Spring Networks:
function createSoftBody(x, y, columns, rows, columnGap, rowGap, crossBrace) {
const particleOptions = {
inertia: Infinity,
friction: 0.00001,
collisionFilter: { group: -1 }, // Particles don't collide with each other
render: { visible: true, radius: 5 }
};
const constraintOptions = {
stiffness: 0.06,
render: { type: 'line', anchors: false }
};
const softBody = Matter.Composites.softBody(
x, y, columns, rows, columnGap, rowGap,
crossBrace,
particleOptions,
constraintOptions
);
return softBody;
}
// Create cloth-like soft body
const cloth = createSoftBody(200, 100, 10, 10, 10, 10, true);
// Pin top corners to make it hang
const topLeftParticle = cloth.bodies[0];
const topRightParticle = cloth.bodies[9];
topLeftParticle.isStatic = true;
topRightParticle.isStatic = true;
Matter.World.add(world, cloth);
Chain and Rope Simulation:
function createChain(x, y, length, linkSize) {
const chain = Matter.Composites.stack(
x, y,
1, length, // 1 column, multiple rows
0, 0,
(x, y) => {
return Matter.Bodies.rectangle(x, y, linkSize, linkSize * 3, {
density: 0.005,
friction: 0.8,
render: { fillStyle: '#8b4513' }
});
}
);
// Connect links with constraints
Matter.Composites.chain(chain, 0.5, 0, -0.5, 0, {
stiffness: 0.9,
length: 2
});
// Pin top link
Matter.Composite.add(chain, Matter.Constraint.create({
bodyB: chain.bodies[0],
pointB: { x: 0, y: -linkSize * 1.5 },
pointA: { x: chain.bodies[0].position.x, y: chain.bodies[0].position.y - linkSize * 1.5 },
stiffness: 0.9
}));
return chain;
}
// Create swinging chain
const chain = createChain(400, 100, 15, 10);
Matter.World.add(world, chain);
// Attach object to bottom of chain
const ball = Matter.Bodies.circle(
chain.bodies[chain.bodies.length - 1].position.x,
chain.bodies[chain.bodies.length - 1].position.y + 50,
30,
{ density: 0.01, render: { fillStyle: '#ff6b6b' } }
);
Matter.Composite.add(chain, Matter.Constraint.create({
bodyA: chain.bodies[chain.bodies.length - 1],
bodyB: ball,
length: 30,
stiffness: 0.9
}));
Matter.World.add(world, ball);
Cannon.js: 3D Physics for Three.js
Setting Up Cannon.js with Three.js
Cannon.js integrates with Three.js to add physics to 3D scenes:
npm install three cannon-es
import * as THREE from 'three';
import * as CANNON from 'cannon-es';
// Three.js setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 15;
camera.position.y = 5;
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 10);
directionalLight.castShadow = true;
scene.add(directionalLight);
// Cannon.js physics world
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.82, 0) // m/s²
});
// Ground plane (physics)
const groundBody = new CANNON.Body({
type: CANNON.Body.STATIC,
shape: new CANNON.Plane()
});
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0); // Rotate to horizontal
world.addBody(groundBody);
// Ground plane (visual)
const groundGeometry = new THREE.PlaneGeometry(30, 30);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x808080 });
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.receiveShadow = true;
scene.add(groundMesh);
// Helper function to sync Three.js mesh with Cannon.js body
function createPhysicsBox(x, y, z, width, height, depth, mass) {
// Physics body
const shape = new CANNON.Box(new CANNON.Vec3(width / 2, height / 2, depth / 2));
const body = new CANNON.Body({ mass, shape });
body.position.set(x, y, z);
world.addBody(body);
// Visual mesh
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshStandardMaterial({
color: Math.random() * 0xffffff
});
const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
scene.add(mesh);
return { body, mesh };
}
// Create falling boxes
const boxes = [];
for (let i = 0; i < 10; i++) {
const box = createPhysicsBox(
(Math.random() - 0.5) * 10,
5 + i * 2,
(Math.random() - 0.5) * 10,
1, 1, 1,
1 // mass
);
boxes.push(box);
}
// Animation loop
const timeStep = 1 / 60; // 60 FPS
function animate() {
requestAnimationFrame(animate);
// Step physics simulation
world.step(timeStep);
// Sync visual meshes with physics bodies
boxes.forEach(({ body, mesh }) => {
mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);
});
renderer.render(scene, camera);
}
animate();
Interactive 3D Physics
Mouse Picking and Dragging:
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
// Add orbit controls
const controls = new OrbitControls(camera, renderer.domElement);
// Raycasting for mouse interaction
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedBody = null;
let jointConstraint = null;
renderer.domElement.addEventListener('mousedown', (event) => {
// Update mouse coordinates
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// Raycast to find clicked object
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
const clickedMesh = intersects[0].object;
// Find corresponding physics body
const physicsPair = boxes.find(({ mesh }) => mesh === clickedMesh);
if (physicsPair) {
selectedBody = physicsPair.body;
// Create joint at click point
const intersectPoint = intersects[0].point;
const jointBody = new CANNON.Body({ mass: 0 });
jointBody.position.copy(intersectPoint);
world.addBody(jointBody);
jointConstraint = new CANNON.PointToPointConstraint(
selectedBody,
new CANNON.Vec3(0, 0, 0),
jointBody,
new CANNON.Vec3(0, 0, 0)
);
world.addConstraint(jointConstraint);
controls.enabled = false; // Disable orbit during drag
}
}
});
renderer.domElement.addEventListener('mousemove', (event) => {
if (selectedBody && jointConstraint) {
// Update mouse position
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// Raycast to world space point
raycaster.setFromCamera(mouse, camera);
const intersectPoint = new THREE.Vector3();
raycaster.ray.at(10, intersectPoint); // Project ray to distance
// Move joint constraint
jointConstraint.bodyB.position.copy(intersectPoint);
}
});
renderer.domElement.addEventListener('mouseup', () => {
if (jointConstraint) {
world.removeConstraint(jointConstraint);
world.removeBody(jointConstraint.bodyB);
jointConstraint = null;
selectedBody = null;
controls.enabled = true;
}
});
Vehicle Physics with Cannon.js
function createVehicle(chassisX, chassisY, chassisZ) {
// Chassis
const chassisShape = new CANNON.Box(new CANNON.Vec3(2, 0.5, 4));
const chassisBody = new CANNON.Body({ mass: 150 });
chassisBody.addShape(chassisShape);
chassisBody.position.set(chassisX, chassisY, chassisZ);
// Chassis mesh
const chassisGeometry = new THREE.BoxGeometry(4, 1, 8);
const chassisMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const chassisMesh = new THREE.Mesh(chassisGeometry, chassisMaterial);
chassisMesh.castShadow = true;
scene.add(chassisMesh);
// Create vehicle
const vehicle = new CANNON.RigidVehicle({
chassisBody
});
// Wheel configuration
const wheelOptions = {
radius: 0.5,
directionLocal: new CANNON.Vec3(0, -1, 0),
suspensionStiffness: 30,
suspensionRestLength: 0.3,
frictionSlip: 1.4,
dampingRelaxation: 2.3,
dampingCompression: 4.4,
maxSuspensionForce: 100000,
rollInfluence: 0.01,
axleLocal: new CANNON.Vec3(-1, 0, 0),
chassisConnectionPointLocal: new CANNON.Vec3(-1, 0, 1),
maxSuspensionTravel: 0.3,
customSlidingRotationalSpeed: -30,
useCustomSlidingRotationalSpeed: true
};
// Add wheels
const wheelPositions = [
new CANNON.Vec3(-1, 0, 2), // Front left
new CANNON.Vec3(-1, 0, -2), // Rear left
new CANNON.Vec3(1, 0, 2), // Front right
new CANNON.Vec3(1, 0, -2) // Rear right
];
const wheelBodies = [];
const wheelMeshes = [];
wheelPositions.forEach((position) => {
const wheelBody = new CANNON.Body({
mass: 10,
shape: new CANNON.Sphere(wheelOptions.radius)
});
wheelBodies.push(wheelBody);
const wheelGeometry = new THREE.CylinderGeometry(
wheelOptions.radius,
wheelOptions.radius,
0.4,
32
);
wheelGeometry.rotateZ(Math.PI / 2);
const wheelMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 });
const wheelMesh = new THREE.Mesh(wheelGeometry, wheelMaterial);
wheelMesh.castShadow = true;
scene.add(wheelMesh);
wheelMeshes.push(wheelMesh);
vehicle.addWheel({
...wheelOptions,
chassisConnectionPointLocal: position
});
});
vehicle.addToWorld(world);
// Controls
const controls = {
forward: false,
backward: false,
left: false,
right: false
};
document.addEventListener('keydown', (event) => {
switch(event.key) {
case 'w': controls.forward = true; break;
case 's': controls.backward = true; break;
case 'a': controls.left = true; break;
case 'd': controls.right = true; break;
}
});
document.addEventListener('keyup', (event) => {
switch(event.key) {
case 'w': controls.forward = false; break;
case 's': controls.backward = false; break;
case 'a': controls.left = false; break;
case 'd': controls.right = false; break;
}
});
return {
vehicle,
chassisMesh,
wheelMeshes,
controls
};
}
// Create and drive vehicle
const car = createVehicle(0, 3, 0);
// In animation loop, apply vehicle forces
function updateVehicle() {
const maxSteerVal = 0.5;
const maxForce = 1000;
if (car.controls.forward) {
car.vehicle.setWheelForce(maxForce, 0);
car.vehicle.setWheelForce(maxForce, 1);
}
if (car.controls.backward) {
car.vehicle.setWheelForce(-maxForce / 2, 0);
car.vehicle.setWheelForce(-maxForce / 2, 1);
}
if (car.controls.left) {
car.vehicle.setSteeringValue(maxSteerVal, 0);
car.vehicle.setSteeringValue(maxSteerVal, 2);
}
if (car.controls.right) {
car.vehicle.setSteeringValue(-maxSteerVal, 0);
car.vehicle.setSteeringValue(-maxSteerVal, 2);
}
// Sync meshes
car.chassisMesh.position.copy(car.vehicle.chassisBody.position);
car.chassisMesh.quaternion.copy(car.vehicle.chassisBody.quaternion);
car.vehicle.wheelInfos.forEach((wheel, i) => {
car.vehicle.updateWheelTransform(i);
car.wheelMeshes[i].position.copy(wheel.worldTransform.position);
car.wheelMeshes[i].quaternion.copy(wheel.worldTransform.quaternion);
});
}
// Add to animation loop
function animate() {
requestAnimationFrame(animate);
world.step(timeStep);
updateVehicle();
renderer.render(scene, camera);
}
Rapier: High-Performance WASM Physics
Installing and Initializing Rapier
npm install @dimforge/rapier3d-compat
import('@dimforge/rapier3d-compat').then(RAPIER => {
// Initialize physics world
const gravity = { x: 0.0, y: -9.81, z: 0.0 };
const world = new RAPIER.World(gravity);
// Create ground
const groundColliderDesc = RAPIER.ColliderDesc.cuboid(10.0, 0.1, 10.0);
world.createCollider(groundColliderDesc);
// Create dynamic rigid body
const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(0.0, 5.0, 0.0);
const rigidBody = world.createRigidBody(rigidBodyDesc);
const colliderDesc = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5);
world.createCollider(colliderDesc, rigidBody);
// Simulation loop
function step() {
world.step();
const position = rigidBody.translation();
console.log(Body position: x=${position.x}, y=${position.y}, z=${position.z}
);
requestAnimationFrame(step);
}
step();
});
Rapier with Three.js Integration
import * as THREE from 'three';
import('@dimforge/rapier3d-compat').then(RAPIER => {
// Three.js setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(10, 10, 10);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Physics world
const world = new RAPIER.World({ x: 0.0, y: -9.81, z: 0.0 });
// Ground
const groundCollider = world.createCollider(
RAPIER.ColliderDesc.cuboid(20.0, 0.1, 20.0)
);
const groundGeometry = new THREE.BoxGeometry(40, 0.2, 40);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x808080 });
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
scene.add(groundMesh);
// Helper to create physics-enabled boxes
function createBox(x, y, z, width, height, depth) {
// Physics
const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(x, y, z);
const rigidBody = world.createRigidBody(rigidBodyDesc);
const colliderDesc = RAPIER.ColliderDesc.cuboid(width / 2, height / 2, depth / 2);
world.createCollider(colliderDesc, rigidBody);
// Visual
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshStandardMaterial({
color: Math.random() * 0xffffff
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
return { rigidBody, mesh };
}
// Create stack of boxes
const boxes = [];
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 5; j++) {
const box = createBox(
i - 2,
j * 1.1 + 0.5,
0,
1, 1, 1
);
boxes.push(box);
}
}
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Step physics
world.step();
// Sync meshes
boxes.forEach(({ rigidBody, mesh }) => {
const position = rigidBody.translation();
const rotation = rigidBody.rotation();
mesh.position.set(position.x, position.y, position.z);
mesh.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w);
});
renderer.render(scene, camera);
}
animate();
});
Advanced Rapier Features
Kinematic Character Controller:
// Create character controller
const characterControllerDesc = RAPIER.CharacterControllerDesc.create(0.1);
characterControllerDesc.enableSnapToGround(0.2);
characterControllerDesc.enableAutostep(0.3, 0.1);
const characterController = world.createCharacterController(characterControllerDesc);
// Character physics body
const characterBody = world.createRigidBody(
RAPIER.RigidBodyDesc.kinematicPositionBased()
.setTranslation(0, 5, 0)
);
const characterCollider = world.createCollider(
RAPIER.ColliderDesc.capsule(0.5, 0.3),
characterBody
);
// Movement input
const movement = { x: 0, z: 0 };
document.addEventListener('keydown', (e) => {
switch(e.key) {
case 'w': movement.z = -0.1; break;
case 's': movement.z = 0.1; break;
case 'a': movement.x = -0.1; break;
case 'd': movement.x = 0.1; break;
}
});
document.addEventListener('keyup', (e) => {
switch(e.key) {
case 'w':
case 's': movement.z = 0; break;
case 'a':
case 'd': movement.x = 0; break;
}
});
// In animation loop
function updateCharacter() {
const desiredMovement = { x: movement.x, y: -0.1, z: movement.z };
characterController.computeColliderMovement(
characterCollider,
desiredMovement
);
const correctedMovement = characterController.computedMovement();
const currentPos = characterBody.translation();
characterBody.setNextKinematicTranslation({
x: currentPos.x + correctedMovement.x,
y: currentPos.y + correctedMovement.y,
z: currentPos.z + correctedMovement.z
});
}
Advanced Use Cases and Real-World Applications
Interactive Product Configurators
Physics-enhanced product configurators enable customers to interact naturally with 3D models, rotating, stacking, and testing products:
// Furniture arrangement tool
function createFurnitureConfigurator() {
const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) });
// Room bounds (walls)
const wallMaterial = new CANNON.Material('wall');
const furnitureMaterial = new CANNON.Material('furniture');
// Wall contact behavior
const wallFurnitureContact = new CANNON.ContactMaterial(
wallMaterial,
furnitureMaterial,
{ friction: 0.9, restitution: 0.1 }
);
world.addContactMaterial(wallFurnitureContact);
// Create walls
const roomWidth = 10;
const roomDepth = 10;
[
{ pos: [0, 0, -roomDepth/2], size: [roomWidth, 3, 0.1] }, // Back wall
{ pos: [0, 0, roomDepth/2], size: [roomWidth, 3, 0.1] }, // Front wall
{ pos: [-roomWidth/2, 0, 0], size: [0.1, 3, roomDepth] }, // Left wall
{ pos: [roomWidth/2, 0, 0], size: [0.1, 3, roomDepth] } // Right wall
].forEach(({ pos, size }) => {
const shape = new CANNON.Box(new CANNON.Vec3(...size.map(s => s/2)));
const body = new CANNON.Body({ mass: 0, material: wallMaterial });
body.addShape(shape);
body.position.set(...pos);
world.addBody(body);
});
// Draggable furniture pieces
function addFurniture(type, x, z) {
const dimensions = {
sofa: { width: 2, height: 0.8, depth: 1 },
chair: { width: 0.6, height: 0.8, depth: 0.6 },
table: { width: 1.5, height: 0.7, depth: 0.8 }
};
const dim = dimensions[type];
const shape = new CANNON.Box(
new CANNON.Vec3(dim.width/2, dim.height/2, dim.depth/2)
);
const body = new CANNON.Body({
mass: 10,
material: furnitureMaterial,
linearDamping: 0.9,
angularDamping: 0.9
});
body.addShape(shape);
body.position.set(x, dim.height/2, z);
// Lock Y rotation for upright furniture
body.angularFactor.set(0, 1, 0);
world.addBody(body);
return { body, type, dimensions: dim };
}
return { world, addFurniture };
}
Educational Physics Simulations
Interactive physics demonstrations make abstract concepts tangible:
// Pendulum simulation with damping controls
function createPendulumLab() {
const world = Matter.Engine.create();
// Pendulum parameters (controllable)
const params = {
length: 200,
mass: 1,
damping: 0.01,
gravity: 1
};
world.gravity.y = params.gravity;
// Create pendulum
function createPendulum(x, y) {
const pivot = Matter.Bodies.circle(x, y, 5, {
isStatic: true,
render: { fillStyle: '#333' }
});
const bob = Matter.Bodies.circle(
x,
y + params.length,
20,
{
density: params.mass,
render: { fillStyle: '#e74c3c' }
}
);
const constraint = Matter.Constraint.create({
bodyA: pivot,
bodyB: bob,
length: params.length,
stiffness: 1,
damping: params.damping
});
return { pivot, bob, constraint };
}
const pendulum = createPendulum(400, 100);
Matter.World.add(world.world, [
pendulum.pivot,
pendulum.bob,
pendulum.constraint
]);
// Interactive controls
const controls = {
length: document.getElementById('length-slider'),
damping: document.getElementById('damping-slider'),
gravity: document.getElementById('gravity-slider')
};
controls.length.addEventListener('input', (e) => {
params.length = parseFloat(e.target.value);
pendulum.constraint.length = params.length;
});
controls.damping.addEventListener('input', (e) => {
params.damping = parseFloat(e.target.value);
pendulum.constraint.damping = params.damping;
});
controls.gravity.addEventListener('input', (e) => {
params.gravity = parseFloat(e.target.value);
world.gravity.y = params.gravity;
});
// Measure period and amplitude
let previousAngle = 0;
let peakDetection = [];
Matter.Events.on(world, 'afterUpdate', () => {
const angle = Math.atan2(
pendulum.bob.position.y - pendulum.pivot.position.y,
pendulum.bob.position.x - pendulum.pivot.position.x
);
// Detect peaks for period calculation
if (previousAngle > 0 && angle < 0) {
const time = world.timing.timestamp;
if (peakDetection.length > 0) {
const period = time - peakDetection[peakDetection.length - 1];
console.log(Period: ${period.toFixed(2)}ms
);
}
peakDetection.push(time);
if (peakDetection.length > 10) peakDetection.shift();
}
previousAngle = angle;
});
return { world, pendulum, params };
}
Game Physics: Platformer Example
// 2D platformer with Matter.js
function createPlatformerGame() {
const engine = Matter.Engine.create();
const world = engine.world;
// Player character
const player = Matter.Bodies.rectangle(100, 100, 40, 60, {
inertia: Infinity, // Prevent rotation
friction: 0.001,
frictionAir: 0.01,
render: { fillStyle: '#3498db' }
});
// Platforms
const platforms = [
Matter.Bodies.rectangle(200, 400, 400, 30, {
isStatic: true,
render: { fillStyle: '#2c3e50' }
}),
Matter.Bodies.rectangle(500, 300, 200, 30, {
isStatic: true,
render: { fillStyle: '#2c3e50' }
}),
Matter.Bodies.rectangle(100, 200, 150, 30, {
isStatic: true,
render: { fillStyle: '#2c3e50' }
})
];
Matter.World.add(world, [player, ...platforms]);
// Player controls
const keys = { left: false, right: false, jump: false };
let isGrounded = false;
document.addEventListener('keydown', (e) => {
if (e.key === 'a' || e.key === 'ArrowLeft') keys.left = true;
if (e.key === 'd' || e.key === 'ArrowRight') keys.right = true;
if (e.key === ' ') keys.jump = true;
});
document.addEventListener('keyup', (e) => {
if (e.key === 'a' || e.key === 'ArrowLeft') keys.left = false;
if (e.key === 'd' || e.key === 'ArrowRight') keys.right = false;
if (e.key === ' ') keys.jump = false;
});
// Ground detection
Matter.Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach(pair => {
if (pair.bodyA === player || pair.bodyB === player) {
isGrounded = true;
}
});
});
Matter.Events.on(engine, 'collisionEnd', (event) => {
event.pairs.forEach(pair => {
if (pair.bodyA === player || pair.bodyB === player) {
isGrounded = false;
}
});
});
// Game loop
Matter.Events.on(engine, 'beforeUpdate', () => {
const moveForce = 0.001;
const jumpForce = 0.08;
if (keys.left) {
Matter.Body.applyForce(player, player.position, { x: -moveForce, y: 0 });
}
if (keys.right) {
Matter.Body.applyForce(player, player.position, { x: moveForce, y: 0 });
}
if (keys.jump && isGrounded) {
Matter.Body.setVelocity(player, { x: player.velocity.x, y: -jumpForce });
}
// Cap horizontal velocity
if (Math.abs(player.velocity.x) > 5) {
Matter.Body.setVelocity(player, {
x: Math.sign(player.velocity.x) * 5,
y: player.velocity.y
});
}
});
return { engine, player };
}
Performance Optimization Strategies
Reducing Active Bodies
Sleeping Bodies: Physics engines automatically deactivate (sleep) bodies that haven't moved recently:
// Matter.js sleeping configuration
const engine = Matter.Engine.create({
enableSleeping: true
});
// Adjust sleep thresholds
Matter.Sleeping.set(body, {
sleepThreshold: 60, // Frames of inactivity before sleeping
sleepMinTimeout: 500 // Minimum time before sleeping eligible
});
Object Pooling: Reuse physics bodies instead of creating/destroying:
class BulletPool {
constructor(world, size) {
this.world = world;
this.pool = [];
this.active = [];
// Pre-create bullets
for (let i = 0; i < size; i++) {
const bullet = Matter.Bodies.circle(0, 0, 5, {
isSleeping: true,
render: { fillStyle: '#f39c12' }
});
this.pool.push(bullet);
Matter.World.add(this.world, bullet);
}
}
spawn(x, y, velocity) {
const bullet = this.pool.pop();
if (!bullet) return null; // Pool exhausted
// Activate and position
Matter.Sleeping.set(bullet, false);
Matter.Body.setPosition(bullet, { x, y });
Matter.Body.setVelocity(bullet, velocity);
this.active.push(bullet);
return bullet;
}
recycle(bullet) {
// Deactivate
Matter.Sleeping.set(bullet, true);
Matter.Body.setPosition(bullet, { x: -1000, y: -1000 }); // Off-screen
Matter.Body.setVelocity(bullet, { x: 0, y: 0 });
// Return to pool
this.active = this.active.filter(b => b !== bullet);
this.pool.push(bullet);
}
update() {
// Recycle bullets that left screen
this.active.forEach(bullet => {
if (bullet.position.y > 1000 || bullet.position.x > 1000) {
this.recycle(bullet);
}
});
}
}
Collision Filtering
Reduce collision checks by filtering which objects can collide:
// Matter.js collision groups
const Category = {
PLAYER: 0x0001,
ENEMY: 0x0002,
BULLET: 0x0004,
WALL: 0x0008,
PICKUP: 0x0016
};
// Player collides with enemies, walls, and pickups (not bullets)
const player = Matter.Bodies.circle(100, 100, 20, {
collisionFilter: {
category: Category.PLAYER,
mask: Category.ENEMY | Category.WALL | Category.PICKUP
}
});
// Enemy bullet collides with player and walls only
const enemyBullet = Matter.Bodies.circle(200, 200, 5, {
collisionFilter: {
category: Category.BULLET,
mask: Category.PLAYER | Category.WALL
}
});
Spatial Partitioning
Some engines automatically use spatial partitioning, but understanding helps optimization:
// Cannon.js broadphase configuration
const world = new CANNON.World({
broadphase: new CANNON.SAPBroadphase(world) // Sweep and Prune
});
// Or grid-based
world.broadphase = new CANNON.GridBroadphase();
Comparison with Alternatives
Feature Comparison Matrix
| Feature | Matter.js | Cannon.js | Rapier | Ammo.js | Oimo.js | |---------|-----------|-----------|--------|---------|---------| | Dimensions | 2D | 3D | 2D/3D | 3D | 3D | | Language | JavaScript | JavaScript | Rust/WASM | C++/WASM | JavaScript | | Performance | Good | Good | Excellent | Excellent | Good | | Learning Curve | Easy | Moderate | Moderate | Hard | Moderate | | Documentation | Excellent | Good | Good | Limited | Limited | | Ecosystem | Large | Moderate | Growing | Small | Small | | File Size | ~100KB | ~200KB | ~500KB | ~1MB | ~150KB | | Soft Bodies | Yes | Limited | No | Yes | No | | Vehicle Physics | No | Yes | Yes | Yes | Yes | | Character Controllers | No | No | Yes | Yes | No | | Multiplayer/Determinism | Good | Good | Excellent | Good | Good | | License | MIT | MIT | Apache 2.0 | zlib | MIT |
When to Choose Each Engine
Matter.js:
- •2D games, animations, or interactive experiences
- •Rapid prototyping and quick iterations
- •Teams without deep physics knowledge
- •Projects prioritizing ecosystem and community support
- •Creative coding and generative art
Cannon.js:
- •3D projects already using Three.js
- •Moderate physics requirements (not extreme performance needs)
- •Vehicle simulations and mechanical systems
- •Projects requiring established Three.js integration patterns
Rapier:
- •Performance-critical applications
- •Large-scale physics simulations (1000+ bodies)
- •Multiplayer games requiring determinism
- •Projects needing both 2D and 3D physics
- •Advanced features like character controllers
Ammo.js:
- •Existing Bullet Physics C++ expertise on team
- •Complex soft-body physics requirements
- •Projects needing Bullet compatibility
Oimo.js:
- •Lightweight 3D physics needs
- •Projects prioritizing small bundle size
- •Simple 3D object interactions
Conclusion
JavaScript physics engines have matured from experimental curiosities into production-ready tools that power everything from marketing websites to browser-based games to educational simulations. Matter.js democratizes 2D physics through approachable APIs and comprehensive documentation, enabling developers without physics backgrounds to create compelling interactive experiences. Cannon.js brings realistic motion to Three.js scenes, opening possibilities for product configurators, architectural visualizations, and 3D web games. Rapier pushes performance boundaries through WebAssembly, delivering desktop-class physics simulation in browsers.
The practical applications extend across industries: marketing teams create memorable interactive brand experiences, educators build intuitive demonstrations of physics concepts, game developers ship browser-based titles rivaling native games, and product teams enable customers to interact naturally with 3D models. The barrier to entry has never been lower—comprehensive documentation, visual debugging tools, and extensive examples accelerate development from concept to deployment.
Looking forward, WebAssembly-based engines like Rapier signal the future: native-level performance, advanced features previously limited to desktop applications, and expanding capabilities that blur lines between web and native development. Multiplayer physics games with hundreds of simultaneous players, real-time architectural simulations, and procedurally generated physics-based worlds become feasible as performance headroom increases.
For developers exploring physics integration, the path forward is clear: start with Matter.js for 2D needs or Cannon.js for 3D projects, understanding that both provide production-ready foundations with active communities. As requirements grow or performance becomes critical, migrate to Rapier's high-performance WASM implementation. The JavaScript physics ecosystem offers solutions spanning simple animations to complex simulations—all deployable through standard web browsers without plugins or native installations.
The web has evolved from static documents to dynamic, physics-driven experiences that feel tangible and responsive. JavaScript physics engines are the foundation enabling this transformation, and mastering them opens creative possibilities limited only by imagination.