Computing Center of Buoyancy in Matter.js
This article explains how to compute the center of buoyancy for partially submerged, irregular 2D polygons using Matter.js. Simulating realistic floating behavior requires finding the geometric intersection between a body and a fluid surface, calculating the centroid and area of the resulting submerged polygon, and applying the corresponding hydrostatic force at that exact point during each physics engine update tick.
1. The Buoyancy Principle in 2D Rigid Bodies
Buoyancy acts upward through the center of volume (in 2D, the center of area) of the displaced fluid. When an irregular polygon is partially submerged:
- The displaced volume corresponds to the area of the polygon below the waterline.
- The center of buoyancy (\(C_b\)) is the centroid of this submerged sub-polygon.
- Applying the upward buoyant force at \(C_b\) rather than the body's center of mass naturally produces the correct stabilizing or destabilizing torque.
2. Clipping the Polygon Against the Waterline
Assuming a horizontal water line defined by a constant depth \(y_{\text{water}}\) (where the positive Y-axis points downwards, following the Matter.js coordinate convention), the submerged region is determined by clipping the body's world vertices against the half-plane \(y \ge y_{\text{water}}\).
The Sutherland-Hodgman algorithm is the standard approach to clip a polygon against a plane:
function getSubmergedVertices(vertices, waterLevel) {
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 >= waterLevel;
const nextInside = next.y >= waterLevel;
if (currentInside && nextInside) {
submerged.push(next);
} else if (currentInside && !nextInside) {
submerged.push(getIntersection(current, next, waterLevel));
} else if (!currentInside && nextInside) {
submerged.push(getIntersection(current, next, waterLevel));
submerged.push(next);
}
}
return submerged;
}
function getIntersection(p1, p2, waterLevel) {
const t = (waterLevel - p1.y) / (p2.y - p1.y);
return {
x: p1.x + t * (p2.x - p1.x),
y: waterLevel
};
}3. Calculating the Submerged Area and Centroid
Once the submerged polygon vertices are computed, use the Shoelace formula to find its signed area and centroid coordinates:
function getCentroidAndArea(vertices) {
const count = vertices.length;
if (count < 3) return { area: 0, centroid: null };
let signedArea = 0;
let cx = 0;
let cy = 0;
for (let i = 0; i < count; i++) {
const p0 = vertices[i];
const p1 = vertices[(i + 1) % count];
const cross = (p0.x * p1.y) - (p1.x * p0.y);
signedArea += cross;
cx += (p0.x + p1.x) * cross;
cy += (p0.y + p1.y) * cross;
}
signedArea *= 0.5;
const factor = 1 / (6 * signedArea);
return {
area: Math.abs(signedArea),
centroid: {
x: cx * factor,
y: cy * factor
}
};
}4. Applying the Force in Matter.js
Hook into the beforeUpdate event of the Matter.js
engine. For every dynamic body:
- Retrieve the transformed vertices via
body.vertices. - Clip the vertices to extract the submerged polygon.
- Compute the area and centroid (\(C_b\)).
- Compute the buoyant force magnitude: \[F_b = \text{area} \times \rho \times g\] where \(\rho\) is the fluid density and \(g\) is gravitational acceleration.
- Apply drag to stabilize the object and prevent perpetual oscillation.
- Apply the force at the center of buoyancy.
Matter.Events.on(engine, 'beforeUpdate', () => {
const waterLevel = 300;
const fluidDensity = 0.001;
const gravity = engine.gravity.scale * engine.gravity.y;
bodies.forEach(body => {
const submergedVertices = getSubmergedVertices(body.vertices, waterLevel);
const { area, centroid } = getCentroidAndArea(submergedVertices);
if (area > 0 && centroid) {
// Upward buoyant force
const forceMagnitude = area * fluidDensity * gravity;
const buoyantForce = { x: 0, y: -forceMagnitude };
// Viscous damping / fluid drag
const linearDrag = {
x: -body.velocity.x * area * 0.0001,
y: -body.velocity.y * area * 0.0001
};
const angularDrag = -body.angularVelocity * 0.05;
// Apply forces to body
Matter.Body.applyForce(body, centroid, {
x: buoyantForce.x + linearDrag.x,
y: buoyantForce.y + linearDrag.y
});
body.torque += angularDrag;
}
});
});Using this method, when an irregular shape tilts, the geometric centroid of the submerged section dynamically shifts, automatically producing restorative torques and physically accurate equilibrium states.