Polygon Simplification in Matter.js Destructible Terrain
Dynamic terrain destruction in Matter.js often degrades physics engine performance due to vertex inflation caused by repeated geometric subtractions. When explosive craters carve into 2D terrain, boolean clipping algorithms produce complex paths containing redundant, micro-distance, and collinear vertices. This guide explains how to implement a robust polygon simplification pipeline using the Ramer-Douglas-Peucker (RDP) algorithm and convex decomposition to maintain steady frame rates and stable physics bodies in Matter.js.
The Performance Problem
Matter.js relies on the Separating Axis Theorem (SAT) for collision detection. The computational cost of SAT scales directly with the number of vertices and the convex sub-bodies making up a composite shape.
When explosives carve out parts of a terrain, libraries such as ClipperLib or polybooljs output complex paths. Consecutive blasts compound this issue:
- Thousands of tiny edges form around jagged explosion perimeters.
- Near-collinear points form along straight edges that intersect explosion boundaries.
- Convex decomposition libraries (like
poly-decomp.js, used internally by Matter.js) become computationally expensive or fail entirely, generating degenerate triangles.
Step 1: Execute the Boolean Subtraction
Terrain destruction begins by modeling the blast as a geometric subtraction. The explosion radius is approximated as a low-resolution polygon (typically 12 to 16 vertices rather than a smooth circle) to avoid introducing unnecessary detail at the source.
// Example using ClipperLib
function carveTerrain(terrainPolygon, blastPolygon) {
const clipper = new ClipperLib.Clipper();
const solution = new ClipperLib.Paths();
clipper.AddPath(terrainPolygon, ClipperLib.PolyType.ptSubject, true);
clipper.AddPath(blastPolygon, ClipperLib.PolyType.ptClip, true);
clipper.Execute(
ClipperLib.ClipType.ctDifference,
solution,
ClipperLib.PolyFillType.pftNonZero,
ClipperLib.PolyFillType.pftNonZero
);
return solution; // Array of remaining polygon paths
}Step 2: Remove Collinear and Redundant Points
Before passing the resulting paths through aggressive simplification, prune identical points and points lying along a straight line. ClipperLib provides a built-in clean method, or you can implement a tolerance-based pass:
- Distance Threshold: Remove points closer than an established threshold (e.g., 1–2 pixels).
- Angle Threshold: Remove any vertex whose angle relative to its neighboring edges is within a fraction of a degree from 180°.
// Clean degenerate micro-edges using ClipperLib's native utility
ClipperLib.JS.Clean(solution, 1.5); // 1.5 pixel toleranceStep 3: Apply the Ramer-Douglas-Peucker (RDP) Algorithm
The Ramer-Douglas-Peucker algorithm reduces a curve composed of line
segments to a similar curve with fewer points based on an
epsilon distance threshold. Applying RDP directly to each
path returned by the boolean subtraction eliminates the jagged perimeter
without visibly degrading the terrain shape.
function getPerpendicularDistance(point, lineStart, lineEnd) {
const dx = lineEnd.x - lineStart.x;
const dy = lineEnd.y - lineStart.y;
const mag = Math.hypot(dx, dy);
if (mag === 0) return Math.hypot(point.x - lineStart.x, point.y - lineStart.y);
return Math.abs(dy * point.x - dx * point.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x) / mag;
}
function simplifyRDP(points, epsilon) {
if (points.length <= 2) return points;
let maxDist = 0;
let index = 0;
const end = points.length - 1;
for (let i = 1; i < end; i++) {
const dist = getPerpendicularDistance(points[i], points[0], points[end]);
if (dist > maxDist) {
maxDist = dist;
index = i;
}
}
if (maxDist > epsilon) {
const left = simplifyRDP(points.slice(0, index + 1), epsilon);
const right = simplifyRDP(points.slice(index), epsilon);
return left.slice(0, -1).concat(right);
} else {
return [points[0], points[end]];
}
}An epsilon value between 1.0 and
3.0 typically yields a 60–80% reduction in vertex count
while keeping visual discrepancies undetectable to the player.
Step 4: Validate Polygon Validity and Winding
Matter.js and poly-decomp.js require paths to follow
strict constraints:
- Clockwise Winding: Ensure outer contours are wound clockwise, while internal holes are wound counter-clockwise.
- Non-Self-Intersecting: RDP can occasionally
introduce self-intersections if
epsilonis set too high on tight geometry. Running a fast simplification pass with Clipper’sSimplifyPolygonsprevents this. - Minimum Vertex Limit: Discard any closed path that ends up with fewer than 3 vertices.
Step 5: Reconstruct the Matter.js Bodies
Once the polygon data is clean, pass the simplified vertices into
Matter.Bodies.fromVertices. Avoid replacing the entire
terrain body every frame; only update the terrain chunks overlapping the
explosion's axis-aligned bounding box (AABB).
function updateTerrainPhysics(world, oldBody, simplifiedPaths) {
// 1. Remove the old, un-carved body
Matter.Composite.remove(world, oldBody);
const newBodies = [];
// 2. Re-create rigid bodies from simplified paths
for (const path of simplifiedPaths) {
if (path.length < 3) continue;
const body = Matter.Bodies.fromVertices(
0, 0,
path,
{
isStatic: true,
render: { fillStyle: '#444444' }
},
true // Flag to auto-decompose concave shapes
);
if (body) {
// Reposition body to its calculated centroid
Matter.Body.setPosition(body, Matter.Vertices.centre(path));
newBodies.push(body);
}
}
// 3. Add updated bodies back to the simulation
Matter.Composite.add(world, newBodies);
return newBodies;
}Architectural Best Practices for Production
- Chunking: Divide the terrain into grid sectors (e.g., 256x256 pixels). Explosions only perform boolean clipping and simplification on chunks they intersect, preventing the global terrain array from ballooning in complexity.
- Worker Threads: Offload boolean clipping and RDP
simplification to a Web Worker. Passing vertex arrays back and forth via
ArrayBufferkeeps the main thread and physics step running at 60 FPS. - Decomposition Caching: If a piece of terrain is detached completely and forms an island that is no longer being destroyed, cache its convex decomposition to prevent redundant processing in future frames.