Cull Off-Screen Bodies in Matter.js for Performance

Optimizing 2D physics simulations requires reducing the computational and rendering load of entities outside the player's view. In Matter.js, culling bodies outside the camera viewport prevents unnecessary canvas draw calls and can significantly lower CPU/GPU usage. This guide explains how to define a dynamic viewport bounding box, test body bounds against camera coordinates using built-in Matter.js utilities, and conditionally toggle rendering visibility and physics sleeping for off-screen objects.

Understanding Culling in Matter.js

Matter.js maintains an internal Axis-Aligned Bounding Box (AABB) for every physics body, stored in body.bounds. When using the built-in Matter.Render module, the engine attempts to draw all visible bodies added to the composite world. By checking whether a body's bounds intersect with the camera's current visible area, you can set body.render.visible = false to skip drawing passes, or put the body to sleep to save collision calculation time.

Step 1: Define Camera Bounds

To check whether bodies are inside the view, define the viewport as a Matter.js Bounds object based on the camera position, screen width, screen height, and any zoom factor:

const camera = {
    x: 0,
    y: 0,
    width: window.innerWidth,
    height: window.innerHeight
};

function getCameraBounds(camera) {
    return {
        min: { x: camera.x, y: camera.y },
        max: { x: camera.x + camera.width, y: camera.y + camera.height }
    };
}

Step 2: Test for Intersections

Matter.js provides the Matter.Bounds.overlaps method, which takes two bounding boxes and returns true if they collide. Iterate through all active bodies in your composite world before each render cycle:

const { Composite, Bounds, Sleeping } = Matter;

function cullBodies(engine, cameraBounds) {
    const allBodies = Composite.allBodies(engine.world);

    for (let i = 0; i < allBodies.length; i++) {
        const body = allBodies[i];

        // Skip static boundaries (e.g., ground) if they should always process
        if (body.isStatic) continue;

        const isVisible = Bounds.overlaps(body.bounds, cameraBounds);

        // Toggle rendering
        body.render.visible = isVisible;

        // Optional: Put off-screen bodies to sleep to pause physics calculations
        if (!isVisible && !body.isSleeping) {
            Sleeping.set(body, true);
        } else if (isVisible && body.isSleeping) {
            Sleeping.set(body, false);
        }
    }
}

Step 3: Integrate with the Render Loop

Hook the culling function into the engine lifecycle before the rendering phase occurs. If using Matter.Render, attach the function to the beforeRender event:

Matter.Events.on(render, 'beforeRender', () => {
    const cameraBounds = getCameraBounds(camera);
    cullBodies(engine, cameraBounds);
});

If using a custom rendering pipeline (such as PixiJS or raw HTML5 Canvas), execute cullBodies right before drawing elements to ensure bodies marked invisible are excluded from your custom draw routines. Adding a slight padding (e.g., 50–100 pixels) to cameraBounds helps prevent visual pop-in as objects enter the edge of the screen.