FERRETGame engine over Babylon.js
Ferret

Media

Package @ferret/physics-media, capability name media, phase field (order 10).

What it is for#

Anything that moves through something. A medium is a substance with a density and a viscosity (and maybe a flow) that fills a place; a body in it feels

  • drag against the flow: Stokes' law at low Reynolds numbers, a quadratic law at high ones (Schiller–Naumann for a sphere), so a feather, a stone and a cannonball each fall as their size says;
  • buoyancy: ρ_medium · V_submerged · g from the exact submerged part of the body, so wood floats at a draft that depends on its density, lead sinks, a hull rights itself;
  • rotational drag.

Air is the default medium (density and temperature from an atmosphere by altitude), can have a wind with gusts, and can be switched off for vacuum. A body half in water and half in air feels the volume-weighted blend. It is not a fluid simulation: the medium is a property of a place. For flowing, splashing water use liquids.

The model#

medium at p  =  the dense medium whose region contains p (surface, zone, bounds)   else   air
body feels   =  blend of the media by the submerged fraction, with their flows
force        =  buoyancy (along gravity) + drag against (body velocity − flow)

A medium (MediumDef) is { name, density (kg/m³), viscosity (Pa·s), flow?, surface?, zone?, bounds?, gust? }:

FieldMeaning
surfacethe y of the top of the medium, or a function when it moves (a reservoir that fills and drains). Below it the medium is there
zonelimits the medium to a named zone of @ferret/zones
bounds{min, max} box the medium fills (a tank); outside it the medium is not there
flowvelocity of the medium, m/s: a current, a river
none of surface, zone, boundsfills everything

EXAMPLE_MEDIA has water, seawater, mud and honey to start from.

First use#

import { PhysicsHost, RigidBody } from '@ferret/physics';
import { gravity } from '@ferret/physics-gravity';
import { media, EXAMPLE_MEDIA, submergedFraction } from '@ferret/physics-media';

const m = media();
const host = world.addService(new PhysicsHost({ capabilities: [gravity(), m] }));
host.enable('gravity'); host.enable('media');
m.addMedium({ ...EXAMPLE_MEDIA.water, surface: 10 });                  // a lake with its top at y = 10

const ball = (name: string, x: number, density: number) => {
  const r = 0.5;
  const e = world.spawn(name, { position: new BABYLON.Vector3(x, 11, 0) });
  e.add('RigidBody', { shape: 'sphere', radius: r, mass: density * (4 / 3) * Math.PI * r ** 3, linearDamping: 4 });
  return e.get(RigidBody)!.body;
};
const wood = ball('wood', 0, 600), lead = ball('lead', 3, 3000);
world.run(12);
const draft = submergedFraction(wood.position[1], 0.5, 10);

Wood of density 600 settles with 60 % of its volume under: ρ_body / ρ_water, as Archimedes says. Lead sinks and falls at the terminal speed its drag allows.

Recipes#

Wind that gusts#

wind is a vector at a reference height (windRefHeight, 10 m); windExponent gives a power-law profile with height (0 = uniform); gust: { amplitude, period, seed? } adds a deterministic gust along it: the same seed gives the same gusts in every run and on every client.

import { PhysicsHost, RigidBody } from '@ferret/physics';
import { media, uniformAtmosphere } from '@ferret/physics-media';

const host = world.addService(new PhysicsHost({ capabilities: [media()] }));
host.kernel.gravity = [0, 0, 0];
host.enable('media');
host.config('media').set('world', 'atmosphere', uniformAtmosphere(1.2));
host.config('media').set('world', 'wind', [10, 0, 0]);
const leaf = world.spawn('leaf', { position: new BABYLON.Vector3(0, 5, 0) });
leaf.add('RigidBody', { shape: 'sphere', radius: 0.1, mass: 50 * (4 / 3) * Math.PI * 0.001 });
world.run(60);

A body in a flow is carried toward the flow's speed and never past it: drag acts on the relative velocity.

A river or a current#

m.addMedium({ ...EXAMPLE_MEDIA.water, surface: 100, flow: [0, 0, 2] }): anything in the water drifts toward 2 m/s along z.

Mud, honey and tar pits#

Same call with a different medium: EXAMPLE_MEDIA.mud (1800 kg/m³, 0.5 Pa·s) or honey (10 Pa·s). The high viscosity gives Stokes drag: a small sphere sinks at 2 r² (ρs − ρ) g / (9 μ), slowly. Limit a bog to a place with zone: 'bog'. For soil that deforms, sinks under load and floods, use terrain; mud as a medium is drag and buoyancy only.

