Modules / World & ground

terrain3dplugin

Endless ground computed from a field file, drawn as rings that follow the camera.

Putting it in a scene

List it before render3d, and before any plugin that stands on its ground. terrain3d has no row: nothing of it changes while the game runs, and a child scene inherits world.terrain3d from the manager scene. The field file is a json entry of the loader catalogue with preload, and every map a ground layer names is a texture entry.

"plugins": ["loader", "scenemanager", "player", "terrain3d", "scatter3d", "skydome3d", "render3d"],
"world": {
  "loader": { "assets": {
    "land": { "type": "json", "url": "/games/mygame/assets/land.json", "preload": true },
    "meadow": { "type": "texture", "url": "/games/mygame/assets/ground/meadow-color.jpg", "wrap": "repeat" },
    "meadowNormal": { "type": "texture", "url": "/games/mygame/assets/ground/meadow-normal.jpg", "space": "linear", "wrap": "repeat" },
    "rock": { "type": "texture", "url": "/games/mygame/assets/ground/rock-color.jpg", "wrap": "repeat" },
    "rockNormal": { "type": "texture", "url": "/games/mygame/assets/ground/rock-normal.jpg", "space": "linear", "wrap": "repeat" }
  } },
  "terrain3d": {
    "field": "land",
    "rings": [{ "reach": 128, "seg": 256 }, { "reach": 512, "seg": 256 }, { "reach": 2048, "seg": 256 }, { "reach": 9000, "seg": 180 }],
    "shadowReach": 512,
    "budget": 8,
    "seabed": false,
    "preload": true,
    "bake": { "rings": [512, 2048, 8192], "res": 2048, "near": 150, "far": 300, "cliff": 0.6, "band": 0.06, "budget": 1024 },
    "maps": { "near": 6, "reach": 300, "step": 8, "small": 256 },
    "mods": { "ready": ["scatter3d"] }
  }
}

Properties

Read from world.terrain3d. Only field is read by the rules; the rest set how the page draws.

.field : string

The loader catalogue name of the field file (below). The ground is made the first time anything asks for it, so a field that cannot be made throws then.

.rings : { reach, seg }[]

The ground is drawn as square rings round the camera, finest first, so it never ends. A ring reaches reach metres from its middle to its edge and is seg cells across. A ring recentres when the camera crosses about a sixteenth of its reach. A set in which a ring's half width is too small to cover the hole left by the finer one throws when the plugin starts. The outermost ring hangs a skirt, the larger of hscale and 40 metres deep, from its edge. The finest ring's cell, 2 × reach / seg, is the cell walker's bodies stand on.

Figure 1. Three rings round the camera, seen from above: ring 0 reaches 40 metres, ring 1 100 and ring 2 400. Each has its own grid of cells, and each coarser ring leaves a hole for the finer one inside it.

.shadowReach : number

Metres. A ring whose reach is within this casts shadows. Every ring receives them.

.budget : number

Milliseconds a frame the page may spend rebuilding rings, at least one unit of work a frame. Where the page has workers, rings are built in a pool of at most one worker a ring and one fewer than the machine's cores, at least one, and the budget bounds only the page's own share. The rings shown stay until every rebuilt ring swaps in together.

.seabed : boolean

Draws the ground under the waterline at its raw height, so the sea has a floor.

.static : boolean = false

The rings stand still, centred on the world's origin, built once. For a place small enough that the finest ring covers all of it. A changed painted layer still rebuilds the rings it reaches.

.preload : boolean = false

The camera's first place is built whole before any of the land is put on the scene, so the land and what stands on it arrive in the same frame.

.bake : { rings, res, near, far, cliff, band, budget }

Optional. The ground's colour and normal are rendered into square textures round the camera, so ground past far reads one baked texel instead of working out every ground layer, and costs the same however many layers the field has. A ring's middle snaps to an eighth of its side, and when it moves only the strip it uncovers is baked, in tiles of 512 × 512 texels, finest ring first. Until a strip is finished, the ground there is worked out live. A new ground, a new land or a changed painted layer bakes every ring again.

KeyMeaning
ringsSides in metres, finest first, each wider than the one before: square textures centred near the camera. The coarsest must lie inside the ground the mesh rings draw, and a set that reaches past it throws when the plugin starts, naming the largest side it may have.
resTexels a side of every ring, a whole number of at least 512.
near, farMetres from the camera over which the ground fades from worked out live to baked. far is beyond near.
cliffGround whose normal's y is below this stays live at every distance, since a bake straight down has nothing for a steep face.
bandBetween 0 and 0.5: the share of a ring's side over which a point fades from it to the next coarser ring before its finished square ends, so a ring's edge never shows.
budgetThousands of texels baked a frame, at least one tile. Above 0.

