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:
- Detect an impact or trigger a destruction event on a parent body.
- Generate seed points (sites) concentrated around the impact point.
- Compute a Voronoi diagram based on these points.
- Clip the Voronoi cells to fit the boundary of the original object.
- 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
- Limit Fragment Count: Keep fragment counts between 15 and 40 per destroyed object. Higher counts introduce vertex complexity that can degrade frame rates.
- Filter Small Polygons: Discard cells below an area threshold. Tiny polygons consume physics steps without adding significant visual value.
- Debris Despawning: Set timeouts or fade-out routines to remove shards from the physics world after they come to rest, preventing excessive sleeping bodies from accumulating in the simulation.