FERRETGame engine over Babylon.js
Ferret

@ferret/physics-terrain

layered soil height field that can be dug, filled, slumps and floods; sinkage; tunnels; terrain as kernel meshes.

22 exports · source in packages/physics-terrain/src

Classes

SoilLibrary class

class SoilLibrary
constructor(initial: Record<string, Soil> = EXAMPLE_SOILS)
add(name: string, s: Soil)
get(name: string): Soil
has(name: string)
names()

packages/physics-terrain/src/soil.ts:32

Terrain class

class Terrain
readonly nx: number
readonly nz: number
readonly cell: number
readonly origin: [number, number]
readonly base: number
readonly soils: SoilLibrary
readonly tile: number
readonly reach: number
waterTable: ((x: number, z: number) => number) | null
readonly tunnels: Tunnel[]bores through the ground: volumes the heightfield cannot hold; solidAt knows them
onSlump: ((r: SlumpResult & { at: [number, number] }) => void) | nullcalled with what a slump moved (the host turns it into terrain:slump)
constructor(o: TerrainOptions)
setColumn(i: number, j: number, strata: Array<{ soil: string; thickness: number }>)Replace the strata of one column (bottom to top); the surface height follows.
static fromLandscape(grid: LandscapeGrid, o: { soils: SoilLibrary; map: Record<string, string>; fallback: string; bedrock?: string; top?: number; depth?: number; tile?: number; waterTable?: TerrainOptions['waterTable'] }): TerrainA terrain from the grid ue/landscape.py writes (Unreal's cm, z up): the heights are the surface; every column is top m of the soil its dominant landscape layer maps to (layers names the layers, layerAt has an index per sample, -1 for none, which takes fallback) over bedrock down to depth m below the lowest point. Axes as in the exported mesh: x = x / 100, z = y / 100, height = z / 100.
inside(i: number, j: number)
x(i: number)
z(j: number)
col(x: number, z: number): [number, number]
height(i: number, j: number): number
heightAt(x: number, z: number): numberSurface height at a point, bilinear between columns (clamped at the edge).
layers(i: number, j: number): readonly Stratum[]
topSoil(i: number, j: number): { name: string; soil: Soil }The soil at the surface of a column.
soilAt(x: number, z: number)
tableAt(x: number, z: number): number
moistureAt(i: number, j: number): number0..1: set by wet, and 1 for a column whose surface is under the water table.
volume(): numberGround in place plus heap, m³: what a dig and a dump must keep.
heapVolume(): numberVolume of heap (loose) in the terrain, m³.
addLayer(i: number, j: number, soil: string, thickness: number, loose = false)Add a layer on top of a column (the sparse edit used by fill and by imports).
dig(o: { footprint: Footprint; depth: number; tool: Tool; dt: number }): DigResultDig. depth is taken off the surface of every column of the footprint, top down, as far as the tool's energy (power x dt, at the soil's hardness per m³) and its reach allow. What comes out is returned as heap.
dump(at: [number, number], spoil: readonly Spoil[]): SlumpResultPour heap at a point: it lands on the column and runs down to the soil's slope.
fill(o: { footprint: Footprint; soil: string; height: number; loose?: boolean }): { added: number; slump: SlumpResult }Fill a footprint with a soil up to a height (in place, not heap). Returns the volume added.
level(o: { footprint: Footprint; height: number; soil?: string }): { cut: number; filled: number; spoil: Spoil[]; slump: SlumpResult }Flatten a footprint to a height: cut what stands above it and fill what is below. Returns the net volume moved (positive = cut).
compact(footprint: Footprint): numberPack heap in a footprint into ground: each loose layer shrinks by its bulking. Returns the volume that vanished.
wet(footprint: Footprint, moisture: number)Wet a footprint (0 = dry, 1 = saturated).
excavate(o: { centre: [number, number]; radius: number; depth: number; throw?: number; reachOuter?: number }): { removed: number; spoil: Spoil[]; thrown: number; fallback: number; slump: SlumpResult; }A crater: a paraboloid of radius R and depth d (depth(r) = d·(1 − (r/R)²)) is taken out, with no tool and no energy account. The heap it makes (volume x bulking) comes down in two places: throw of it as ejecta on the ring from R out to reachOuter·R, falling off as r⁻³, and the rest back in the crater, in proportion to what was taken. Then the ground relaxes.
solidAt(x: number, y: number, z: number): booleanIs there ground at the point: below the surface and outside every bore. A body in a tunnel stands in the open.
tunnel(o: { from: [number, number, number]; to: [number, number, number]; radius: number; lined?: boolean; limit?: number }): TunnelResultBore a tunnel. The roof is checked by Broms and Bennermark's stability number N = gamma (H + R) / c for the soil at the crown, with H the cover above the crown and R the radius, and the soil's cohesion c (wet ground holds more): an unlined bore stands while N is below limit (6, the flow limit of an open face in clay), a lined one always; sand with no cohesion cannot stand unlined. When it does not stand the roof caves in: the bore fills with the soil above it, which swells by the soil's bulking, and the surface settles over it by the volume that came down.
tileMesh(ti: number, tj: number): { positions: number[]; indices: number[] }The surface of tile (ti, tj) as a triangle mesh in world coordinates: one vertex per column, the edge columns shared with the next tile so the seams close.
tunnelMesh(t: Tunnel, sides = 16, step = 1): { positions: number[]; indices: number[] }The inside of a bore as a tube mesh in world coordinates (rings every step m along the axis, sides round).
takeDirtyTiles(): string[]Columns changed since the last call, as tile keys "i,j": what the navigation and the mesh have to rebuild.
slump(maxPasses = SLUMP_PASSES): SlumpResultRelax the columns that changed (and what they disturb) until every step stands, moving soil from the higher column to the lower.

