Dynamic 2D Shadows with Matter.js Bodies
Implementing dynamic 2D lighting and shadows in a Matter.js physics simulation requires combining the physics engine's body definitions with geometric raycasting or shadow volume extrusion. By extracting the world-space vertices of Matter.js rigid bodies, determining their silhouette relative to a point light source, and projecting shadow geometry onto an overlay canvas, you can create realistic, performant real-time lighting without a 3D rendering pipeline.
1. Extracting Geometry from Matter.js
Matter.js stores the transformed world coordinates of any rigid body
in its vertices array. To calculate shadows, you must
iterate over your active physics bodies and collect their edges.
function getBodyEdges(bodies) {
const segments = [];
for (const body of bodies) {
// Skip sensors or non-colliding decorative bodies if needed
if (body.isSensor) continue;
const vertices = body.vertices;
for (let i = 0; i < vertices.length; i++) {
const nextIndex = (i + 1) % vertices.length;
segments.push({
p1: { x: vertices[i].x, y: vertices[i].y },
p2: { x: vertices[nextIndex].x, y: vertices[nextIndex].y }
});
}
}
return segments;
}2. Choosing a Shadow Generation Technique
There are two primary approaches for 2D dynamic shadows:
- Radial Visibility Polygons (Ray-Sweeping): Rays are
cast from the light position toward every vertex in the scene, plus
offset rays at
angle ± 0.00001radians. Rays are sorted by angle, ray-segment intersections are calculated, and a single light polygon is drawn. - Shadow Volume Extrusion (Edge Projection): For each line segment of an obstacle, determine if it faces away from the light source. Extrude the endpoints of the segment away from the light to construct a shadow quad.
For polygonal bodies in Matter.js, Shadow Volume Extrusion is often the most performant and numerically stable approach.
3. Implementing Shadow Volume Extrusion
For each edge of a polygon, evaluate whether it is a boundary edge relative to the light. If the edge faces the light source, its endpoints are projected outward to create a shadow polygon:
function renderShadows(ctx, light, segments, lightRadius) {
ctx.fillStyle = "rgba(0, 0, 0, 1)";
for (const segment of segments) {
const { p1, p2 } = segment;
// Normal vector of the edge
const nx = -(p2.y - p1.y);
const ny = p2.x - p1.x;
// Vector from light to the first vertex
const lx = p1.x - light.x;
const ly = p1.y - light.y;
// Normal dot light direction determines if edge faces the light
if (nx * lx + ny * ly < 0) {
// Calculate projection directions
const d1x = p1.x - light.x;
const d1y = p1.y - light.y;
const d2x = p2.x - light.x;
const d2y = p2.y - light.y;
// Project vertices to the boundary of the light radius
const p3 = {
x: p1.x + d1x * lightRadius,
y: p1.y + d1y * lightRadius
};
const p4 = {
x: p2.x + d2x * lightRadius,
y: p2.y + d2y * lightRadius
};
// Draw the shadow quad
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.lineTo(p4.x, p4.y);
ctx.lineTo(p3.x, p3.y);
ctx.closePath();
ctx.fill();
}
}
}4. Compositing the Light and Shadows
To render the dynamic effect using the HTML5 Canvas 2D API:
- Create an Offscreen Canvas: Allocate an offscreen light canvas matching your main canvas dimensions.
- Render the Light Texture: Draw a radial gradient
centered at the light source (
createRadialGradient), transitioning from the light's color to complete transparency atlightRadius. - Subtract the Shadows: Set the offscreen canvas's
globalCompositeOperation = 'destination-out'and executerenderShadows(). The shadow quads will erase the light gradient where geometry blocks the light. - Blend with the Main Scene: Draw the ambient dark
layer over the main scene, then draw the resulting offscreen light
canvas on top using
globalCompositeOperation = 'lighter'to illuminate the environment and Matter.js bodies.
5. Performance Considerations
- Spatial Partitioning: Only extract segments from
Matter.js bodies whose axis-aligned bounding boxes (AABB) intersect the
circular bounds of the light source
(
Matter.Bounds.overlaps). - Vertex Reduction: Complex bodies generated from SVG paths or convex decompositions can introduce hundreds of vertices. Simplify physics hulls using algorithms like Ramer-Douglas-Peucker before passing them to the shadow pipeline.
- Canvas Resolution: Scale down the offscreen light canvas by a factor of 2 or 4 and scale it up when blending. The bilinear interpolation will naturally soften shadow edges and significantly reduce fill-rate overhead.