Integrate Matter.js with Three.js for 2D Physics
Integrating Matter.js with Three.js allows developers to drive 3D visual assets using a lightweight, predictable 2D rigid-body physics engine. This guide explains how to map 2D physics bodies to 3D meshes along a single plane, reconcile coordinate systems between both libraries, and synchronize transformations inside an active animation loop.
Understanding the Core Concept
In a hybrid 2D/3D setup, Matter.js acts as the invisible logic layer while Three.js serves as the rendering layer. Physics calculations are restricted to a single flat plane—typically the XY or XZ plane in Three.js space. For every rigid body created in Matter.js, a corresponding 3D mesh is created in Three.js. On each frame, the position and rotation of the 3D mesh are updated to match the state of its associated Matter.js body.
Handling Coordinate Systems
Three.js and Matter.js use different default coordinate conventions:
- Matter.js: The origin
(0, 0)is at the top-left corner of the screen. The X-axis increases to the right, and the Y-axis increases downward. - Three.js: The origin
(0, 0, 0)is centered in the world space. The X-axis increases to the right, the Y-axis increases upward, and the Z-axis extends toward the viewer.
To keep them aligned, you must convert the coordinates when transferring position data from Matter.js to Three.js. When mapping to the Three.js XY plane:
- X position: Shift the Matter.js body position by
half the viewport width:
mesh.position.x = body.position.x - width / 2 - Y position: Invert the Matter.js Y value and shift
by half the viewport height:
mesh.position.y = -(body.position.y - height / 2) - Rotation: Invert the angle to match the coordinate
reversal:
mesh.rotation.z = -body.angle
Step-by-Step Implementation
1. Initialize Engines and Scene
Create the Three.js scene, orthographic or perspective camera, and renderer. Then initialize the Matter.js engine and world without attaching the default Matter.js canvas renderer.
import * as THREE from 'three';
import Matter from 'matter-js';
const width = window.innerWidth;
const height = window.innerHeight;
// Three.js setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.z = 600;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
// Matter.js setup
const { Engine, Bodies, Composite } = Matter;
const engine = Engine.create();
const world = engine.world;2. Create Paired Objects
Create a helper function to instantiate a Matter.js body and a matching Three.js mesh simultaneously, storing them together in an array for synchronization.
const physicsPairs = [];
function createBox(x, y, w, h, isStatic = false) {
// Create Matter.js body
const body = Bodies.rectangle(x, y, w, h, { isStatic });
Composite.add(world, body);
// Create Three.js mesh
const geometry = new THREE.BoxGeometry(w, h, 40);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
physicsPairs.push({ body, mesh });
}
// Ground
createBox(width / 2, height - 20, width, 40, true);
// Dynamic boxes
createBox(width / 2, 100, 60, 60);
createBox(width / 2 + 20, 200, 80, 80);3. Run the Synchronization Loop
Advance the physics engine on each frame, copy the updated positions and rotations to the Three.js meshes, and render the scene.
function animate() {
requestAnimationFrame(animate);
// Advance physics simulation
Matter.Engine.update(engine, 1000 / 60);
// Sync Matter.js bodies with Three.js meshes
for (let i = 0; i < physicsPairs.length; i++) {
const { body, mesh } = physicsPairs[i];
mesh.position.x = body.position.x - width / 2;
mesh.position.y = -(body.position.y - height / 2);
mesh.rotation.z = -body.angle;
}
renderer.render(scene, camera);
}
animate();Best Practices
- Use Orthographic Cameras for Precision: If the
scene requires exact pixel-to-unit mapping matching standard 2D canvas
designs, use a
THREE.OrthographicCameraconfigured to match the window dimensions. - Offscreen Removal: When objects fall out of bounds,
remember to remove both the body via
Composite.remove(world, body)and the mesh viascene.remove(mesh)along with disposing of its geometry and materials to prevent memory leaks. - Fixed Timesteps: In production environments, use a
fixed delta time with
Matter.Engine.update(engine, delta)to prevent physics tunneling or erratic collisions on displays running above 60Hz.