Build a Submarine with Ballast Tanks in Matter.js
This guide explains how to construct a controllable submarine in the Matter.js physics engine using realistic buoyancy, hydrodynamic drag, and adjustable ballast tanks. Because Matter.js is a rigid-body engine without built-in fluid dynamics, you must simulate the interaction between water and the vessel manually. By dynamically adjusting the contents of virtual ballast tanks and applying buoyant and damping forces on every physics tick, you can achieve smooth diving, surfacing, and neutral buoyancy.
1. The Core Physics Mechanics
To simulate a submarine, you need to balance two primary vertical forces and counteract kinetic energy using fluid resistance:
- Gravity: Handled automatically by the Matter.js engine pulling the body downward based on its mass (\(F = m \cdot g\)).
- Buoyancy: An upward force proportional to the volume of fluid displaced by the submerged portion of the hull (Archimedes' principle).
- Ballast Control: Adding or removing mass (or modifying upward buoyant force) to switch between positive buoyancy (surfacing), neutral buoyancy (hovering), and negative buoyancy (diving).
- Fluid Drag: Resistance that slows both linear and rotational movement inside the fluid to prevent unnatural bobbing.
2. Creating the Submarine Body
Using a compound body allows you to simulate separate fore and aft ballast tanks, giving you realistic pitch control alongside depth control.
const { Bodies, Body, Composite } = Matter;
// Submarine dimensions
const hullWidth = 160;
const hullHeight = 40;
// Main hull
const hull = Bodies.rectangle(400, 300, hullWidth, hullHeight, {
density: 0.001,
frictionAir: 0.02
});
const submarine = Body.create({
parts: [hull],
frictionAir: 0.02
});
// Ballast tank state (values from 0.0 = empty to 1.0 = fully flooded)
submarine.ballast = {
tankLevel: 0.5, // 0.5 represents neutral buoyancy
maxCapacity: 0.0008 // Force modifier
};
Composite.add(engine.world, submarine);3. Calculating Submergence and Buoyancy
Define a vertical coordinate in your world representing the water
surface line (waterLineY). On every engine update,
determine how much of the submarine is below this line.
const waterLineY = 250;
const waterDensity = 0.0015;
function getSubmergedRatio(body, waterY) {
const top = body.bounds.min.y;
const bottom = body.bounds.max.y;
if (bottom <= waterY) return 0; // Fully above water
if (top >= waterY) return 1; // Fully submerged
return (bottom - waterY) / (bottom - top);
}4. Applying Ballast and Hydrodynamic Forces
Hook into the beforeUpdate event of your engine. In this
loop, calculate the displacement, compute the upward buoyant force
against the submarine's mass and current ballast level, and apply
opposing drag forces.
Matter.Events.on(engine, 'beforeUpdate', () => {
const submergedRatio = getSubmergedRatio(submarine, waterLineY);
if (submergedRatio > 0) {
// 1. Calculate Base Buoyant Force
const displacedMass = submarine.mass * submergedRatio;
const gravity = engine.gravity.scale * engine.gravity.y;
let buoyantForceMagnitude = displacedMass * gravity;
// 2. Factor in Ballast Tanks
// A lower tank level produces net positive buoyancy; higher produces negative buoyancy
const ballastOffset = (0.5 - submarine.ballast.tankLevel) * submarine.ballast.maxCapacity;
const netUpwardForce = buoyantForceMagnitude + (ballastOffset * submergedRatio);
// Apply buoyant force at the center of mass
Body.applyForce(submarine, submarine.position, {
x: 0,
y: -netUpwardForce
});
// 3. Fluid Drag (Linear and Angular Damping)
const dragFactor = 0.05 * submergedRatio;
Body.applyForce(submarine, submarine.position, {
x: -submarine.velocity.x * dragFactor * submarine.mass * 0.01,
y: -submarine.velocity.y * dragFactor * submarine.mass * 0.01
});
// Angular resistance to stabilize rotation underwater
submarine.torque -= submarine.angularVelocity * 0.15 * submergedRatio;
}
});5. Controlling Ballast to Submerge and Surface
To submerge, open the flood valves to increase the ballast level. To surface, blow the tanks with compressed air to decrease the level.
// Flood ballast (Submerge)
function floodBallast(rate = 0.01) {
submarine.ballast.tankLevel = Math.min(1.0, submarine.ballast.tankLevel + rate);
}
// Blow ballast (Surface)
function blowBallast(rate = 0.01) {
submarine.ballast.tankLevel = Math.max(0.0, submarine.ballast.tankLevel - rate);
}
// Example keybindings
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') floodBallast();
if (e.key === 'ArrowUp') blowBallast();
});6. Tuning for Realism
- Adjust Tank Volume: Modify
maxCapacityto change how aggressively the submarine climbs or dives. - Split Tanks for Pitch: Divide the ballast state
into
bowTankandsternTankvariables and apply buoyancy forces at separate offset points along the hull to tip the submarine up or down. - Water Surface Transition: Multiplying drag and
buoyancy directly by
submergedRatioensures the submarine breaks the surface smoothly without erratic oscillation.