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:

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.

  1. Forward Vector: Calculate the unit vector representing the boat's heading: [Math.cos(boat.angle), Math.sin(boat.angle)].
  2. Perpendicular Vector: Calculate the normal unit vector: [-Math.sin(boat.angle), Math.cos(boat.angle)].
  3. Decompose Velocities: Use the dot product to split boat.velocity into parallel and perpendicular speeds.
  4. 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.