Simulate Boat Water Resistance in Matter.js
This article explains how to realistically simulate water resistance and boat deceleration in Matter.js using custom fluid dynamics logic. Because Matter.js is a rigid-body 2D physics engine without built-in fluid mechanics, developers must approximate hydrodynamic drag and hull displacement manually. Below, you will learn the core mathematical principles behind fluid resistance, how to calculate the submerged volume of a hull, and how to apply custom counteracting forces to a boat body on every physics update step.
Understanding the Physics of Water Drag
Water resistance relies on hydrodynamic drag, which opposes the relative motion of any object moving through a fluid. The standard drag equation is:
\[F_d = \frac{1}{2} \rho v^2 C_d A\]
Where:
- \(\rho\) (rho): Fluid density (water is significantly denser than air).
- \(v\): Velocity of the body relative to the fluid.
- \(C_d\): Drag coefficient (determined by hull shape).
- \(A\): Frontal reference area (directly related to submerged displacement).
In 2D physics engines like Matter.js, displacement is represented by the cross-sectional area or the vertical immersion depth of the hull intersecting with a flat water plane.
Calculating Hull Displacement
To simulate displacement, define a global water line coordinate (\(Y_{water}\)). On each tick, determine how far the hull's bounding box or vertices extend below this threshold.
A fast and effective approximation uses the boat's vertical bounds:
function getSubmergedRatio(boat, waterLevelY) {
const minY = boat.bounds.min.y;
const maxY = boat.bounds.max.y;
const totalHeight = maxY - minY;
// Entirely above water
if (maxY <= waterLevelY) {
return 0;
}
// Entirely submerged
if (minY >= waterLevelY) {
return 1;
}
// Partially submerged
const submergedHeight = maxY - waterLevelY;
return Math.min(Math.max(submergedHeight / totalHeight, 0), 1);
}Applying Drag in the Physics Loop
Matter.js provides lifecycle hooks to modify forces prior to
collision resolution. Hook into the beforeUpdate event to
evaluate velocity, determine current displacement, and apply an opposing
drag force to the body.
const { Engine, Events, Body, Vector } = Matter;
const WATER_LEVEL = 300;
const WATER_DENSITY = 0.002;
const LINEAR_DRAG_COEF = 0.05;
const ANGULAR_DRAG_COEF = 0.08;
Events.on(engine, 'beforeUpdate', () => {
const submergedRatio = getSubmergedRatio(boat, WATER_LEVEL);
if (submergedRatio <= 0) {
return; // Boat is out of the water
}
const velocity = boat.velocity;
const speed = Vector.magnitude(velocity);
if (speed > 0.0001) {
// Drag increases quadratically with speed and linearly with submerged hull area
const dragMagnitude = 0.5 * WATER_DENSITY * (speed * speed) * LINEAR_DRAG_COEF * submergedRatio;
// Normalize velocity and invert to oppose motion
const dragVector = Vector.mult(Vector.normalise(velocity), -dragMagnitude);
// Apply drag force at the center of mass
Body.applyForce(boat, boat.position, dragVector);
}
// Dampen angular velocity based on water depth
const angularDrag = boat.angularVelocity * ANGULAR_DRAG_COEF * submergedRatio;
Body.setAngularVelocity(boat, boat.angularVelocity - angularDrag);
});Directional Drag: Longitudinal vs. Lateral Resistance
Real boats cut through water efficiently when moving forward, but experience intense resistance when sliding sideways. To simulate a keel or rudder, decompose the linear drag into forward (longitudinal) and sideways (lateral) components.
- Forward Vector: Calculate the unit vector
representing the boat's heading:
[Math.cos(boat.angle), Math.sin(boat.angle)]. - Perpendicular Vector: Calculate the normal unit
vector:
[-Math.sin(boat.angle), Math.cos(boat.angle)]. - Decompose Velocities: Use the dot product to split
boat.velocityinto parallel and perpendicular speeds. - Apply Separate Coefficients: Apply a small drag coefficient to parallel velocity and a substantially higher drag coefficient to perpendicular velocity. This creates realistic steering behavior where the boat drifts minimally and naturally glides forward.