packages/physics-terrain/src/terrain.ts:69

Functions

criticalHeight function

function criticalHeight(s: Soil, moisture = 0): number

The height of a vertical cut that stands, Hc = (4c/γ)·tan(45° + φ/2); 0 for a soil without cohesion.

packages/physics-terrain/src/soil.ts:60

culmannHeight function

function culmannHeight(s: Soil, betaDeg: number, moisture = 0): number

Culmann: the height of a face of slope angle β (degrees) that stands, Hc = (4c/γ)·sinβ·cosφ / (1 − cos(β − φ)). It is Infinity at β ≤ φ (a slope of repose never fails), meets criticalHeight at 90°, and is 0 without cohesion above the repose angle.

packages/physics-terrain/src/soil.ts:69

effective function

function effective(s: Soil, moisture: number): { cohesion: number; density: number }

Cohesion and density at a moisture between 0 (dry) and 1 (saturated).

packages/physics-terrain/src/soil.ts:51

sinkageDepth function

function sinkageDepth(s: Soil, pressure: number): number

Sinkage z (m) under a pressure p (Pa): none up to the bearing, then z = ((p − bearing)/k)^(1/n).

packages/physics-terrain/src/soil.ts:78

terrain function

function terrain(t: Terrain): TerrainCapability

packages/physics-terrain/src/index.ts:37

Interfaces

DigResult interface

interface DigResult
removed: numbervolume of ground taken out, m³
spoil: Spoil[]what was taken, as heap: loose volume per soil
energy: numberJ spent
blocked: booleanthe tool met a soil harder than it can break
flooded: numbervolume of water the pit needs to reach the water table, m³ (0 when it stayed above)
slump: SlumpResult

packages/physics-terrain/src/terrain.ts:47

Footprint interface

interface Footprint
box?: [number, number, number, number]world rectangle: x0, z0, x1, z1
centre?: [number, number]or a disc
radius?: number

packages/physics-terrain/src/terrain.ts:30

LandscapeGrid interface

interface LandscapeGrid

The height grid of an Unreal landscape (cm) with, per sample, the index of its dominant layer (the physical material of the layer the trace hit).

origin: number[]
step: number
nx: number
ny: number
heights: number[]
layers?: string[]
layerAt?: number[]

packages/physics-terrain/src/terrain.ts:28

SlumpResult interface

interface SlumpResult
moved: number
passes: number
columns: number
converged: boolean

packages/physics-terrain/src/terrain.ts:61

Soil interface

interface Soil

A soil or rock as data: the same record for a pile of sand, a clay bank, a dig and a footstep.

