Directional Hydrodynamic Drag in Matter.js

This guide explains how to implement realistic hydrodynamic drag that opposes a body's velocity vector in Matter.js. While Matter.js includes a basic isotropic linear resistance via frictionAir, simulating fluid resistance requires calculating a force proportional to the square of velocity directed strictly opposite the movement vector, optionally factoring in the body's orientation relative to its flow direction.

Understanding Matter.js Air Friction Limitations

Matter.js provides a built-in property on rigid bodies called frictionAir. This property applies a linear drag force calculated as:

\[F = -v \cdot \text{frictionAir}\]

In real-world hydrodynamics, fluid drag is typically quadratic and depends on velocity squared, fluid density, cross-sectional area, and the drag coefficient:

\[F_d = \frac{1}{2} \rho v^2 C_d A\]

Because frictionAir lacks directional awareness and quadratic scaling, you must disable or minimize it (frictionAir: 0) and manually apply opposing forces during the physics engine update cycle.

Implementing Quadratic Drag Opposing Velocity

To apply hydrodynamic drag opposing the movement vector, hook into the beforeUpdate event of the Matter.js engine. Calculate the body's current speed, determine the opposing normalized velocity vector, compute the quadratic force magnitude, and apply it to the body's center of mass using Matter.Body.applyForce.

const { Engine, Events, Body, Vector } = Matter;

// Drag coefficient constant (combines fluid density, drag coefficient, and area)
const FLUID_DRAG_COEFFICIENT = 0.005;

Events.on(engine, 'beforeUpdate', () => {
    const bodies = composite.bodies; // or a specific subset of bodies submerged in fluid

    for (let i = 0; i < bodies.length; i++) {
        const body = bodies[i];
        
        // Skip static bodies
        if (body.isStatic) continue;

        const velocity = body.velocity;
        const speedSquared = Vector.magnitudeSquared(velocity);

        // Avoid calculations if the body is nearly at rest
        if (speedSquared < 0.0001) continue;

        const speed = Math.sqrt(speedSquared);

        // Calculate drag force magnitude: F = k * v^2
        const dragMagnitude = FLUID_DRAG_COEFFICIENT * speedSquared;

        // Normalized vector directly opposing the velocity
        const opposingDirection = {
            x: -velocity.x / speed,
            y: -velocity.y / speed
        };

        // Resulting drag force vector
        const dragForce = {
            x: opposingDirection.x * dragMagnitude,
            y: opposingDirection.y * dragMagnitude
        };

        // Apply force opposing the movement vector at the body's center of mass
        Body.applyForce(body, body.position, dragForce);
    }
});

Adding Heading-Dependent Directional Resistance

Elongated objects (such as boats, arrows, or fins) experience significantly less drag when moving parallel to their forward heading than when moving sideways. You can extend the opposing force calculation by decomposing the velocity vector into local longitudinal (forward) and lateral (sideways) components.

1. Define Directional Coefficients

Assign different drag coefficients based on local axes:

const bodyConfig = {
    dragForward: 0.001,  // Streamlined profile moving forward/backward
    dragLateral: 0.02    // High resistance moving broadside
};

2. Project Velocity onto Body Axes

Rotate the global velocity vector into the body's local coordinate space using the body's current angle, compute the drag for each axis separately, and transform the combined opposing force back into world space:

Events.on(engine, 'beforeUpdate', () => {
    const angle = body.angle;
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);

    // Transform world velocity to local velocity
    const localVx = cos * body.velocity.x + sin * body.velocity.y;
    const localVy = -sin * body.velocity.x + cos * body.velocity.y;

    // Calculate opposing quadratic forces in local space
    const localForceX = -Math.sign(localVx) * Math.pow(localVx, 2) * bodyConfig.dragForward;
    const localForceY = -Math.sign(localVy) * Math.pow(localVy, 2) * bodyConfig.dragLateral;

    // Transform local opposing forces back to world coordinates
    const worldForce = {
        x: cos * localForceX - sin * localForceY,
        y: sin * localForceX + cos * localForceY
    };

    Body.applyForce(body, body.position, worldForce);
});

Angular Hydrodynamic Damping

Linear movement is not the only motion restricted by fluid; angular rotation is also dampened. Set body.frictionAir to zero and apply angular damping alongside linear drag:

const ANGULAR_DRAG = 0.05;

// Within the beforeUpdate loop:
const angularSpeed = Math.abs(body.angularVelocity);
if (angularSpeed > 0.0001) {
    const opposingTorque = -Math.sign(body.angularVelocity) * Math.pow(body.angularVelocity, 2) * ANGULAR_DRAG;
    body.torque += opposingTorque;
}

Preventing Numerical Instability

Quadratic drag increases rapidly at high velocities. If the calculated drag force exceeds the body's current momentum within a single update step (delta), the body will oscillate or reverse direction unnaturally. Clamp the maximum applied drag force to the body's current momentum over time:

const maxForce = (body.mass * speed) / engine.timing.lastDelta;
const appliedMagnitude = Math.min(dragMagnitude, maxForce);

const clampedDragForce = {
    x: opposingDirection.x * appliedMagnitude,
    y: opposingDirection.y * appliedMagnitude
};

Body.applyForce(body, body.position, clampedDragForce);