Log inGet started
What it does

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 renderable mesh via the terrain.* Luau API.

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.

Related types

  • 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.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

This asset doesn't declare a schema or structured payload.

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

0items
This part has no composite children. See the Files segment for its leaf payloads.
This part has no leaf payloads.

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.