Render Matter.js Bodies with PixiJS Graphics

Pairing Matter.js with PixiJS allows developers to replace the default Canvas 2D debug renderer with high-performance WebGL rendering. By decoupling the physics calculation loop from the visual presentation, you can simulate thousands of rigid bodies while maintaining a consistent 60 frames per second. This article explains how to set up the dual-engine pipeline, translate Matter.js geometry into PIXI.Graphics objects, and synchronize transform data efficiently to maximize WebGL throughput.

Decoupling Physics and Rendering

Matter.js includes a built-in Matter.Render module, but it relies on the CPU-bound Canvas 2D context. To achieve higher throughput, omit Matter.Render entirely. Instead, manage a headless physics simulation with Matter.Engine and sync the resulting transform data (positions and angles) directly to a PIXI.Application stage running on WebGL.

Initialization

First, instantiate both engines independently. Ensure the PixiJS canvas matches the dimensions of your physics world boundaries.

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

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

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

Creating Graphics from Matter.js Bodies

A common performance pitfall is clearing and redrawing vector shapes inside the animation loop via graphics.clear(). Doing this forces the GPU to rebuild vertex buffers every frame, nullifying WebGL benefits.

Instead, draw the local geometry of the Matter.js body to a PIXI.Graphics object once during initialization. Center the geometry at local coordinate (0, 0), and attach the graphic to the physics body object for quick reference.

function createPhysicsGraphic(body, color = 0xffffff) {
    const graphic = new PIXI.Graphics();
    
    // Draw geometry based on vertices relative to the body center
    graphic.fill(color);
    
    const vertices = body.vertices;
    graphic.moveTo(vertices[0].x - body.position.x, vertices[0].y - body.position.y);
    
    for (let i = 1; i < vertices.length; i++) {
        graphic.lineTo(vertices[i].x - body.position.x, vertices[i].y - body.position.y);
    }
    
    graphic.closePath();
    graphic.fill();

    // Map body to graphic
    body.pixiGraphic = graphic;
    app.stage.addChild(graphic);

    return graphic;
}

Spawning Bodies

When creating bodies with Matter.js, instantiate their physics representation, generate their visual representation, and push them to their respective containers.

// Create a dynamic box
const box = Matter.Bodies.rectangle(400, 200, 80, 80);
createPhysicsGraphic(box, 0xff0044);

// Create a static ground body
const ground = Matter.Bodies.rectangle(400, 580, 810, 40, { isStatic: true });
createPhysicsGraphic(ground, 0x222222);

// Add bodies to the Matter world
Matter.Composite.add(world, [box, ground]);

The Synchronization Render Loop

To achieve optimal throughput, use the PixiJS ticker to step the physics engine and update the hardware-accelerated transform properties (position and rotation) of each corresponding PIXI.Graphics object.

const bodies = Matter.Composite.allBodies(world);

app.ticker.add((ticker) => {
    // 1. Step the physics engine forward by delta time
    Matter.Engine.update(engine, ticker.deltaMS);

    // 2. Update graphic transforms
    for (let i = 0; i < bodies.length; i++) {
        const body = bodies[i];
        if (body.pixiGraphic) {
            body.pixiGraphic.position.set(body.position.x, body.position.y);
            body.pixiGraphic.rotation = body.angle;
        }
    }
});

Key Optimization Strategies