FERRETGame engine over Babylon.js
Ferret

Terrain

Package @ferret/physics-terrain, capability terrain.

What it is for#

Ground that behaves. Dig a trench and its walls slump if the soil cannot stand. Pour sand and it settles into a cone. A pit below the water table floods. A heavy crate sinks into mud and stays on top of sand. A tunnel stands in clay and caves in through dry sand.

Use it when the player or an excavator changes the ground (digging, building, quarries, trenches), and for soft ground that grips and swallows. Do not use it for a static level surface: the ground of an ordinary level is a mesh.

Soil and strata#

A Terrain is a grid of columns. Each column is a stack of strata, bottom to top, each of some soil and some thickness. The surface is their sum.

A soil is a record of physical numbers:

FieldUnitWhat it means
densitykg/m³dry, in place
frictiondegreesinternal friction angle, which is also the angle of repose of the loose material
cohesionPawhat makes a cut stand (0 for dry sand and gravel)
hardnessJ/m³the energy needed to remove one cubic metre
bulkingnoneloose volume per volume in place; 1.3 means a 1 m³ hole in clay makes 1.3 m³ of heap
bearingPapressure the ground carries without sinking
sinkage{ k, n }beyond the bearing, how fast resistance grows as a body sinks
dampingPa·s/mhow slowly a body sinks
wet{ cohesion, density }the values when saturated; moisture 0..1 blends dry and wet

EXAMPLE_SOILS has sand, gravel, clay, silt, mud and rock to start from; a game defines its own. Heap is loose spoil, a stratum marked loose: it is not ground in place, and it settles at the angle of repose until it is compacted.

Everything conserves volume. What is dug ends up in the spoil, what is dumped goes into the heap, and compacting heap shrinks it by the bulking and reports how much volume vanished.

import { EXAMPLE_SOILS, SoilLibrary, Terrain } from '@ferret/physics-terrain';

const t = new Terrain({ nx: 81, nz: 81, cell: 0.1, origin: [0, 0],
                        soils: new SoilLibrary(EXAMPLE_SOILS), strata: [{ soil: 'clay', thickness: 3 }] });

const dug = t.dig({ footprint: { box: [3.95, 3.95, 4.95, 4.95] }, depth: 1, tool: { power: 1e9, reach: 1e9 }, dt: 1 });
t.dump([6, 6], dug.spoil);              // pour the heap somewhere else
t.compact({ box: [0, 0, 8, 8] });       // pack it into the ground

Digging#

A tool has a power in watts and a reach in J/m³. It removes power / hardness cubic metres per second. If the soil is harder than the tool's reach, nothing is removed and the result says blocked: true. A small shovel takes sand quickly and rock never; a pick with a low reach cannot break clay.

A DigResult has removed (m³), spoil (the heap you can dump), energy spent, blocked, flooded (how much water a pit needs to reach the water table) and slump (what the walls did afterwards).

Slopes and slumping#

After every edit the terrain relaxes: wherever a step between neighbouring columns cannot stand, soil moves from the higher to the lower, until everything stands. So:

  • poured sand ends as a cone whose flank is at its friction angle;
  • poured clay stands in a taller pile than sand of the same volume, as far as its cohesion allows;
  • a vertical cut in clay stands up to criticalHeight(soil, moisture) and slumps above it; dry sand never holds one; wet sand holds a cut as high as its wet cohesion allows (terrain.wet wets an area).

culmannHeight(soil, slopeDeg) gives the height at which a sloped face stands. Use these functions to give the player advice such as "this trench will fail at 1.6 m".

Water table and floods#

waterTable (a number, or a function of x and z) is the height of the ground water. A pit that goes below it reports flooded, the volume of water needed to fill it to the table, and the capability raises terrain:flooded { volume }. A column under the table counts as saturated, so it is heavier and less cohesive.

Terrain does not add water as a medium. Put a water medium (media) with its surface at the table's height over the pit to make things float in it.

Soft ground#

With sinkage: true (the default) a body sinks into soil it is too heavy for and is held by the soil's stiffness. The pressure under its footprint (m²; a sphere's cross-section if omitted) is compared with the soil's bearing: below it the ground carries the body, above it the body creeps down until the soil's resistance catches up. The creep is slow, because soil damping is large.

