Realistic Matter.js Shatter Debris Using Voronoi

Creating realistic shatter physics in a 2D environment requires dividing an object into organic, irregular fragments and converting them into independent physical bodies. By combining Voronoi cell decomposition with Matter.js, you can generate natural-looking shards from an impact point and simulate procedural destruction. This guide covers the mathematical pipeline, polygon clipping, and physics body generation needed to produce believable shattering debris.

1. Understanding the Fragmentation Pipeline

Matter.js is a rigid-body physics engine that does not include native procedural destruction tools. To shatter an object, you must:

  1. Detect an impact or trigger a destruction event on a parent body.
  2. Generate seed points (sites) concentrated around the impact point.
  3. Compute a Voronoi diagram based on these points.
  4. Clip the Voronoi cells to fit the boundary of the original object.
  5. Replace the original body in the Matter.js world with new dynamic bodies constructed from the clipped cell vertices.

2. Generating Voronoi Seeds

Voronoi decomposition divides a plane into regions based on distances to specific seed points. For realistic breakage—such as glass or concrete—fragments should be smaller and denser near the point of impact, growing larger toward the edges.

You can achieve this by distributing points using a Gaussian or power-law distribution centered on the impact coordinate:

function generateSeeds(impactPoint, count, radius) {
  const points = [];
  for (let i = 0; i < count; i++) {
    // Bias the distribution toward the center using a square root or power distribution
    const r = radius * Math.pow(Math.random(), 2);
    const theta = Math.random() * 2 * Math.PI;

    points.push([
      impactPoint.x + r * Math.cos(theta),
      impactPoint.y + r * Math.sin(theta)
    ]);
  }
  return points;
}

3. Computing and Clipping Cells

Using an external computational geometry library like d3-delaunay, compute the Voronoi diagram over the bounding box of the target object.

Because Voronoi cells extend outward, non-rectangular shapes must be constrained by the original shape's geometry using a polygon clipping library (such as polygon-clipping or clipper-lib):

import { Delaunay } from "d3-delaunay";
import polygonClipping from "polygon-clipping";

function createFragments(parentPolygon, seedPoints, bounds) {
  const delaunay = Delaunay.from(seedPoints);
  const voronoi = delaunay.voronoi([bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y]);

  const clippedPolygons = [];

  for (const cell of voronoi.cellPolygons()) {
    // Intersect each Voronoi cell with the original shape polygon
    const intersection = polygonClipping.intersection([parentPolygon], [cell]);
    
    if (intersection.length > 0) {
      // Intersection can return multiple components; extract valid vertex lists
      for (const poly of intersection) {
        clippedPolygons.push(poly[0].map(([x, y]) => ({ x, y })));
      }
    }
  }

  return clippedPolygons;
}

4. Instantiating Matter.js Rigid Bodies

Convert each clipped polygon into a dynamic body using Matter.Bodies.fromVertices(). Matter.js uses poly-decomp under the hood to handle non-convex polygons, but Voronoi cells are inherently convex, making creation efficient.

function spawnDebris(world, fragmentVertices, originalBody, impactPoint) {
  // Remove the original body from the simulation
  Matter.Composite.remove(world, originalBody);

  const debrisBodies = fragmentVertices.map(vertices => {
    // Calculate centroid of the vertices
    const center = Matter.Vertices.centre(vertices);

    const body = Matter.Bodies.fromVertices(center.x, center.y, [vertices], {
      density: originalBody.density,
      friction: originalBody.friction,
      restitution: 0.1, // Shards typically have low bounciness
      render: originalBody.render
    });

    if (!body) return null;

    // Apply explosive radial force outward from impact location
    const forceDirection = Matter.Vector.normalise(
      Matter.Vector.sub(body.position, impactPoint)
    );
    const distance = Matter.Vector.magnitude(
      Matter.Vector.sub(body.position, impactPoint)
    );
    
    // Closer pieces receive higher force
    const magnitude = 0.05 / (1 + distance * 0.05);
    Matter.Body.applyForce(body, body.position, {
      x: forceDirection.x * magnitude,
      y: forceDirection.y * magnitude
    });

    // Add angular velocity for realistic spinning debris
    Matter.Body.setAngularVelocity(body, (Math.random() - 0.5) * 0.2);

    return body;
  }).filter(Boolean);

  Matter.Composite.add(world, debrisBodies);
}

5. Performance Optimization Tips