Ground past the coarsest baked ring is always worked out live.

.maps : { near, reach, step, small }

Optional. Without it, every ground layer's maps are held at the size of the largest colour map. With it, every layer is held as a small copy, and only the few layers the ground near the camera shows are held at full size, so a field may have as many layers as the small copies fit.

KeyMeaning
nearHow many layers are held at full size, a whole number of at least 1.
reachMetres from the camera, above 0: the square whose ground decides which layers are held at full size. The near layers with the most weight there hold the full-size slots, worked out again whenever the camera moves a quarter of reach or the land changes. A layer keeps a slot it already has.
stepMetres between the points that square is sampled at, above 0 and at most reach.
smallTexels a side of every layer's small copy, a whole number of at least 16.

A layer that comes into reach is drawn from its small copy until its full maps are in place. The far read of far reads the small copies, and so does a baked ring whose texel is as wide as a small copy's, the smallest layer scale over small.

.mods : { ready }

The plugins asked whether their share of a prepared place is done (see Mods).

The field file

{
  "program": {
    "world": 4096, "hscale": 120, "sea": 0.35, "flood": true, "out": "land",
    "nodes": [
      { "id": "base", "op": "noise", "kind": "simplex", "freq": 6, "seed": 1 },
      { "id": "hills", "op": "fold", "input": "base", "kind": "ridge" },
      { "id": "wx", "op": "noise", "kind": "value", "freq": 12, "seed": 2 },
      { "id": "wz", "op": "noise", "kind": "value", "freq": 12, "seed": 3 },
      { "id": "warped", "op": "warp", "input": "hills", "x": "wx", "z": "wz", "amount": 60 },
      { "id": "land", "op": "clearing", "input": "warped", "floor": 0.1, "flat": 80 }
    ]
  },
  "analysis": { "slopeStep": 1, "curveStep": 4, "positionRadius": 20, "occlusionRadius": 30, "occlusionSteps": 8, "cliffProbe": 3, "cliffSlope": 0.5, "cliffDrop": 4, "flatAngle": 0.05, "records": 20000 },
  "ground": {
    "compose": "stack",
    "heightBlend": { "amount": 2, "depth": 0.1 },
    "far": { "amount": 0.7, "scale": 7.3 },
    "roughness": 0.9,
    "layers": [
      { "name": "meadow", "map": "meadow", "normal": "meadowNormal", "scale": 36,
        "warp": [{ "freq": 0.005, "amp": 30, "stretch": [1, 1, 1], "rotate": 0 }, { "freq": 0.035, "amp": 3, "stretch": [1, 1, 1], "rotate": 0 }] },
      { "name": "rock", "map": "rock", "normal": "rockNormal", "scale": 30, "tri": true, "sharp": 12,
        "where": [{ "q": "slope", "lo": 0.3, "hi": 0.45 }],
        "noise": { "freq": 0.12, "amp": 0.8 }, "edge": 0.35 }
    ]
  },
  "surfaces": {
    "road": { "colour": "#3a3a3c", "cover": 0, "grip": 1.1, "drag": 0.01, "spray": false, "mark": 0 },
    "mud": { "colour": "#4a3a28", "cover": 0.3, "grip": 0.6, "drag": 0.08, "mark": 1 }
  },
  "layers": [{ "at": [0, 0], "size": 512, "px": 256, "strokes": [] }]
}

A field with no ground throws when its land is made.

program

The height of the ground, as a list of nodes run in order. Each node has an id and an op, every input names an earlier node, and out names the node whose value is the ground. Values are unitless and are multiplied once by hscale, in metres; only a warp's amount, a crater's radius and a clearing's flat are in metres.

