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:

  1. Radial Visibility Polygons (Ray-Sweeping): Rays are cast from the light position toward every vertex in the scene, plus offset rays at angle ± 0.00001 radians. Rays are sorted by angle, ray-segment intersections are calculated, and a single light polygon is drawn.
  2. 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:

  1. Create an Offscreen Canvas: Allocate an offscreen light canvas matching your main canvas dimensions.
  2. Render the Light Texture: Draw a radial gradient centered at the light source (createRadialGradient), transitioning from the light's color to complete transparency at lightRadius.
  3. Subtract the Shadows: Set the offscreen canvas's globalCompositeOperation = 'destination-out' and execute renderShadows(). The shadow quads will erase the light gradient where geometry blocks the light.
  4. 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