Skip to content

@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).

SymbolSignatureDescription
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) => RectGrow 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) => booleanBounds check.
gridStep(hf) => numberMetres 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 ​

SymbolSignature / ShapeDescription
TerrainAssetHeightfield & { 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 / TerrainLayerInputinterfacesWhat a generator hands over: Float32 heights, optional maps, and layers as INSTANCE_STRIDE-float records.
makeTerrainAsset(input) => TerrainAssetPack a TerrainInput once. Validates collider proxies and the per-layer vertex budget.
INSTANCE_STRIDE10Floats per input instance: x, y, z, scale, yaw, r, g, b, tilt-normal x, tilt-normal z.
ATTR_STRIDE8Bytes per packed attribute record: scale (1/32 steps), yaw (1/256 turns), r, g, b, tilt x, tilt z, flags.
TERRAIN_COLOR_SCALE2 / 255Colours are u8 multipliers over 0..2.
DEFAULT_CHUNK_RES / defaultChunkRes129 / (resolution) => numberChunk vertices per side: 129 when that tiles the grid, otherwise the whole terrain is one chunk.
MAX_LAYER_VERTICES12_000_000Vertices one layer may bake to across the terrain (instances × prototype vertices).
packInstances / unpackInstancehelpersRecords to u16 positions + u8 attributes, and one instance back to floats.
packColors / unpackColorshelpersFloat colours to u8 and back.
terrainAssetBytes(t) => numberByte 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.

SymbolSignatureDescription
chunkCount(resolution, chunkRes) => numberChunks per side.
chunkMesh(hf, cx, cz, chunkRes, level, skirtDepth, colours) => MeshDataOne chunk's mesh at one level, relative to chunkOrigin. Normals come from the whole heightfield, so neighbours agree at the border.
chunkOrigin / chunkWorldSize / chunkAthelpersA 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_RADIANS0.0015About 2 pixels at 1080p over a 60° field of view.
chunkSkirtEdges, SKIRT_N/S/W/E(hf, chunkRes, level) => Float32ArraySkirt depth per chunk edge, in that N/S/W/E order: the deepest crack a reachable level can open there.

instances and collision ​

SymbolSignatureDescription
bakeLayerChunk(asset, layer, cx, cz, stride = 1) => MeshData | nullOne layer's instances inside one chunk merged into one mesh (yaw, tilt onto the surface normal, scale, tint, position).
bakeInstances(proto, instances, keep?) => MeshDataMerge instance records into one mesh.
layerChunkCounts(asset, layer) => Uint32ArrayInstances per chunk.
LAYER_LOD_STRIDES[1, 4, 16]A fading layer keeps every instance, every 4th, then every 16th.
instanceRotator / instancePlacer(inst) => InstanceWriterTransform 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 | nullEvery proxy in one chunk merged into one static trimesh.
colliderTriangles / terrainColliderTriangleshelpersProxy 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.

SymbolSignatureDescription
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) => booleanWhether 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 ​

SymbolSignatureDescription
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.
TerrainQueryclass implements TerrainQueryLikeAnswers 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 ​

SymbolShapeDescription
TerrainComponentType<{ 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 ​

Awaken: browser-native WebGPU game engine.