How to Slice a Matter.js Body Along a Line
Slicing a rigid body cleanly into two halves in Matter.js requires geometry processing because the engine does not have a built-in polygon clipping API. The process involves identifying where an arbitrary cutting line intersects the edges of a body's polygon, dividing the original ordered vertices along those intersection points into two separate closed loops, and replacing the original body with two newly instantiated bodies in the physics world while preserving linear and angular momentum.
1. Define the Cutting Line
Represent the cutting line with two distinct 2D points: \(A(x_1, y_1)\) and \(B(x_2, y_2)\). To determine which side of the line a vertex falls on, use the 2D cross product (determinant):
\[\text{side}(P, A, B) = (B.x - A.x)(P.y - A.y) - (B.y - A.y)(P.x - A.x)\]
- A positive value places the point on one side of the line.
- A negative value places the point on the opposite side.
- A zero value means the point lies directly on the cutting line.
2. Find Edge Intersections
Iterate through the perimeter edges of the body. An edge connects vertex \(V_i\) to vertex \(V_{i+1}\) (wrapping around to the first vertex). An intersection exists if \(V_i\) and \(V_{i+1}\) have opposite signs when evaluated against the cutting line equation.
Calculate the exact intersection point \(I\) between segment \(V_i V_{i+1}\) and line \(AB\):
function getLineIntersection(p1, p2, p3, p4) {
const d = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
if (d === 0) return null; // Parallel lines
const ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / d;
return {
x: p1.x + ua * (p2.x - p1.x),
y: p1.y + ua * (p2.y - p1.y)
};
}3. Reconstruct the Polygons
To build the two new shapes, traverse the original vertices in
sequence and construct two separate vertex arrays (polyA
and polyB):
- Initialize empty arrays
polyAandpolyB. - For each vertex \(V_i\):
- Classify \(V_i\) using \(\text{side}(V_i, A, B)\). If positive, push
it to
polyA; if negative, push it topolyB. - Check if the segment connecting \(V_i\) to \(V_{i+1}\) crosses the cutting line.
- If a cut occurs, compute intersection point \(I\) and push a copy of \(I\) to both
polyAandpolyB.
- Classify \(V_i\) using \(\text{side}(V_i, A, B)\). If positive, push
it to
- Ensure both resulting arrays have at least 3 vertices. If a cut did not fully pass through two separate edges, the body was not cleanly split.
4. Replace the Original Body in Matter.js
Once two valid sets of vertices are formed:
- Calculate Geometric Centers: Matter.js expects body
positions at their center of mass. Use
Matter.Vertices.centre(vertices)to find the centroid of each new polygon, and offset the vertices relative to this center before creating the bodies. - Create New Bodies: Use
Matter.Bodies.fromVertices()to instantiate the two new halves. Note that Matter.js relies on thepoly-decomplibrary internally if the sliced geometry produces concave shapes. - Inherit Physics Properties: Assign the original body's velocity, angular velocity, friction, and restitution to the new bodies so the motion remains continuous.
- Update the World: Remove the parent body from the composite world and add the two new halves.
function sliceBody(world, body, lineStart, lineEnd) {
const vertices = body.vertices;
const polyA = [];
const polyB = [];
for (let i = 0; i < vertices.length; i++) {
const current = vertices[i];
const next = vertices[(i + 1) % vertices.length];
const currentSide = (lineEnd.x - lineStart.x) * (current.y - lineStart.y) -
(lineEnd.y - lineStart.y) * (current.x - lineStart.x);
const nextSide = (lineEnd.x - lineStart.x) * (next.y - lineStart.y) -
(lineEnd.y - lineStart.y) * (next.x - lineStart.x);
if (currentSide >= 0) polyA.push({ x: current.x, y: current.y });
if (currentSide <= 0) polyB.push({ x: current.x, y: current.y });
if ((currentSide > 0 && nextSide < 0) || (currentSide < 0 && nextSide > 0)) {
const intersect = getLineIntersection(current, next, lineStart, lineEnd);
if (intersect) {
polyA.push({ x: intersect.x, y: intersect.y });
polyB.push({ x: intersect.x, y: intersect.y });
}
}
}
if (polyA.length < 3 || polyB.length < 3) return false;
const centerA = Matter.Vertices.centre(polyA);
const centerB = Matter.Vertices.centre(polyB);
const bodyA = Matter.Bodies.fromVertices(centerA.x, centerA.y, polyA, {
render: body.render
});
const bodyB = Matter.Bodies.fromVertices(centerB.x, centerB.y, polyB, {
render: body.render
});
if (!bodyA || !bodyB) return false;
Matter.Body.setVelocity(bodyA, body.velocity);
Matter.Body.setVelocity(bodyB, body.velocity);
Matter.Body.setAngularVelocity(bodyA, body.angularVelocity);
Matter.Body.setAngularVelocity(bodyB, body.angularVelocity);
Matter.Composite.remove(world, body);
Matter.Composite.add(world, [bodyA, bodyB]);
return true;
}5. Handling Concave Polygons
If slicing a shape results in concave polygons, ensure
poly-decomp.js is installed and registered globally on the
window object prior to loading Matter.js:
window.decomp = require('poly-decomp');Without poly-decomp,
Matter.Bodies.fromVertices() falls back to computing the
convex hull, which distorts complex sliced shapes.