Calculate Fluid Overlap Area in Matter.js
Accurately simulating buoyancy or hydrodynamic drag in Matter.js requires calculating the exact portion of a rigid body submerged beneath a fluid surface. This guide explains how to compute the surface area overlap between an arbitrary 2D Matter.js body and a horizontal fluid plane by clipping the body's polygonal vertices against the fluid line and calculating the area of the resulting submerged polygon using the Shoelace formula.
The Challenge with Matter.js
Matter.js provides collision detection using the Separating Axis Theorem (SAT), allowing you to identify when a body overlaps with a fluid sensor body. However, the engine only returns collision penetration vectors and contact points, not the intersecting polygon or its area. To determine the submerged area, you must compute the intersection geometry directly.
Step 1: Extract the Body's Vertices
Every rigid body in Matter.js contains an array of vertices transformed into world space coordinates. You can access these points directly:
const vertices = body.vertices; // Array of { x: number, y: number }Ensure the vertices form a closed polygon defined in a consistent winding order (Matter.js vertices are ordered clockwise).
Step 2: Clip the Polygon Against the Fluid Line
Assuming a horizontal fluid plane defined by a specific vertical
coordinate (fluidLevelY), the submerged region corresponds
to the half-plane where y >= fluidLevelY (in Matter.js,
the positive Y-axis points downward).
You can use a simplified version of the Sutherland-Hodgman polygon clipping algorithm to clip the body's vertices against this single line:
function getSubmergedVertices(vertices, fluidLevelY) {
const submerged = [];
const count = vertices.length;
for (let i = 0; i < count; i++) {
const current = vertices[i];
const next = vertices[(i + 1) % count];
const currentInside = current.y >= fluidLevelY;
const nextInside = next.y >= fluidLevelY;
if (currentInside) {
submerged.push(current);
}
// If edge crosses the fluid line, calculate the intersection point
if (currentInside !== nextInside) {
const dy = next.y - current.y;
const t = (fluidLevelY - current.y) / dy;
const intersectionX = current.x + t * (next.x - current.x);
submerged.push({ x: intersectionX, y: fluidLevelY });
}
}
return submerged;
}Step 3: Compute the Submerged Area
Once you obtain the clipped polygon vertices, calculate its area using the Shoelace formula (Gauss's area formula):
function calculatePolygonArea(vertices) {
const count = vertices.length;
if (count < 3) return 0;
let area = 0;
for (let i = 0; i < count; i++) {
const current = vertices[i];
const next = vertices[(i + 1) % count];
area += (current.x * next.y) - (next.x * current.y);
}
return Math.abs(area) * 0.5;
}Full Integration
Combine these steps inside your engine's beforeUpdate
event to dynamically evaluate buoyant forces based on the overlap:
Matter.Events.on(engine, 'beforeUpdate', () => {
const fluidLevelY = 300;
// 1. Clip polygon below fluid level
const submergedVertices = getSubmergedVertices(boxBody.vertices, fluidLevelY);
// 2. Compute area of the overlap
const submergedArea = calculatePolygonArea(submergedVertices);
// 3. Apply hydrostatic force if submerged
if (submergedArea > 0) {
const fluidDensity = 0.001;
const gravity = engine.gravity.y * engine.gravity.scale;
const buoyancyForceMagnitude = submergedArea * fluidDensity * gravity;
// Apply force upward at the body's center of mass or submerged centroid
Matter.Body.applyForce(boxBody, boxBody.position, {
x: 0,
y: -buoyancyForceMagnitude
});
}
});For non-horizontal fluid lines (such as waves or angled planes), substitute the 1D linear interpolation in the clipping function with a standard line-segment intersection test against the fluid boundary.