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:

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:

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