Builtin Library
README.md
Terrain (component)
Ground, as scene content. Part of the terrainSystem package — every terrain entity in a scene carries exactly one of these.
An entity carrying this component IS a landscape: a .terrainData standing over
a rect of world, drawn through a quadtree of LOD nodes that
refines under whichever camera is looking at it, with a collider the default
avatar can walk.
-- The whole of it: one reference. The landscape says how big it is.
entity(id).component.add("Terrain", { terrain = dunes })
A landscape is held whole: every texel of it, on the CPU and on the GPU, for as
long as the entity stands. A map larger than that (many kilometres, an open
world, a driving or flight game, ground that must not all be in memory at once
on a phone) is a TerrainStream: the
same ground streamed in tiles that load, draw and collide around the player,
the camera and every vehicle, from a height(x, z) function or one baked
.terrainData a tile.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
terrain | AssetRef<terrainData> | nil | The landscape this entity IS — the heightfield, the control textures, the holes mask and the layer table, held together as one asset. It states the rect it covers and the reach it declares. |
material | AssetRef<material> | nil | What the ground is shaded with. Falls back to the package's terrainStandard. |
size | vec2 | { 0, 0 } | Where the ground stands, when it should stand somewhere other than where the landscape says. Left at zero, the container's own placement is used. |
origin | vec2 | { 0, 0 } | World centre of that rect, same rule. |
heightRange | vec2 | { 0, 0 } | The vertical reach the geometry declares — x the lowest metre, y the highest. Left at zero, the landscape's own reach is used. |
collision | boolean | true | A static heightfield collider over the whole rect. Each painted layer collides as the surface named after it (Physics.defineSurface): a sample of the collider names the layer covering it most, so contacts and ray and shape casts on the ground report that layer as surface, and a surface defined under a layer's name gives that ground its friction, restitution and rolling resistance. |
collisionResolution | number | 0 | Collider samples along each axis. Zero collides on the ground's own texels, exactly where heightAt answers; a count coarser than the ground trades fidelity for cells. |
heightRange is the mesh's bounding box, so a range smaller than the ground
culls the terrain away at grazing angles and the symptom is nothing at all on
screen.
By default the collider's samples stand on the ground's texel centres, read as
they are, with a half-texel strip out to each edge, and each cell splits into
the same two triangles heightAt reads it as
(terrainHeightfield.colliderTiles). A ray onto the collision meets the height
heightAt answers anywhere on the rect. The mesh stands every vertex on a
stop of that same lattice. A node of the mesh 32 texels across or less (the
finest level of detail, on ground of 2 m texels or coarser) draws the
collider's own triangles, so a body rests on the ground a player sees; the one
strip of cells along an edge it stitches to a coarser neighbour follows that
neighbour's edge instead. A larger node draws a subset of the lattice and
stands off the collider by its level of detail. The collider is tiled, at most
512 samples a side a tile, so a landscape of up to 2048 x 2048 texels collides
at its own resolution;
past that the collider is coarsened evenly and the component reports it. A
collisionResolution coarser than the ground samples the rect at that count
instead, which costs fewer cells and puts the collision away from the drawn
ground by however much the ground bends between samples.
The heightfield's own texel count sets how finely the ground is described inside
the rect; the landscape's size sets how much world that description covers.
Where the ground stands
The landscape's coordinates are the entity's local space. The mesh is drawn through the entity's transform and every collider tile is a child of the entity standing in those same coordinates, so moving, turning, tilting or scaling the entity (a positive scale along each of its own axes) carries the drawn ground and its collision together, and a ray onto the collision meets the ground where it is drawn.
The queries below name a WORLD point and answer for that same ground: heightAt
is the world Y a ray cast straight down at (x, z) meets it at, normalAt and
slopeAt are the world's normal and slope there (on a tilted entity the tilt is
part of the slope), and hasGroundAt, waterAt and depthAt read the point of
the landscape under (x, z). Under an entity turned on its side or past it no
ground faces up, and each query answers as it does off the ground: nil, false or
0. field() is the heightfield itself, which answers in the landscape's own
coordinates.
Where the landscape comes from
A graph. It opens a rect of ground with terrain.source, shapes it with the terrain
nodes (fbm, cone, ridge, erode, ...), writes the coverage of each surface as a
mask layer with terrain.mask, and ends in terrain.write, which bakes the manifest and
every child texture in one step and answers the reference to the .terrainData. That is
the same move every other procgen family makes to persist what it made. The reference is
what the component's terrain property holds:
entity(id).component.add("Terrain", { terrain = ref }).
A heightmap made anywhere else (an 8-bit image, a downloaded elevation tile) comes in
through terrain.from_texture and back out through terrain.write.
Lifecycle
awake— opens the landscape, builds the LOD pool, binds it to the entity, adds theModeland the collider.update/editorUpdate— follows the on-screen camera with the cut, in edit and in play alike.onPropertyChanged— amaterialchange swaps the Model's material in place; anything else describes the ground itself, so the ground is rebuilt.onDestroy— releases the mesh, the pool and the heightfield's pixels.
Methods
| Method | |
|---|---|
:heightAt(x, z) | World Y at world (x, z): the surface the collision holds and the mesh is drawn on. |
:normalAt(x, z) | Unit surface normal in the world, three numbers. |
:slopeAt(x, z) | Degrees from the world's level; 0 is flat. |
:field() | The heightfield itself: the value every terrainHeightfield verb takes, in the landscape's own coordinates. |
:reload() | Read terrain again and rebuild what the entity draws. |
The queries answer from the same texels the geometry is built out of, so what a script is told and what a player walks on cannot disagree.
Re-baking the landscape and calling :reload() moves every terrain standing on
that asset, without a restart and without touching the scene graph —
the mesh keeps its guid, so the Model is never re-bound.
One draw
Every node of the LOD cut is one range of one index buffer, so the whole
terrain — at every level, however large — is a single indexed draw on a single
entity. Nodes are never entities. modules/terrainMesh is the whole surface.