Walking, wading and swimming#

The character controller samples the medium at the character: air changes nothing, water slows a runner to what its push can do. A level that puts water with a surface over a pool therefore makes wading work with no code.

Vacuum and other planets#

host.config('media').set('world', 'air', false) makes a vacuum: no drag at all, a rock keeps its speed. Another planet's air is an Atmosphere: standardAtmosphere (the International Standard Atmosphere up to 47 km), exponentialAtmosphere(seaLevelDensity, scaleHeight, temperature) for a game world, uniformAtmosphere(density) for a closed hall, vacuum. seaLevel is the y of altitude zero.

A closed volume of gas#

GasVolume is a bounded amount of gas obeying p·V = n·R·T: a balloon envelope, an air pocket in a ballast tank, a diving bell. It is one pressure and one temperature for the whole volume, not a flow.

import { GasVolume, standardAtmosphere } from '@ferret/physics-media';

const g = GasVolume.ofAir(standardAtmosphere, 0, 2);        // 2 m³ of sea-level air
const p0 = g.pressure, T0 = g.temperature;
g.heat(g.cv * 30);                                           // +30 K at constant volume
const hot = g.pressure;
const squeezed = GasVolume.ofAir(standardAtmosphere, 0, 2);
squeezed.setVolume(1, 'adiabatic');                          // halve the volume without letting the heat out

heat(joules, 'volume' | 'pressure'), setVolume(v, 'isothermal' | 'adiabatic'), addMoles(dn) (a leak lowers the pressure). Vessels use gas volumes for ballast and buoyancy; see vehicles.

Parameters#

ValueDefaultEffect
airtrueair is a medium; false = vacuum
atmospherestandardAtmospheredensity, pressure, temperature by altitude
seaLevel0y of altitude zero
wind[0,0,0]m/s at windRefHeight
windRefHeight, windExponent10, 0power-law profile; 0 = the same wind at every height
gustnone{ amplitude, period, seed? } along the wind
media[]list of MediumDef; addMedium / removeMedium edit it
cdby Reynolds numbera constant drag coefficient instead of the law; an entity may set its own
angularDragtruerotational drag
samples4how many points a body is sampled at when it is part in, part out of a medium

All scoped (world, level, zone, entity), so one room can be windy and one boat can be draggy.

Commands#

media.add {name, density, viscosity, flow?, surface?, zone?}, media.remove {name}, media.at {position} (the medium there), atmosphere.at {altitude} (density, pressure, temperature), wind.set {vector, gust?, scope?}.

Interplay#

  • gravity gives the direction and strength buoyancy acts against; with it off, the kernel's constant is used.
  • terrain reports terrain:flooded when a dig reaches the water table. It does not add a medium by itself: put a water medium over the flooded area (a surface at the water table's height) so things float in it.
  • Vessels (vehicles) compute their own weight and buoyancy from hull volume and are marked exempt from gravity, media and surface; read vehicles for how the water they sit in acts on them.
  • Characters read fields.medium and fields.column (how much of a vertical segment stands in a dense medium) to wade and swim.

Budget#

A medium lookup plus a few samples per body per step. Cost grows with the number of bodies in a dense medium, not with the number of media. A hundred bodies in a lake is cheap; if the step runs long, lower samples.

Debugging#

SymptomCause
A body sinks that should floatits density is above the medium's (mass / volume): check the mass against the shape and the units
It floats but bobs foreverno damping: water drag damps it only as long as it moves; give linearDamping for a crude fix, or let the quadratic drag work (do not set cd to 0)
Nothing happens in the watermedia is not enabled, or the body is not inside the medium's region: media.at at the body's position tells which medium it is in
A pool medium is everywherewith neither surface, zone nor bounds a medium fills everything: give it a place
Wind does nothing in a closed roombounds or a zone limit the dense media; the wind is air's: set wind in the room's zone scope
A body in a vacuum keeps its speedthat is correct: air: false removes drag entirely

Reference#

media, MediaCapability, MediumDef, MEDIA_DEFAULTS, EXAMPLE_MEDIA, submergedFraction, sphereCd, gustAt, standardAtmosphere, exponentialAtmosphere, GasVolume, @ferret/physics-media.