density: numberkg/m³, dry in place
friction: numberinternal friction angle, degrees: the angle of repose of the loose material
cohesion: numbercohesion, Pa (0 for dry sand and gravel)
hardness: numberspecific dig energy, J/m³: the energy it takes to remove a cubic metre
bulking: numbervolume of loose spoil per volume in place (1.3 = a 1 m³ hole of clay makes 1.3 m³ of heap)
bearing: numberpressure, Pa, the ground carries without sinking
sinkage?: { k: number; n: number }below the bearing: pressure beyond it grows as k·zⁿ with the sinkage z (Bekker); stiff when missing
damping?: numberdamping of a sinking body, Pa·s/m
wet?: { cohesion?: number; density?: number }values when saturated; the moisture of a column (0..1) blends dry and wet

packages/physics-terrain/src/soil.ts:2

Spoil interface

interface Spoil
soil: string
inPlace: number
loose: number

packages/physics-terrain/src/terrain.ts:45

Stratum interface

interface Stratum

One layer of a column, bottom to top. loose thickness is heap (spoil, a dump), not ground in place.

soil: string
thickness: number
loose?: boolean

packages/physics-terrain/src/terrain.ts:4

TerrainCapability interface

interface TerrainCapability extends Capability
readonly terrain: Terrain
depthOf(b: Body): numberDepth the body is below the surface of the ground, m (0 when above it).
collide(mode: TerrainCollision): voidPut the ground into the kernel as static meshes: tunnels builds the bores (a body in a tunnel stands on its floor; the surface still goes by the sinkage law), all also builds the surface in tiles (a body then rests on the surface and the sinkage law has nothing to act on). Tiles dug since the last call and new bores are rebuilt; call it again after a dig, or it follows terrain:changed by itself.
readonly bodies: { tiles: Map<string, Body>; tunnels: Map<number, Body> }Bodies the capability made: tile meshes by "i,j" and bore meshes by id.

packages/physics-terrain/src/index.ts:23

TerrainOptions interface

interface TerrainOptions
nx: numbercolumns along x and z
nz: number
cell: numbercolumn spacing, m
origin?: [number, number]world x, z of column (0, 0)
base?: numberelevation of the bottom of every column, m
soils: SoilLibrary
strata: Array<{ soil: string; thickness: number }>the strata every column starts with, bottom to top
tile?: numbercolumns per tile edge: what the NavMesh and the mesh rebuild by
waterTable?: number | ((x: number, z: number) => number)water table height, m (a number, or a function of x, z); none when missing
reach?: numbercolumns looked at to find the foot of a face when judging its stability

packages/physics-terrain/src/terrain.ts:6

Tool interface

interface Tool
power: numberW
reach: numberthe hardest soil (J/m³) the tool can break at all

packages/physics-terrain/src/terrain.ts:38

Tunnel interface

interface Tunnel

A tunnel: a round bore along a line from a to b (world coordinates, y up), radius m, unlined unless lined.

id: number
a: [number, number, number]
b: [number, number, number]
radius: number
lined: boolean

packages/physics-terrain/src/terrain.ts:66

TunnelResult interface

interface TunnelResult
id: number
stable: boolean
stability: number
roof: number
subsided: number
limit: number

packages/physics-terrain/src/terrain.ts:67

Types

TerrainCollision type

type TerrainCollision = 'none' | 'tunnels' | 'all'

What the capability puts into the rigid-body kernel: nothing (the height field is read by the sinkage law only), the inside of the bores, or the bores and the whole surface as static meshes.

packages/physics-terrain/src/index.ts:21

Constants

EXAMPLE_SOILS const

const EXAMPLE_SOILS: Record<string, Soil>

packages/physics-terrain/src/soil.ts:23

TERRAIN_DEFAULTS const

const TERRAIN_DEFAULTS: { sinkage: boolean; footprint: number | undefined; }

the ground. A Terrain (layered height field) is given to the capability; it keeps the terrain relaxed, tells the world when ground slumps or changes, and makes a body sink where its weight is more than the soil bears.

values (each at world / level / zone / entity scope):

  sinkage   true: bodies sink into soft ground and are held by its bearing and stiffness   (true)
  footprint contact area of a body, m²; the cross-section of a sphere when missing       (undefined)

events: terrain:slump { volume, passes, columns, at }, terrain:changed { tiles } after a command, terrain:flooded { volume }.

packages/physics-terrain/src/index.ts:18