A 400 kg crate on a 0.25 m² face presses with about 16 kPa. That is above mud's 4 kPa bearing and well below sand's 100 kPa, so the crate sinks into mud and stays on sand.

import { terrain } from '@ferret/physics-terrain';

const cap = terrain(t);
host.register(cap);
host.enable('terrain');
cap.depthOf(crateBody);          // how far it has sunk

Tunnels#

t.tunnel({ from, to, radius, lined? }) bores a round tunnel along a line in world coordinates (y up) and checks that it can stand. A lined tunnel stands, and so does one in cohesive ground deep enough. In dry sand it caves in, and the surface above settles by the volume that fell (TunnelResult.subsided, stable: false). Cutting into bedrock throws. t.solidAt(x, y, z) answers "is there ground here", knowing the bores.

Letting bodies stand on it#

By default the terrain is read only by the sinkage law. Two other modes exist:

  • cap.collide('tunnels') turns the insides of the bores into static meshes for the kernel, so a ball dropped in a tunnel rests on its floor.
  • cap.collide('all') also builds the surface in tiles, so bodies rest on the ground, and the sinkage law then has nothing to act on. Dug tiles are rebuilt on terrain:changed.

Choose one world: soft ground that swallows things, or hard ground that holds them.

From an Unreal landscape#

LandscapeGrid (the height grid of a landscape in centimetres, with the dominant layer of each sample) becomes a terrain with a soil per layer. This is the path for levels made in Unreal; see porting from Unreal.

Parameters#

Capability valueDefaultEffect
sinkagetruebodies sink into soft ground and are held by it
footprinta sphere's cross-sectioncontact area of a body, m²
Terrain optionMeaning
nx, nz, cellcolumns and their spacing; 0.25–1 m for a play area; cost grows with the square of the number of columns
originworld x, z of column (0, 0)
baseelevation of the bottom of every column
stratathe starting stack, bottom to top
tilecolumns per tile edge, which is what the mesh and the navigation mesh rebuild by
waterTablemetres, or a function of x and z
reachhow many columns away to look for the foot of a face

Commands and events#

Commands: terrain.dig { footprint, depth, tool, dt? }, terrain.dump { spoil, at }, terrain.fill { footprint, soil, height, loose? }, terrain.level { footprint, height, soil? }, terrain.compact { footprint }, terrain.wet { footprint, moisture }, terrain.at { x, z }. A footprint is { box: [x0, z0, x1, z1] } or { centre, radius }. An agent can dig a trench or level a building site with these alone.

Events: terrain:slump { volume, passes, columns, at }, terrain:changed { tiles } (what the navigation and the mesh must rebuild), terrain:flooded { volume }.

Works with#

media: a flooded pit needs a water medium over it to float things; mud as soil sinks things, mud as a medium only drags. explosions: craters are dug in the terrain you hand them, and terrain:changed is raised for the tiles touched, so a blast and a shovel leave the ground the same way. Navigation rebuilds the tiles named in terrain:changed.

Cost#

Every edit relaxes only the affected columns, so cost grows with the size of the disturbed area, not with the terrain. A trench is cheap; a landslide over 200 columns takes milliseconds. Keep cell coarse where nobody digs.

When it does not behave#

SymptomCause
Nothing is removed by a digthe tool's reach is below the soil's hardness (blocked: true), or power is tiny for the dt
A pit never slumpsthe soil is cohesive and the cut is below criticalHeight; that is correct. Make it deeper, or wet the ground
A body does not sinkthe pressure under it is below the soil's bearing: weight over footprint area
A body sinks foreverthe soil is mud with no bottom beneath it: give it a stratum of something firm
a tunnel needs bedrockthe bore runs below the strata you gave; add a rock stratum
A body falls through a dug pityou are on collide('all') and have not rebuilt, or in sinkage-only mode on mud; pick a mode
Flood volume is 0no waterTable, or the pit stayed above it

Worked numbers (volume balance of digging and compacting, sinkage depth in mud against sand) are in tests/unit/examples/physics-terrain.test.ts.

Reference#

Terrain, terrain, TerrainCapability, Soil, SoilLibrary, EXAMPLE_SOILS, Tool, DigResult, criticalHeight, culmannHeight, sinkageDepth.