@awaken/terrain
Heightfield terrain as an engine runtime type: the terrain asset, the chunk and LOD mesh builder, the per-chunk vegetation bakes and collision proxies derived from it, the world-space surface query, and the Terrain component. It depends on @awaken/core and @awaken/render (for MeshData).
The package does not generate terrain. Noise, erosion and biomes belong to whatever produces the asset: the editor's terrain plugin (examples/plugins/terrain), an importer, or a script. It holds no GPU or DOM code. For the user-facing workflow see Terrain.
ts
import { makeTerrainAsset, buildTerrainMeshes, TerrainQuery, sceneTerrains, Terrain } from "@awaken/terrain";Only the asset is saved and shipped. AssetStore.installTerrain(id, asset) (in @awaken/assets) rebuilds every derived mesh into the store, so a project or game file carries a heightfield and packed instance lists instead of the vertices they expand to.
heightfield
The grid every query, mesh builder and edit reads. heights is row-major (index = z * resolution + x) and normalized 0..1; a vertex's local Y is heights[i] * height. The grid is centred on the terrain's local origin: vertex (0, 0) sits at (-size/2, -size/2).
| Symbol | Signature | Description |
|---|---|---|
Heightfield | { resolution, size, height, heights: Float32Array } | resolution vertices per side (generators keep it 2^n + 1), size metres per side, height metres a value of 1.0 reaches. |
Rect / EMPTY_RECT / isEmptyRect | { x0, z0, x1, z1 } | Grid-vertex-inclusive bounds; an empty rect has x1 < x0 or z1 < z0. |
growRect | (r, resolution, by = 1) => Rect | Grow a rect by by vertices, clamped to the grid (a vertex normal reads its neighbours, so an edit rebuilds one ring further). |
isValidRect | (r, resolution) => boolean | Bounds check. |
gridStep | (hf) => number | Metres between grid vertices: size / (resolution - 1). |
gridToLocal | (hf, gx, gz) => [x, z] | Grid vertex to terrain-local metres. |
sampleHeight / sampleNormal | (hf, x, z) | Bilinear normalized height, and the surface normal, at local (x, z). |
heightNormal | (hf, gx, gz) => [x, y, z] | Normal at a grid vertex. |
sliceRect / writeRect | (heights, resolution, rect, slice?) | Copy a rect of heights out, or back in (undo of a sculpt stroke). |
asset
| Symbol | Signature / Shape | Description |
|---|---|---|
TerrainAsset | Heightfield & { chunkRes, colors, biome, biomeNames, water, seaLevel, layers } | The saved form. colors is resolution² × 3 u8 at TERRAIN_COLOR_SCALE (or null); biome is resolution² indices into biomeNames; water is per-cell normalized water height, -1 where dry; seaLevel is normalized, -1 for no sea. |
TerrainLayer | { name, proto, count, pos, attr, castShadow, doubleSided, collider, fade } | One scattered layer (trees, rocks, grass): an inline prototype MeshData plus packed instances. pos is count × 3 u16; attr is count × ATTR_STRIDE u8. fade (metres) thins the layer with distance; 0 never thins. |
TerrainInput / TerrainLayerInput | interfaces | What a generator hands over: Float32 heights, optional maps, and layers as INSTANCE_STRIDE-float records. |
makeTerrainAsset | (input) => TerrainAsset | Pack a TerrainInput once. Validates collider proxies and the per-layer vertex budget. |
INSTANCE_STRIDE | 10 | Floats per input instance: x, y, z, scale, yaw, r, g, b, tilt-normal x, tilt-normal z. |
ATTR_STRIDE | 8 | Bytes per packed attribute record: scale (1/32 steps), yaw (1/256 turns), r, g, b, tilt x, tilt z, flags. |
TERRAIN_COLOR_SCALE | 2 / 255 | Colours are u8 multipliers over 0..2. |
DEFAULT_CHUNK_RES / defaultChunkRes | 129 / (resolution) => number | Chunk vertices per side: 129 when that tiles the grid, otherwise the whole terrain is one chunk. |
MAX_LAYER_VERTICES | 12_000_000 | Vertices one layer may bake to across the terrain (instances × prototype vertices). |
packInstances / unpackInstance | helpers | Records to u16 positions + u8 attributes, and one instance back to floats. |
packColors / unpackColors | helpers | Float colours to u8 and back. |
terrainAssetBytes | (t) => number | Byte size of an asset's typed arrays. |
chunks
A terrain is cut into square chunks of chunkRes vertices. Every chunk carries LOD_LEVELS (3) meshes: every vertex, every 2nd, every 4th. Where a neighbour can be drawn at a different level, a skirt (a short wall straight down from the border) hides the crack; an edge whose depth is 0 gets no skirt. LOD switching itself is the engine's LodGroup component; this module only builds the meshes.
| Symbol | Signature | Description |
|---|---|---|
chunkCount | (resolution, chunkRes) => number | Chunks per side. |
chunkMesh | (hf, cx, cz, chunkRes, level, skirtDepth, colours) => MeshData | One chunk's mesh at one level, relative to chunkOrigin. Normals come from the whole heightfield, so neighbours agree at the border. |
chunkOrigin / chunkWorldSize / chunkAt | helpers | A chunk's centre in local metres, its side length, and which chunk holds a point. |
rectToChunks | (rect, resolution, chunkRes) => { cx, cz }[] | Chunks an edit rect touches. |
chunkLodError / chunkLodDistances | (hf, cx, cz, chunkRes, …) | The level's height error, and the distances at which levels 1 and 2 switch in so the error stays under LOD_ERROR_RADIANS on screen. |
LOD_ERROR_RADIANS | 0.0015 | About 2 pixels at 1080p over a 60° field of view. |
chunkSkirtEdges, SKIRT_N/S/W/E | (hf, chunkRes, level) => Float32Array | Skirt depth per chunk edge, in that N/S/W/E order: the deepest crack a reachable level can open there. |
instances and collision
| Symbol | Signature | Description |
|---|---|---|
bakeLayerChunk | (asset, layer, cx, cz, stride = 1) => MeshData | null | One layer's instances inside one chunk merged into one mesh (yaw, tilt onto the surface normal, scale, tint, position). |
bakeInstances | (proto, instances, keep?) => MeshData | Merge instance records into one mesh. |
layerChunkCounts | (asset, layer) => Uint32Array | Instances per chunk. |
LAYER_LOD_STRIDES | [1, 4, 16] | A fading layer keeps every instance, every 4th, then every 16th. |
instanceRotator / instancePlacer | (inst) => InstanceWriter | Transform a prototype direction or point by one instance. |
LayerCollider | { kind: "cylinder" | "box", radius, height } | The proxy one instance collides as, in prototype units: a 6-sided trunk prism or a box. null on a layer means walk-through (grass). |
chunkColliderMesh | (asset, cx, cz) => MeshData | null | Every proxy in one chunk merged into one static trimesh. |
colliderTriangles / terrainColliderTriangles | helpers | Proxy cost: 24 triangles per cylinder, 12 per box. |
build
The id scheme here is the contract between the entities a generator creates and the meshes a loader puts back.
| Symbol | Signature | Description |
|---|---|---|
chunkMeshId | (id, cx, cz, level) => string | <id>_c<cx>_<cz>_l<level>. |
layerMeshId | (id, li, cx, cz, level = 0) => string | <id>_veg<li>_c<cx>_<cz>, plus _l<level> above level 0. |
chunkColliderId | (id, cx, cz) => string | <id>_col_c<cx>_<cz>. |
isTerrainMeshId | (id, meshId) => boolean | Whether a mesh id is derived from terrain id. |
buildTerrainMeshes / buildTerrainColliders | (asset, id) => Generator<{ id, data }> | Every derived render mesh, and every collision proxy mesh. |
terrainMeshIds / terrainColliderIds | (asset, id) => string[] | The ids those generators yield (what a save or export leaves out). |
terrainChunkLayout | (asset, id) => TerrainChunkLayout[] | Per chunk: grid coords, local origin, LOD mesh ids and distances, the layer bakes it holds and its collision mesh id, for the generator to build entities from. |
rebuildTerrainRect | (asset, id, rect) => { id, data }[] | Only the meshes a sculpt rect changed. |
query
| Symbol | Signature | Description |
|---|---|---|
TerrainQueryLike | { heightAt, normalAt, biomeAt, waterAt } | The shape a script host exposes as api.terrain. @awaken/runtime declares the same shape (TerrainApi) rather than importing this package. |
TerrainQuery | class implements TerrainQueryLike | Answers in world space through each terrain entity's transform, so a moved, turned or scaled terrain answers correctly. new TerrainQuery(source, version?); caches resolved terrains until refresh() or until version() (the player passes World.structureRev) changes. The first terrain whose footprint holds the point answers. |
sceneTerrains | (world, assetOf) => TerrainEntry[] | Every enabled Terrain component whose asset resolves, with its world matrix. |
waterAt returns the highest of the cell's own water level and the sea level where the ground lies below the sea, or null on dry ground. normalAt transforms by the normal matrix, so a non-uniformly scaled terrain reports the slope it draws.
component
| Symbol | Shape | Description |
|---|---|---|
Terrain | ComponentType<{ asset: string }> | Ties an entity to a terrain asset. asset is read-only in the Inspector: the tool that built the terrain sets it. Fields: Component Reference. |
Persistence
@awaken/assets stores the asset through packTerrain / unpackTerrain (terrainSerde.ts), one packer shared by the .awaken project, the ship container and the JSON scene file. Heights are stored as u16 (1/65535 of height, 6 mm on a 400 m climb) and re-quantizing a value already on the grid returns the same value, so repeated saves never drift. See Data Formats.
See also
- Terrain - building and editing terrain in the editor.
- @awaken/assets -
AssetStore.installTerrainand terrain persistence. - Component Reference - the
TerrainandLodGroupfields. - Scripting API -
api.terrainheight, normal, biome and water queries.