How to Slice and Sever Bodies in Matter.js

Slicing rigid bodies into severed polygons is a popular mechanic in 2D physics games, but Matter.js does not feature a native geometry-cutting API. To sever a body, you must mathematically intersect a cutting line with the body's polygon edges, split the original vertex set across the cut line, insert the intersection points to close the new shapes, and instantiate fresh bodies using Matter.Bodies.fromVertices. This guide walks through the step-by-step process of calculating these severed vertex arrays and converting them back into active Matter.js physics objects.

Step 1: Define the Slicing Line

A cut is defined by two points in 2D space: a start point \(A(x_1, y_1)\) and an end point \(B(x_2, y_2)\). The line equation or the cross product can determine which side of the line any given vertex falls on:

\[\text{side}(P) = (B.x - A.x) \times (P.y - A.y) - (B.y - A.y) \times (P.x - A.x)\]

Step 2: Detect Edge Intersections

Iterate through the target body's vertices in world coordinates (body.vertices). Check each edge formed by vertex pair \((V_i, V_{i+1})\) for an intersection with line segment \(AB\):

function getLineIntersection(p1, p2, p3, p4) {
    const denominator = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
    if (denominator === 0) return null; // Lines are parallel

    const ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denominator;
    const ub = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denominator;

    // Check if the intersection falls within both segments
    if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
        return {
            x: p1.x + ua * (p2.x - p1.x),
            y: p1.y + ua * (p2.y - p1.y)
        };
    }
    return null;
}

A clean cut across a convex polygon produces exactly two intersection points.

Step 3: Reconstruct Two Vertex Lists

To rebuild the severed geometry into two distinct shapes, traverse the original polygon's perimeter. Sort the vertices into two lists, inserting the newly computed intersection points where the cut occurs:

function splitPolygon(vertices, cutStart, cutEnd) {
    const polyA = [];
    const polyB = [];

    for (let i = 0; i < vertices.length; i++) {
        const current = vertices[i];
        const next = vertices[(i + 1) % vertices.length];

        const sideA = (cutEnd.x - cutStart.x) * (current.y - cutStart.y) - 
                      (cutEnd.y - cutStart.y) * (current.x - cutStart.x);

        if (sideA >= 0) {
            polyA.push(current);
        } else {
            polyB.push(current);
        }

        const intersection = getLineIntersection(current, next, cutStart, cutEnd);
        if (intersection) {
            polyA.push(intersection);
            polyB.push(intersection);
        }
    }

    return [polyA, polyB];
}

Step 4: Ensure Correct Vertex Winding and Convexity

Matter.js expects vertices to follow a clockwise winding order. If the resulting vertex lists are wound counter-clockwise, reverse them before creating bodies.

Furthermore, Matter.js requires convex hulls for physics calculation. If cutting a body produces non-convex (concave) pieces, ensure the poly-decomp library is installed and exposed globally to Matter.js:

// Provide decomp to Matter before creating bodies
window.decomp = require('poly-decomp');

Step 5: Replace the Old Body with the Severed Pieces

Compute the center of mass for each new vertex list, generate the new physics bodies using Bodies.fromVertices, transfer any momentum or properties from the parent body, and update the engine world:

function cutBody(world, body, cutStart, cutEnd) {
    const [pointsA, pointsB] = splitPolygon(body.vertices, cutStart, cutEnd);

    // Minimum 3 vertices required to form a polygon
    if (pointsA.length < 3 || pointsB.length < 3) return;

    // Calculate approximate center for placement
    const centerA = Matter.Vertices.centre(pointsA);
    const centerB = Matter.Vertices.centre(pointsB);

    const bodyA = Matter.Bodies.fromVertices(centerA.x, centerA.y, [pointsA], {
        render: { fillStyle: body.render.fillStyle }
    });

    const bodyB = Matter.Bodies.fromVertices(centerB.x, centerB.y, [pointsB], {
        render: { fillStyle: body.render.fillStyle }
    });

    if (bodyA && bodyB) {
        // Inherit parent linear and angular velocity
        Matter.Body.setVelocity(bodyA, body.velocity);
        Matter.Body.setVelocity(bodyB, body.velocity);
        Matter.Body.setAngularVelocity(bodyA, body.angularVelocity);
        Matter.Body.setAngularVelocity(bodyB, body.angularVelocity);

        // Swap out old body for new severed bodies
        Matter.Composite.remove(world, body);
        Matter.Composite.add(world, [bodyA, bodyB]);
    }
}

This dynamic polygon replacement severs the geometry instantly while preserving kinetic continuity in the simulation.