Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit8d45df8664

README.md

terrainHeightfield

A .texture standing over a rect of world, read as ground.

The durable form of a landscape is an ordinary texture asset — one with a guid, that a component references, that travels with a world, that the renderer already knows how to sample. terrain.to_texture is what puts a graph's raster into one and texture.write persists it. This module is the other end: what a runtime holds while that ground is on screen.

local terrainHeightfield = require("~terrainSystem.modules.terrainHeightfield")

-- A `.terrainData` states the rect it covers, so opening one needs nothing else.
local hf = terrainHeightfield.open("/zero/source/terrain/dunes.terrainData")

-- The heightfield alone can be read too; then the caller says where it stands.
local bare = terrainHeightfield.open(tex, { size = { 4000, 4000 }, origin = { 0, 0 } })

local y = hf:heightAt(120, -60)   -- metres
local nx, ny, nz = hf:normalAt(120, -60)
local slope = hf:slopeAt(120, -60) -- radians
hf:close()

The texels

rgba32f, in the layout terrain.to_texture writes:

ChannelHolds
Rthe height, in METRES — it goes negative
Gthe gradient along world X
Bthe gradient along world Z
Acoverage

A height's interesting relief is often a few metres inside a range of hundreds, so eight bits band it into terraces; that is what the precision is for. The gradient is what a surface normal comes out of without differencing the height four more times, and it is why terrain_mesh costs four texel reads a vertex rather than twenty.

A heightmap authored anywhere else — an 8-bit image, a downloaded elevation tile, a raster an earlier graph baked — becomes one of these by going through a graph: terrain.from_texture reads it as ground, and terrain.to_texture writes the result back out at this precision.

Two carriers, one set of texels

An open heightfield holds the texture in both forms the runtime reads it in:

  • hf.texels — a GPU buffer of the texel bytes, which the terrain_mesh compute kernel builds vertices out of.
  • hf.cpu — the pixels, which heightAt / normalAt / slopeAt / sampleGrid answer from.

Both are the same texels, uploaded from one read, so what the ground LOOKS like and what a query SAYS about it cannot drift apart.

The pixels are the heightfield's own copy, decoded from the asset when it opens and held in a buffer for as long as it stands, and so are its holes mask, its water surface and its layer weights. A heightfield answers in edit, in play and after play, whatever else loads or unloads the same texture. The cost is one copy a heightfield: 16 bytes a texel for the heights and the water, 4 for the holes mask and each layer-weight texture. Two heightfields open on one landscape hold two copies, which is also what keeps an edit to one (setHeights) to that one.

The surface

Verb
open(ground, opts?)Read a .terrainData — or the heightfield .texture alone — as ground. A container carries its own { size, origin, heightRange }; a bare texture takes them from opts, and an explicit opt outranks the container either way.
hf:heightAt(x, z)Metres: the texel cell around the point read through cellHeight, the surface the terrain's collider holds.
hf:normalAt(x, z)Unit normal, three numbers, from the carried gradient.
hf:slopeAt(x, z)Radians; 0 is flat.
hf:sampleGrid(cols, rows)Row-major heights over the whole rect at any sample count, read through heightAt.
hf:colliderTiles(perTile?)The heightfield collider tiles that hold this ground exactly: samples on the texel centres, a half-texel strip out to each edge. Each tile is { minX, minZ, sizeX, sizeZ, xs, zs }.
hf:texelLattice(xs, zs)Row-major heights of the texels at the named columns and rows, as they are: a tile's grid from colliderTiles.
terrainHeightfield.cellHeight(h00, h10, h01, h11, fx, fz)The ground inside one cell from its four corner heights.
hf:hasGroundAt(x, z)Whether there is ground at all — false over a hole.
hf:refresh()Read the texture's pixels again and put them back on the GPU. Bumps gen.
hf:close()Let go of the pixels and the GPU buffer. The asset is untouched.

heightRange is the vertical reach the geometry declares, which becomes the mesh's bounding box — a range smaller than the ground culls the terrain away at grazing angles. Left out, it is read off the pixels, which costs one pass over them.

One surface

The ground between texel centres is two flat triangles a cell, split along the diagonal from the cell's corner one texel along +Z to its corner one texel along +X: the split a Rapier heightfield collider makes, so a collider built from colliderTiles meets a ray at the height heightAt answers there, to float precision. The terrain_mesh kernel stands every vertex on a stop of the collider's lattice (the rect's edges and the texel centres), so a node meshed at the texel spacing or finer draws the collider's own triangles (save the strip along an edge stitched to a coarser neighbour, which follows that neighbour's edge), and a coarser one differs from it only by the level of detail it is cut to. Past the outermost texel centres the ground runs level out to the rect's edge, for the query, the collider and the mesh alike.

Holes

A landscape can be CUT OUT — a cave mouth, a quarry, a shaft — and its holes mask is carried the same two ways the heightfield is: on the GPU for the index kernel, which writes the quads it cuts out as degenerate triangles, and on the CPU for hasGroundAt. Same pixels, so a place with no surface to draw is a place nothing can be stood on either, and scatter that asks before it places never drops a tree into a shaft.

A landscape with no mask is solid everywhere and answers hasGroundAt true without reading anything. The edge of a hole follows the mask at the density the node is meshed at, so a hole is quad-shaped at the LOD it is seen from.

Generations

hf.gen counts the times the texels have been read. terrainMesh records the generation each node was meshed at, so re-authoring the ground under a standing terrain and calling hf:refresh() brings the geometry with it — nobody has to say which nodes changed.