Buoyant Force Proportional to Depth in Matter.js

This article explains how to simulate Archimedes' buoyant force proportional to submerged depth in Matter.js. Because Matter.js does not provide native fluid simulation, you must compute the submerged portion of a rigid body relative to a defined water line and manually apply an upward force using Matter.Body.applyForce during each engine update tick.

The Physics Principles

Archimedes' principle states that the buoyant force exerted on a body submerged in a fluid is equal to the weight of the fluid displaced by the body:

\[F_b = \rho \cdot V_{\text{submerged}} \cdot g\]

Where:

In a discrete 2D physics engine, we can approximate \(V_{\text{submerged}}\) by determining the ratio of the body that lies below the fluid surface line (\(y_{\text{surface}}\)).

Implementation Steps

  1. Define Fluid Properties: Establish a horizontal water surface line, fluid density, and linear fluid drag (viscosity) to dampen oscillations.
  2. Listen to Engine Updates: Register an event listener on Events.on(engine, 'beforeUpdate', callback).
  3. Calculate Submergence: Compare the body's bounding box against the water surface line to calculate the fraction of the body below water.
  4. Apply Force and Drag: Compute the net upward force proportional to the submerged fraction and apply opposing linear drag forces to simulate fluid resistance.

Code Example

Below is a complete implementation using Matter.js:

const { Engine, Render, Runner, Bodies, Composite, Events, Body, Vector } = Matter;

// 1. Initialize Matter.js environment
const engine = Engine.create();
const world = engine.world;

const render = Render.create({
  element: document.body,
  engine: engine,
  options: { width: 800, height: 600, wireframes: false }
});
Render.run(render);
Runner.run(Runner.create(), engine);

// 2. Define water line and fluid parameters
const waterLevel = 350; // Y-coordinate of fluid surface
const fluidDensity = 0.0015;
const fluidDrag = 0.05;

// 3. Create a floating body
const box = Bodies.rectangle(400, 100, 60, 60, {
  density: 0.001 // Less dense than fluid to allow floating
});
Composite.add(world, box);

// 4. Hook into beforeUpdate to compute buoyancy
Events.on(engine, 'beforeUpdate', () => {
  const bounds = box.bounds;
  const bodyHeight = bounds.max.y - bounds.min.y;

  // Check if body touches the water
  if (bounds.max.y > waterLevel) {
    // Calculate submerged depth clamped between 0 and the total height
    const submergedDepth = Math.min(bounds.max.y - waterLevel, bodyHeight);
    const submergedFraction = submergedDepth / bodyHeight;

    // Archimedes force: Proportional to displaced area/volume and gravity
    const gravity = engine.gravity.y * engine.gravity.scale;
    const displacedMass = (box.mass / box.density) * fluidDensity * submergedFraction;
    const buoyancyMagnitude = displacedMass * gravity;

    // Apply upward buoyant force at center of mass
    Body.applyForce(box, box.position, {
      x: 0,
      y: -buoyancyMagnitude
    });

    // Apply fluid resistance (linear drag) to stabilize motion
    const dragForce = {
      x: -box.velocity.x * fluidDrag * submergedFraction,
      y: -box.velocity.y * fluidDrag * submergedFraction
    };
    Body.applyForce(box, box.position, dragForce);

    // Apply angular drag to dampen rotation underwater
    box.torque -= box.angularVelocity * fluidDrag * submergedFraction * 1000;
  }
});

Key Considerations