KeyMeaning
worldMetres a noise's freq is counted across.
hscaleMetres a value of 1 stands for.
sea, floodThe waterline, as a value of the program: the ground is shifted so that value is at y = 0. With flood true, ground below 0 is under water; with it false there is no sea, and a valley below 0 is dry.
out, nodesThe node that is the ground, and the nodes.
Figure 2. A flooded land in cross-section. The bed is the raw ground, the top line is the height, the ground or the water's surface at y = 0, and the depth is the gap between the two.
opParameters and value
constvalue.
noisekind value, simplex or worley, at freq cells across world, with seed. rotate turns it in radians and stretch [x, z] scales it, [1, 1] when absent. A worley noise's metric is f1 (default), f2 or f2-f1. Values are 0 to 1.
warpThe value of input at the point moved by amount metres times the values of nodes x and z.
foldinput folded by kind: ridge (1 − |2v − 1|)², billow |2v − 1|², abs |2v − 1|, or terrace, steps floors joined by smooth risers.
remapinput from inLo..inHi, clamped, onto outLo..outHi, eased linear, smooth or smoother. inLo equal to inHi throws.
combinea and b by mode: add, sub, mul, min, max, or lerp by t, 0.5 when absent.
scaleinput × by.
cratersOne crater in every cell of a lattice freq cells across world, of a radius between radius [min, max] metres. depth is the floor's depth, rim and rimWidth the rim's height and width in radii, and ejecta how many radii the apron reaches. The deepest floor wins and the rims add.
clearinginput levelled toward its own value at the origin, fully at the origin and fading out over flat metres. floor, 0 to 1, is how much of the ground's shape is kept at the middle.
biomeslist: [{ name, strength, height }] and sharp above 0. strength and height name nodes: how much of the biome stands at a point, and its own ground there. Each biome's share is max(0, strength)sharp over their sum, and the value is the shares' mix of the heights. A higher sharp narrows the ground between two biomes. At most one per program, with distinct names.
Figure 3. One noise shown as it is, then folded as ridge, billow and a terrace of 4 steps.

analysis

How a land record, the facts a rule reads about a point, measures the ground. All in metres unless noted.

KeyMeaning
slopeStepThe half-span the normal for slope and aspect is read over, and the step rays march the ground in.
curveStepThe span the curvatures are read over.
positionRadiusThe radius of the ring of 8 points position is measured against.
occlusionRadius, occlusionStepsHow far the march in 8 directions reaches, and the samples along each.
cliffProbeHow far away a cliff is looked for.
cliffSlope, cliffDropThe slope, and the rise or drop in metres, at which a face counts as a whole cliff.
flatAngleRadians: a direction whose horizon is within this of level is flat, for form.
recordsHow many records a land keeps. Past it, the one asked for longest ago is dropped.

Rules and the record

A rule { q, lo, hi } reads key q of the record and ramps it from 0 at lo to 1 at hi, clamped. With lo above hi the ramp falls, and with them equal it is a step at lo. Ground layers and scatter3d's packs both use these rules, and a rule naming no key throws when the land is made.

qMeaning
height, reliefThe height in metres, and the height over hscale.
slope1 − the normal's y: 0 on the level, 1 on a wall.
aspectThe way downhill faces, in radians: 0 faces +z and π/2 faces +x.
profile, planThe curvature along the slope and across it, positive where the ground is concave, as at a slope's foot or in a hollow.
positionThe height minus the mean of the ring of 8 points: positive on a rise, negative in a dip.
occlusionHow much the surrounding ground rises, 0 when nothing does.
formThe landform: 0 flat, 1 peak, 2 ridge, 3 shoulder, 4 spur, 5 slope, 6 hollow, 7 footslope, 8 valley, 9 pit.
cliffAbove, cliffBelow0 to 1: how much of a cliff rises uphill within cliffProbe, or drops downhill.
wet0 to 1: high in hollows and dips, where water gathers.
waterThe depth of the sea there, in metres.
biome:<name>That biome's share of the mix, 0 to 1.
surface:<name>That surface's painted weight, 0 to 1, and 0 where another surface is painted.
layer:<name>That ground layer's weight, 0 to 1, as the ground draws it before its height blend, so a scatter3d rule on layer:rock places on the rock that is drawn. A ground layer's own rules cannot read it.

ground

The ground's colour and normal, as texture layers laid one over another. Each pixel works out every layer's weight and reads the maps of each layer whose weight is above 0.01. The land record's layer:<name> works the weights out with the same masks, noise and warps on the server, in the workers and on the page.

KeyMeaning
layersAt least one, in order. The first covers everything, and each later one lies over the ones before it where its mask is.
composeHow the masks m become weights that sum to 1: stack (the default), each layer over the ones before it, wk = mk × Πj>k(1 − mj); normalise, m / Σm; or sharpened, m⁴ / Σm⁴.
heightBlend{ amount, depth }: where layers meet, the one whose texture stands taller, by its luminance, wins within depth. Only the drawing does this; layer:<name> is the weight before it.
far{ amount, scale }: a second read of each map at scale times its repeat, laid on by amount as a ratio to the map's mean colour, so the far ground varies at a size no repeat has.
roughnessThe ground's one roughness, 0.9 when absent. No roughness map is read.

A layer:

