Render Matter.js Bodies with PixiJS for WebGL

Combining Matter.js with PixiJS allows developers to separate 2D rigid-body physics calculations from hardware-accelerated WebGL rendering. While Matter.js includes a built-in HTML5 Canvas renderer, it is meant primarily for prototyping and debugging rather than production performance. By bypassing Matter's default canvas renderer and using PixiJS's Application and Ticker to visually mirror physics bodies, you achieve smooth, high-framerate rendering even with hundreds of colliding objects on screen.

Architecture Overview

To render Matter.js bodies using PixiJS:

  1. Initialize the Matter.js physics engine (Engine and World) without instantiating Matter.Render.
  2. Initialize the PixiJS Application configured for WebGL.
  3. Create pairs of Matter.js bodies and PixiJS display objects (PIXI.Sprite or PIXI.Graphics).
  4. Drive the physics simulation inside the PixiJS render loop and synchronize the transforms (position and rotation) from the physics bodies to the visual display objects.

Step 1: Initialize Matter.js and PixiJS

Set up both engines independently. Avoid calling Matter.Runner.run() directly if you want strict frame synchronization; instead, advance the physics step manually inside Pixi's ticker.

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

// 1. Initialize PixiJS
const app = new PIXI.Application({
    width: 800,
    height: 600,
    backgroundColor: 0x1099bb,
    preference: 'webgl'
});
document.body.appendChild(app.view);

// 2. Initialize Matter.js Engine
const engine = Matter.Engine.create();
const world = engine.world;

Step 2: Create Linked Physics and Visual Objects

When creating physics bodies, generate a corresponding PixiJS display object. Store a reference connecting the two entities so their properties can be updated during the loop.

Important: Matter.js sets the origin of bodies to their center of mass. For rectangles and regular shapes, ensure the PixiJS Sprite's anchor is set to (0.5, 0.5) so rotations occur around the exact center.

// Array to keep track of paired objects
const physicsVisualPairs = [];

function createBox(x, y, width, height) {
    // Matter.js rigid body
    const body = Matter.Bodies.rectangle(x, y, width, height, {
        restitution: 0.8
    });
    Matter.Composite.add(world, body);

    // PixiJS visual representation
    const graphics = new PIXI.Graphics();
    graphics.beginFill(0xffffff);
    graphics.drawRect(-width / 2, -height / 2, width, height);
    graphics.endFill();
    graphics.x = x;
    graphics.y = y;
    app.stage.addChild(graphics);

    // Store pair
    physicsVisualPairs.push({ body, view: graphics });
}

// Add a ground plane and dynamic boxes
const ground = Matter.Bodies.rectangle(400, 590, 800, 20, { isStatic: true });
Matter.Composite.add(world, ground);

const groundGraphics = new PIXI.Graphics();
groundGraphics.beginFill(0x27ae60);
groundGraphics.drawRect(-400, -10, 800, 20);
groundGraphics.endFill();
groundGraphics.x = 400;
groundGraphics.y = 590;
app.stage.addChild(groundGraphics);

for (let i = 0; i < 50; i++) {
    createBox(200 + Math.random() * 400, 50 + Math.random() * 200, 30, 30);
}

Step 3: Implement the Synchronization Loop

Use app.ticker to bind the physics step and render step together. Update Matter's engine, then copy the physics body's position.x, position.y, and angle to the PixiJS object's x, y, and rotation.

app.ticker.add((delta) => {
    // Advance physics based on the frame delta time (in milliseconds)
    // delta is normalized in PixiJS (1 = ~16.66ms at 60fps)
    const deltaMs = delta * (1000 / 60);
    Matter.Engine.update(engine, deltaMs);

    // Synchronize transforms
    for (let i = 0; i < physicsVisualPairs.length; i++) {
        const { body, view } = physicsVisualPairs[i];
        view.x = body.position.x;
        view.y = body.position.y;
        view.rotation = body.angle;
    }
});

Optimization Techniques for Maximum Performance