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);