KeyMeaning
nameDistinct among the layers: the record's layer:<name>.
map, normalLoader texture entries: the colour map, in sRGB, and an optional OpenGL normal map. A layer with no normal map is drawn flat. A name that is no texture entry of the catalogue throws.
scaleMetres a repeat, above 0.
strengthThe normal map's x and y multiplied, 1 when absent.
tri, sharpWith tri, the maps are projected along all three axes, each axis's share its part of the normal to the power sharp (4 when absent), so a cliff does not stretch the map down its face. Without it, they are projected straight down.
whereRules on the land record, their ramps multiplied: the layer's coarse mask. height and slope are worked out at each pixel, the slope from the drawn ground's normal. Every other key is worked out at the grid points of each ring and blended between them.
noise{ freq, amp }: the coarse mask v moved by ±amp × 4v(1 − v) of four octaves of value noise at freq cycles a metre, above 0, read at the warped point. The layer's edge, where the mask crosses a half, wanders at every scale, and where the rules give 0 or 1 the noise moves nothing.
edgeHow wide that crossing is drawn, from 0, a cut, to 1. 0.3 when absent.
warp[{ freq, amp, stretch: [x, y, z], rotate }]: octaves of 3D noise of the world point, each turned rotate radians about y and scaled by freq × stretch on each axis, adding up to amp metres of offset. The maps and the mask's noise are read at the warped point, which hides the repeat. Stretching y alone draws strata.

The ground is drawn mid grey until every layer's maps have landed, and stays so if one fails to load.

surfaces

What a surface stroke paints, by name.

KeyMeaning
colourOptional: drawn over the ground layers by the paint's weight, through the edge when it has one. Without it, a ground layer with a rule on surface:<name> can lay a texture where the surface is painted instead.
edge{ width, ragged, scale }: turns the weight into a crisp line where it crosses a half, width wide, broken up by a noise of scale cycles a metre times ragged. Without an edge, the weight blends smoothly.
coverMultiplies scatter3d's density: 0 on a road.
gripMultiplies a wheel's friction on it. 1 when left out, and on unpainted ground.
dragRolling resistance: newtons a rolling wheel loses per newton of load, as soft ground sinks a tyre. 0 when left out.
sprayThe particle3d effect a wheel throws up, or false for none. Left out, what unpainted ground throws.
mark0 to 1: how dark a track a tyre leaves, deep in mud and none on asphalt. Left out, as unpainted ground.

layers

Painted ground. A layer is a square size metres across centred on at [x, z], of px × px pixels, baked from its strokes in order, the same bytes on the server and every seat. Layers apply in order, and paint feathers out over one pixel at a layer's edge.

A stroke is { verb, radius, hardness, strength, level, surface, path }, applied along the polyline path of [x, z] points in metres. An empty [] in the path breaks it into separate runs. Each pixel within radius metres moves toward the verb's goal by a falloff that is full inside hardness of the radius and falls to 0 at its edge.

verbGoal
raise, lowerThe ground ± strength metres. A later program edit carries the painted spot with it.
flattenThe height level.
smoothThe mean of the ground there and half a radius away along x and z.
benchThe ground under the nearest point of the path: it follows the hill along the path and is flat across it, which cuts a road.
surfaceNo height: paints the surface named surface.

Methods

Rules

From #plugins/terrain3d/terrain3d.sim.ts. Each takes w and reads the land of the field world.terrain3d names. Points are in metres.

heightAt( w, x, z ) : number

The height of the ground, or of the water's surface where it is flooded.

rawAt( w, x, z ) : number

The height of the ground itself, under any water.

depthAt( w, x, z ) : number

The depth of water, 0 on land and everywhere when the program does not flood.

normalAt( w, x, z, step ) : number[]

The ground's unit normal, read over ±step metres.

restsAt( w, x, z, span ) : number

The mean height of the four corners of a square span metres across, so a body stands still over ground rougher than itself.

paintAt( w, x, z ) : { weight, surface, cover, grip, drag, spray, mark }

The painted surface there and its weight, 0 to 1, with cover, grip and drag blended toward unpainted ground by the weight. spray and mark are the surface's where the weight is at least a half, undefined below.

occludes( w, a, b ) : boolean

Whether the ground rises above the segment between points a and b, each [x, y, z].

ray( w, origin, dir, far ) : number[] | undefined

The first point along the ray, within far metres, at or below the ground, or undefined.

analyse( w, x, z ) : record

The land record there. read(q) gives any key of the table above.

ramp( w, rule, x, z ) : number

A rule's ramp there, 0 to 1.

edits( w ) : { land, layers: { version, rect }[] }

Each painted layer's version, which moves every time the layer is baked again, and rect [x0, z0, x1, z1], the area it reaches in metres. Anything built on the ground compares these with what it saw last to know what to rebuild. The land also answers mix(x, z), each biome's share by name, and layers(x, z), each ground layer's weight by name.

