Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit8d45df8664

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

NameTypeDefaultDescription
terrainAssetRef<terrainData>nilThe 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.
materialAssetRef<material>nilWhat the ground is shaded with. Falls back to the package's terrainStandard.
sizevec2{ 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.
originvec2{ 0, 0 }World centre of that rect, same rule.
heightRangevec2{ 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.
collisionbooleantrueA 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.
collisionResolutionnumber0Collider 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 the Model and the collider.
  • update / editorUpdate — follows the on-screen camera with the cut, in edit and in play alike.
  • onPropertyChanged — a material change 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.