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:

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:

// Clean degenerate micro-edges using ClipperLib's native utility
ClipperLib.JS.Clean(solution, 1.5); // 1.5 pixel tolerance

Step 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:

  1. Clockwise Winding: Ensure outer contours are wound clockwise, while internal holes are wound counter-clockwise.
  2. Non-Self-Intersecting: RDP can occasionally introduce self-intersections if epsilon is set too high on tight geometry. Running a fast simplification pass with Clipper’s SimplifyPolygons prevents this.
  3. 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