On the page

Through plugins.terrain3d.

ground : object | undefined

This page's ground, once the field has loaded, the same bytes as the rules'. It holds height, raw, depth, ramp, edits, mix(x, z), each biome's share by name, and layers(x, z), each ground layer's weight by name; at(x, z), the land record; cover, spray and mark of the paint there; rules, the check of a rule list; ray(origin, dir), marched out to the camera's far plane; and shown, prepare, prepared, unprepare and task as below.

prepare( x, z )

Builds the land off the scene at a place the camera is about to jump to, for a cut, a teleport or a respawn. When the camera arrives, every ring is swapped in at once, so no ring shows half built. One place is in hand at a time, and unprepare() lets it go.

prepared( ) : { at, done } | undefined

The place in hand. done is true once every ring is built, every map the ground layers name has landed or failed, and every plugin in mods.ready says its share is done.

shown( ) : boolean

Whether the rings are on the scene. With preload it is false until the first place is done; without, true from the first frame.

building( ) : { done, total } | undefined

How far the rebuild in hand has got, in units of work.

task( url, input ) : Promise | undefined

Runs the task(ground, input) of the module at url in the least busy ring worker, on its own copy of the land, and returns a promise of its out. Undefined where the page has no workers.

baked( ) : { side, at, valid, left }[] | undefined

Each baked ring's side in metres, its middle, the square [x0, z0, x1, z1] of it that is finished, and how many tiles it has left to bake. Undefined without bake.

bake( meshes, x, z, cover )

For a plugin that draws terrain3d's rings itself, such as a globe. Moves the baked rings to follow the ground point (x, z) and bakes this frame's share from meshes, drawn with the ground's own material, which cover the square cover [x0, z0, x1, z1]. A mesh is placed in the bake by its ground point, so a ground bent in its vertices bakes flat. The plugin calls it with its own rings each frame.

focus( x, z )

For a plugin that draws the rings itself: centres the square that decides which layers maps holds at full size on the ground point (x, z). The plugin calls it with the camera's point.

carry( list )

For a plugin that draws the rings itself: list is every ring it draws, { k, C, grid? }, ring index, middle and, where the ring is new, the values its build worked out for the layers' rules, the surfaces and the biomes. A ring left out of the list is not drawn. The plugin calls it as its rings swap in.

Properties of the plugin

land

This page's land, made again when the field's content changes.

rings : { at, half, cell, mesh }[]

Where each ring stands: its middle and half width in metres, its cell size, and its mesh.

Mods

Name them in world.terrain3d.mods.

  • scatter3d
    ready: a prepared place is done only once every scatter3d cell in reach of it is placed.

terrain3d gives twelve mods. Each is named in the other plugin's world.<plugin>.mods as "terrain3d".

  • walker
    height, depth and cell: the ground under any water, so a body wades on the bed, the depth of the water over it, and the cell of the finest ring, 2 × reach / seg of rings[0] in metres, so a body stands on the triangles that are drawn.
  • box3d
    ground: chunk [i, j] of the land as a height field of world.box3d.chunk.count heights a side, world.box3d.chunk.cell metres apart. Neighbouring chunks share the heights along their edge.
  • box3dvehicle
    land on the page, whose spray and mark are the dust and the tyre marks of the surface painted under each wheel; grip and drag on the server, the painted surface's grip, 1 on unpainted ground, and its rolling drag, 0 on unpainted ground.
  • scatter3d
    ground on the server and land on the page: the height, the painted cover, the record's ramps, the rule check and what has changed, so a trunk a rule asks about stands where a seat sees it. Its rules may read layer:<name>.
  • solids
    land: the page's ground, on which every model solids draws is stood.
  • hitscan3d
    ground: { t, normal } where a shot's line first meets the ground, t metres along it and the normal over ±slopeStep, or undefined when it stays over the ground for far metres. A line starting under the ground meets it at once.
  • flight3d
    ground: the height under a point, the water's surface where the land floods, so a plane that flies into the sea has hit it.
  • ragdoll3d
    land: the page's ground, on which limp bodies are laid.
  • dismember3d
    land: the page's ground, on which the pieces and the gore come to rest.
  • skydome3d
    land: the page's ground, on which rain splashes land, and whose biomes under the camera set the sky's weather.
  • ocean3d
    land: the page's ground, whose depth the sea's seabed tile is baked from, again whenever its edits() move.
  • cutscene
    land: the page's ground, on which the cutscene's points, marks and shots stand, as its server half stands them on heightAt.

A land mod is undefined until the field has loaded.

In the guide