Implementing Buoyancy in Ammo.js Physics
This article explores how the ammo.js physics library
handles experimental buoyancy and basic fluid interactions. Because
ammo.js is a direct JavaScript port of the C++ Bullet
Physics engine, it focuses primarily on rigid body dynamics and does not
feature a built-in, real-time fluid solver. Instead, developers simulate
buoyancy and fluid drag by combining collision triggers, calculating
submerged volumes, and manually applying custom force vectors to rigid
bodies.
The Core Architecture of Ammo.js Fluids
Standard builds of ammo.js do not include active
Smoothed Particle Hydrodynamics (SPH) or native Navier-Stokes solvers
due to performance constraints in browser environments. Instead, the
library relies on approximation methods. To simulate an object floating
in water, developers must manually bridge the gap between collision
detection and custom force application.
This approximation is achieved through a three-step pipeline: detection, force calculation, and damping.
Step 1: Fluid Volume Detection (Ghost Objects)
To simulate fluid, you must define the boundaries of the liquid. In
ammo.js, this is represented by a sensor volume or a “Ghost
Object” (btGhostObject).
- Sensor Setup: A collision shape (usually a box or
cylinder) is designated as a trigger by setting its collision flags to
CF_NO_CONTACT_RESPONSE. This allows other rigid bodies to pass through it without bouncing off the surface. - Overlap Detection: During the physics world step, the engine checks for overlapping pairs. If a dynamic rigid body intersects with the ghost object representing the fluid, it triggers the buoyancy calculations.
Step 2: Applying Buoyancy Forces
Once a rigid body is detected inside the fluid volume, you must manually apply an upward force to counteract gravity. This relies on Archimedes’ principle: the upward buoyant force is equal to the weight of the fluid displaced by the object.
To implement this in ammo.js:
- Calculate Submergence Depth: Determine how far below the fluid’s surface plane the rigid body’s center of mass is located.
- Determine Displaced Volume: Calculate a scalar multiplier based on how much of the object is submerged (e.g., 0% at the surface, 100% when fully underwater).
- Apply Central Force: Apply an upward force vector
using the
applyCentralForce()method on the rigid body.
The formula used in the physics tick callback looks like this:
\[\vec{F}_{buoyancy} = \vec{u} \times (\rho \times V_{submerged} \times g)\]
Where \(\vec{u}\) is the upward vector, \(\rho\) is the fluid density, \(V_{submerged}\) is the volume submerged, and \(g\) is the gravity constant.
Step 3: Simulating Fluid Drag and Viscosity
An upward force alone will cause a floating object to bob up and down indefinitely. To make the interaction realistic, you must simulate water resistance (drag).
ammo.js handles this through damping properties. When an
object enters the fluid volume, you must dynamically increase its linear
and angular damping factors using:
rigidBody.setDamping(linearDamping, angularDamping);- Linear Damping: Simulates the fluid resistance slowing down the object’s travel speed.
- Angular Damping: Simulates rotational resistance, stopping the object from spinning endlessly underwater.
When the object exits the fluid volume, these values must be restored to their default atmospheric settings.
Experimental Soft Body and Particle Approaches
For visual fluid interactions, developers sometimes leverage the
experimental soft body dynamics in ammo.js
(btSoftBody). Deformable meshes can be configured with low
stiffness to mimic gelatinous surfaces or basic soft-body fluid
envelopes.
Alternatively, SPH particle simulations can be managed in external
JavaScript code, using ammo.js rigid bodies as kinematic
colliders that push particles aside. However, for standard gameplay and
physics interactions, the rigid body trigger-and-force method remains
the most performant and reliable choice.