Log inGet started

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 .terrain asset 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 .terrain suffix 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; currently 1. Bump when the layout changes.
  • width / depth — integer grid resolution. Must equal len(heights).
  • sizeX / sizeZ — world dimensions in meters. Defaults to width-1 / depth-1 if absent.
  • originY — Y offset added to every vertex (so the asset can describe a sea-level baseline).
  • material — optional default material identity / AssetRef. The Terrain component falls back to this when no material override 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 Terrain component watches public.terrainAsset via onPropertyChanged. Pointing at a new asset (or changing the asset data on disk in --dev) rebuilds the mesh.
  • The mesh registered with renderer.mesh.create carries 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 width and depth reasonable 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 / sizeZ to 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.toAsset writes one row per line for that reason.
  • Model.component — a terrain ultimately renders through a MeshInstance, the same primitive Model uses. The Terrain component manages that instance on your behalf so the entity carries a single semantic component.
  • Material — terrains use materials like any other mesh. The asset's material field is just the default; per-instance overrides live on the Terrain component.
  • asset-type
  • reference