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:
- 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.
- 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:
- Threshold Pass: Samples the offscreen texture and isolates bright areas above a defined luminosity threshold.
- Blur Pass: Performs a two-pass separable Gaussian blur (horizontal, then vertical) on the extracted bright areas. Multiple downsampled iterations yield a smoother glow.
- 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:
- Set Up an Orthographic Scene: Align camera bounds to match screen pixels (e.g., width 800, height 600).
- Synchronize Meshes: Create flat 2D planes or meshes for each Matter.js body.
- Configure EffectComposer:
- Attach a
RenderPass. - Attach an
UnrealBloomPasswith custom threshold, strength, and radius values.
- Attach a
- Update Coordinates: Update the Three.js mesh
positions and rotations using
body.position.x,body.position.y, andbody.angleinsiderequestAnimationFrame. - Render: Call
composer.render()instead of the standard renderer.
Summary Checklist
- Do not use
Matter.Render.create()for custom WebGL effects. - Isolate bodies intended to glow by giving them higher RGB values or rendering them into a distinct brightness-threshold buffer.
- Downsample the blur target buffers (e.g., half or quarter resolution) to maintain high frame rates during the multi-pass Gaussian blur.
- Keep the final composition additive to ensure dark backgrounds remain dark while glowing objects blend naturally.