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.