Model Sailing Boat Physics in Matter.js
This article explains how to model realistic sailing boat physics using the 2D rigid-body engine Matter.js. By breaking down the simulation into distinct components—calculating apparent wind, resolving aerodynamic lift and drag on the sail, modeling the hydrodynamic resistance and lift generated by the keel, and applying these forces to a Matter.js body—you can simulate authentic sailing dynamics, including tacking, reaching, and leeway drift.
1. Representing the Boat as a Rigid Body
In Matter.js, the boat can be represented as a single composite rigid body or a rectangular body with low air and water friction. Standard friction parameters in physics engines do not distinguish between longitudinal (forward) and lateral (sideways) movement, so you should minimize default friction and apply directional hydrodynamic forces manually.
const boat = Matter.Bodies.rectangle(x, y, 20, 60, {
angle: 0,
mass: 1000,
frictionAir: 0.005 // Minimal base damping
});
Matter.World.add(world, boat);2. Calculating the Apparent Wind
Boats react to the apparent wind—the wind vector experienced by the moving vessel—rather than the true wind. Subtract the boat's velocity vector from the true wind vector to determine the apparent wind.
const apparentWind = {
x: trueWind.x - boat.velocity.x,
y: trueWind.y - boat.velocity.y
};
const apparentSpeed = Math.hypot(apparentWind.x, apparentWind.y);
const apparentAngle = Math.atan2(apparentWind.y, apparentWind.x);3. Calculating Aerodynamic Forces on the Sail
The sail generates aerodynamic lift and drag based on the angle of attack (\(\alpha\)), which is the difference between the sail's angle and the apparent wind direction.
- Angle of Attack (\(\alpha\)): \(\alpha = \text{apparentAngle} - \text{sailAngle}\)
- Lift and Drag Coefficients: Approximate
thin-airfoil theory using sinusoidal curves:
- Lift: \(C_L = 2 \cdot \sin(2\alpha)\)
- Drag: \(C_D = 1 - \cos(2\alpha) + C_{D,\text{base}}\)
- Magnitude: Both forces scale with the square of the apparent wind speed: \(\text{Force} = 0.5 \cdot \rho_{\text{air}} \cdot \text{Area} \cdot v_{\text{apparent}}^2 \cdot C\)
Lift acts perpendicular to the apparent wind vector, while drag acts parallel to it. Combine these components to find the total sail force vector.
const alpha = apparentAngle - (boat.angle + sailTrimAngle);
// Coefficients
const Cl = 1.5 * Math.sin(2 * alpha);
const Cd = 0.5 * (1 - Math.cos(2 * alpha)) + 0.1;
const dynamicPressure = 0.5 * airDensity * sailArea * apparentSpeed * apparentSpeed;
const liftMagnitude = Cl * dynamicPressure;
const dragMagnitude = Cd * dynamicPressure;
// Vectors
const liftVector = {
x: -Math.sin(apparentAngle) * liftMagnitude,
y: Math.cos(apparentAngle) * liftMagnitude
};
const dragVector = {
x: Math.cos(apparentAngle) * dragMagnitude,
y: Math.sin(apparentAngle) * dragMagnitude
};
const totalSailForce = {
x: liftVector.x + dragVector.x,
y: liftVector.y + dragVector.y
};4. Simulating the Keel and Hydrodynamic Lift
Without a keel, the lateral component of the sail force pushes the boat sideways (leeway). The keel acts as an underwater wing, generating hydrodynamic lift opposite to the leeway drift and allowing the boat to translate lateral sail force into forward motion.
- Determine Boat Velocity Components: Decompose the boat's velocity into forward (surge) and lateral (sway) velocities relative to the boat's heading.
- Calculate Leeway Angle: The drift angle through the water serves as the keel's angle of attack: \(\beta = \text{atan2}(v_{\text{lateral}}, v_{\text{forward}})\)
- Generate Keel Lift: The keel creates a corrective lateral force proportional to forward speed and leeway angle: \(F_{\text{keel, lateral}} = -C_{\text{keel}} \cdot v_{\text{forward}} \cdot v_{\text{lateral}}\)
- Water Drag: Apply forward resistance proportional to the square of forward speed to limit top speed.
const forwardHeading = { x: Math.cos(boat.angle), y: Math.sin(boat.angle) };
const lateralHeading = { x: -Math.sin(boat.angle), y: Math.cos(boat.angle) };
// Velocity projections
const vForward = boat.velocity.x * forwardHeading.x + boat.velocity.y * forwardHeading.y;
const vLateral = boat.velocity.x * lateralHeading.x + boat.velocity.y * lateralHeading.y;
// Keel forces
const lateralResistanceForce = -vLateral * keelLiftCoefficient * Math.abs(vForward);
const forwardHullDrag = -0.5 * waterDensity * hullArea * vForward * Math.abs(vForward) * hullDragCoefficient;
const totalWaterForce = {
x: lateralHeading.x * lateralResistanceForce + forwardHeading.x * forwardHullDrag,
y: lateralHeading.y * lateralResistanceForce + forwardHeading.y * forwardHullDrag
};5. Applying Forces in the Matter.js Update Loop
Hook into Matter.js's beforeUpdate event to calculate
and apply the net forces on each tick.
Matter.Events.on(engine, 'beforeUpdate', () => {
// 1. Calculate sail and keel forces
// (Using the logic detailed above)
// 2. Combine forces
const totalForce = {
x: totalSailForce.x + totalWaterForce.x,
y: totalSailForce.y + totalWaterForce.y
};
// 3. Apply force at center of mass
Matter.Body.applyForce(boat, boat.position, totalForce);
// 4. Steering (Rudder)
const rudderTorque = -rudderAngle * vForward * rudderEffectiveness;
boat.torque = rudderTorque;
});By balancing sail lift against keel lift, the boat naturally achieves realistic sailing behaviors: accelerating on a beam reach, beating upwind at an angle to the true wind, and stalling when pointing directly into the wind (in irons).