Apply Bloom Shaders to a Matter.js Canvas

This guide explains how to integrate WebGL-based post-processing bloom shaders into a custom 2D canvas driven by Matter.js physics. Because the native Matter.js renderer relies on the standard Canvas 2D API—which lacks hardware-accelerated shader support—implementing bloom requires decoupling the physics engine, rendering visuals either directly to an offscreen canvas or through a WebGL framework, and passing the frame through a multi-pass bloom shader pipeline.

1. Decouple Matter.js Physics from Default Rendering

The default Matter.Render module uses the standard 2D context (CanvasRenderingContext2D), which cannot execute GLSL shaders. To apply bloom, initialize only the engine and runner:

const { Engine, Runner, Bodies, Composite } = Matter;

const engine = Engine.create();
const runner = Runner.create();
Runner.run(runner, engine);

// Add your physics bodies to the engine world
const box = Bodies.rectangle(400, 200, 80, 80);
Composite.add(engine.world, [box]);

2. Choose an Architecture

There are two primary approaches to applying bloom shaders to Matter.js coordinates:

  1. Offscreen Canvas 2D to WebGL (Hybrid): Render custom graphics to an offscreen 2D canvas, bind that canvas as a dynamic WebGL texture, and run a bloom post-processing pass over a full-screen quad.
  2. Native WebGL 2D Scene: Mirror Matter.js body coordinates directly in a 2D WebGL layer (using raw WebGL, Pixi.js, or Three.js with an orthographic camera), rendering directly into a post-processing pipeline.

The hybrid offscreen method is ideal when you already have complex custom Canvas 2D rendering logic. Direct WebGL rendering offers significantly better performance.

3. Implementing Bloom via Offscreen Canvas (Hybrid Approach)

Step A: Setup the Offscreen Canvas

Create a hidden HTML5 canvas element for drawing the scene using familiar Canvas 2D methods.

const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = 800;
offscreenCanvas.height = 600;
const offscreenCtx = offscreenCanvas.getContext('2d');

Step B: Setup the WebGL Post-Processing Pipeline

Initialize a visible WebGL canvas that acts as the final display. Create a full-screen quad (two triangles covering the viewport) and set up three shader passes:

  1. Threshold Pass: Samples the offscreen texture and isolates bright areas above a defined luminosity threshold.
  2. Blur Pass: Performs a two-pass separable Gaussian blur (horizontal, then vertical) on the extracted bright areas. Multiple downsampled iterations yield a smoother glow.
  3. Composite Pass: Adds the blurred bright texture additively on top of the original offscreen texture.
// Fragment Shader: Additive Blend Pass
precision mediump float;
varying vec2 vUv;

uniform sampler2D uBaseTexture;
uniform sampler2D uBloomTexture;
uniform float uBloomIntensity;

void main() {
    vec4 baseColor = texture2D(uBaseTexture, vUv);
    vec4 bloomColor = texture2D(uBloomTexture, vUv);
    
    // Additive blend
    gl_FragColor = baseColor + (bloomColor * uBloomIntensity);
}

Step C: Update and Render Loop

On each animation frame, clear the offscreen context, render your Matter.js bodies, upload the offscreen canvas to the WebGL texture, and execute the shader passes.

function render() {
    // 1. Draw custom 2D graphics based on Matter.js body coordinates
    offscreenCtx.fillStyle = '#050505';
    offscreenCtx.fillRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);

    offscreenCtx.save();
    offscreenCtx.translate(box.position.x, box.position.y);
    offscreenCtx.rotate(box.angle);
    // Use bright or saturated colors for elements meant to bloom
    offscreenCtx.fillStyle = '#00ffff';
    offscreenCtx.fillRect(-40, -40, 80, 80);
    offscreenCtx.restore();

    // 2. Upload offscreen canvas as WebGL texture
    gl.bindTexture(gl.TEXTURE_2D, baseTexture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, offscreenCanvas);

    // 3. Execute Brightness Threshold Pass to Framebuffer A
    // 4. Execute Ping-Pong Gaussian Blur Passes between Framebuffer A and B
    // 5. Draw final composite quad to screen canvas

    requestAnimationFrame(render);
}
requestAnimationFrame(render);

4. Faster Alternative: Using Three.js Post-Processing

Writing multi-pass framebuffer ping-pong buffers from scratch in raw WebGL is verbose. You can streamline the process using Three.js with an orthographic camera and the built-in UnrealBloomPass:

  1. Set Up an Orthographic Scene: Align camera bounds to match screen pixels (e.g., width 800, height 600).
  2. Synchronize Meshes: Create flat 2D planes or meshes for each Matter.js body.
  3. Configure EffectComposer:
    • Attach a RenderPass.
    • Attach an UnrealBloomPass with custom threshold, strength, and radius values.
  4. Update Coordinates: Update the Three.js mesh positions and rotations using body.position.x, body.position.y, and body.angle inside requestAnimationFrame.
  5. Render: Call composer.render() instead of the standard renderer.

Summary Checklist