Terrain (asset type)
A terrain is a serialized heightmap — a flat width×depth array of floats plus the dimensions, world size, and an optional default material — that the Terrain component reads at startup to build a…
Authoring is decoupled from runtime: a terrain painter writes a fresh
heightmap to a .terrain asset; the runtime entity carries a
Terrain component pointing at that asset and rebuilds the mesh
deterministically. The same asset can be referenced by many entities.
When to use one
- You painted or generated a heightmap interactively and want to save it so subsequent loads (or other entities) reuse the same surface.
- You want a sharable, version-controlled terrain that doesn't drift —
the
.terrainasset is plain JSON, so diffs surface every height change and the heightmap roundtrips bit-exactly. - You want one entity per terrain in the scene file while the heavy per-vertex data lives in a sibling asset folder.
If you just want a runtime-generated terrain that doesn't need to
persist (procedural mountains regenerated every play), call
terrain.fromNoise / terrain.spawn directly — you don't need a
.terrain asset at all.
Where it lives
- Source:
/zero/source/.../<Name>.terrain/ - Identity:
<Name>(the.terrainsuffix strips). - Folder shape:
terrain.json— canonical terrain data: dimensions, world size, optional material, and the flat heights array. Required.README.md— instance documentation. Required.
How to create one
-- From a procedural heightmap:
local t = terrain.fromNoise({
width = 64, depth = 64, sizeX = 64, sizeZ = 64,
seed = 7, octaves = 5, amplitude = 8, ridge = true,
})
terrain.toAsset(t, "/source/terrain/rolling_hills.terrain")
-- From a painted heightmap (the demo at
-- /zero/source/demos/terrain_painter saves on the 'S' key):
local painter = require("@me::tools.terrain_painter")
painter.run({ width = 32, depth = 32 })
Loading from an entity:
entity.find("ground").component.add("Terrain", {
terrainAsset = "/source/terrain/rolling_hills.terrain",
material = "@me::materials.grass", -- optional override
})
The Terrain component reads the asset on awake, builds the mesh via
terrain.create, and attaches a MeshInstance so the entity renders.
Changing terrainAsset at runtime rebuilds the mesh and replaces the
previous one.
terrain.json schema
{
"version": 1,
"name": "rolling_hills",
"width": 64,
"depth": 64,
"sizeX": 64.0,
"sizeZ": 64.0,
"originY": 0.0,
"material": "@me::materials.grass",
"heights": [0.0, 0.1, 0.2, ...]
}
version— schema version; currently1. Bump when the layout changes.width/depth— integer grid resolution. Must equallen(heights).sizeX/sizeZ— world dimensions in meters. Defaults towidth-1/depth-1if absent.originY— Y offset added to every vertex (so the asset can describe a sea-level baseline).material— optional default material identity / AssetRef. TheTerraincomponent falls back to this when nomaterialoverride is set on the component instance.heights— flat row-major array, length =width * depth, Lua-indexed (heights[z * width + x + 1]).
Lifecycle / hot-reload
- The
Terraincomponent watchespublic.terrainAssetviaonPropertyChanged. Pointing at a new asset (or changing the asset data on disk in--dev) rebuilds the mesh. - The mesh registered with
renderer.mesh.createcarries a fresh synthetic guid derived from the asset name + an incrementing instance counter, so multiple entities loading the same asset get distinct mesh handles but identical geometry.
Authoring conventions
- Keep
widthanddepthreasonable for in-memory authoring (≤ 256² is comfortable; larger grids should probably switch to a binary heights blob — file an issue if you need that). - Match
sizeX / sizeZto the visual scale your world expects (meters). Decoupling grid resolution from world size lets you bump fidelity without changing in-game scale. - Commit the JSON to source control. Diffs are line-per-row-ish and
readable;
terrain.toAssetwrites one row per line for that reason.
Related types
Model.component— a terrain ultimately renders through aMeshInstance, the same primitiveModeluses. TheTerraincomponent manages that instance on your behalf so the entity carries a single semantic component.Material— terrains use materials like any other mesh. The asset'smaterialfield is just the default; per-instance overrides live on theTerraincomponent.