terrain
Heightmap terrain substrate. Build a mesh from a flat heightmap, sample
it, save / load it as a .terrain asset, optionally spawn an entity in
one call. Wraps the Internal __terrain.* FFI binding (FBM noise,
mesh tessellation, bilinear sample) into one ergonomic Luau surface.
Exposed as the global terrain via --!global terrain; the
higher-level system surface (terrainSystem.*) lives in
~terrainSystem.modules.terrainSystem.
Exports
Build verbs:
terrain.create(opts) -> TerrainHandle?— heightmap → handle.terrain.fromNoise(opts) -> TerrainHandle?— FBM noise → handle.terrain.heightmap(opts) -> { number }?— pure FBM heightmap step.terrain.spawn(opts) -> (entityId, TerrainHandle)— build + spawn entity with a Model component (and optional material) in one call.
Queries (call with the handle returned by a build verb):
terrain.heightAt(handle, x, z) -> number?— bilinear height withhandle.originYfolded in.terrain.normalAt(handle, x, z) -> (nx, ny, nz)— three numbers.terrain.slopeAt(handle, x, z) -> number?— slope in radians.
Asset round-trip:
terrain.fromAsset(source) -> TerrainHandle?— read a.terrainasset.terrain.toAsset(handle, path, opts?) -> (boolean, string?)— write a.terrainasset.
Types
type TerrainHandle = {
mesh: any, -- renderer-ready AssetRef envelope
heights: { number }, -- flat width*depth heightmap, row-major
width: number, -- grid resolution along +X
depth: number, -- grid resolution along +Z
sizeX: number, -- world width
sizeZ: number, -- world depth
originX: number, -- world center X (mesh centered here)
originY: number, -- world Y offset added to every vertex
originZ: number, -- world center Z
name: string?, -- mesh asset name when one was supplied
material: any?, -- asset's default material (when loaded)
}
Heightmap convention: row-major, Lua-indexed.
heights[z * width + x + 1] is the height at grid cell (x, z).
Usage
Build from a Luau-computed heightmap:
local h = terrain.create({
heights = myFlatTable,
width = 64, depth = 64,
sizeX = 80, sizeZ = 80,
})
Build a noise terrain in one call:
local h = terrain.fromNoise({
width = 128, depth = 128, sizeX = 100, sizeZ = 100,
seed = 7, octaves = 5, frequency = 0.02, amplitude = 12,
ridge = true, bowl = 0.6,
})
Build + spawn an entity in one call:
local eid, h = terrain.spawn({
width = 64, depth = 64, sizeX = 80, sizeZ = 80,
entityName = "ground", material = "@me::materials.grass",
})
Sample for placement:
local y = terrain.heightAt(h, 12.5, -3.0)
local nx, ny, nz = terrain.normalAt(h, 12.5, -3.0)
local slope = terrain.slopeAt(h, 12.5, -3.0)
Round-trip via the .terrain asset format:
local ok, path = terrain.toAsset(h, "/source/terrain/island.terrain")
local h2 = terrain.fromAsset("/source/terrain/island.terrain")
Notes
- The heavy work — mesh tessellation, central-difference normals, FBM
noise, bilinear sample — runs in Rust through
__terrain.*. This module is the thin Luau adapter; user code should never call__terrain.*directly. terrain.spawnadds onlyModel+Material— no collider. Add a Collider component to the returned entity if you need terrain-vs-physics interaction:entity(eid).component.add("MeshCollider", { fit = "exact" }).- Determinism: same
seed→ identical heights on every platform (native / WASM / Android). The FBM uses an integer hash, not the OS RNG.
Scoped to this part · feeds back into the world's score.