Terrain
Ground built from a heightmap: a grid of height samples that becomes a lit mesh, a collider and a navigation grid, all from the same data.
component: terrain
Vulkan
WebGPU
Desktop · Android · iOS · Web
awake:asset:terrain holds the backend-free data: Heightmap, mesh building, sculpting, clipmap
rings and splat weights. The terrain scene component, TerrainComponent, lives in
awake:scene:scene3d and is one of the default scene components.
Add a terrain
A 4 × 4 sample hill, 2 m between samples. Both forms below build the same component.
world.scene {
entity("hill") {
with(
TerrainComponent(
heightmap = Heightmap(
samples = floatArrayOf(
0f, 0f, 0f, 0f,
0f, 1.5f, 1f, 0f,
0f, 1f, 0.5f, 0f,
0f, 0f, 0f, 0f,
),
width = 4,
depth = 4,
scale = Vec3f(2f, 1f, 2f),
),
tilingScale = 16f,
),
)
}
}
There is no dedicated DSL function for terrain yet, so attach a TerrainComponent with
with(...).
- In the Hierarchy, click Add entity and choose Environment & World: Terrain Heightmap. Studio adds a 32 × 32 heightmap.
- Select it to edit the terrain in the Inspector.
Studio also saves a terrain_asset component beside terrain, recording which asset files the
terrain was authored from.
Properties
| Property | Type | Default | What it does |
|---|---|---|---|
width |
integer | required | Samples along X, at least 2. |
depth |
integer | required | Samples along Z, at least 2. |
samples |
list of numbers | required | width * depth heights, row by row: index z * width + x. |
scaleX |
number | 1 |
Metres between samples on X. |
scaleY |
number | 1 |
Multiplier applied to every height. |
scaleZ |
number | 1 |
Metres between samples on Z. |
tilingScale |
number | 16 |
How often surface textures repeat across the terrain. |
isVisible |
boolean | true |
Whether the terrain is drawn. |
surface |
object or none | none | Names a surface provider and its data; see How it works. |
TerrainComponent also has splatMap and clipmapConfig, which only Kotlin can set.
Work with a heightmap
A Heightmap is centred on its entity: it spans -halfExtentX..halfExtentX and the same on Z.
val heightmap = Heightmap(
samples = FloatArray(33 * 33) { i -> if (i % 33 in 12..20 && i / 33 in 12..20) 2f else 0f },
width = 33,
depth = 33,
scale = Vec3f(1f, 1f, 1f), // metres between samples on X and Z, and a height multiplier on Y
)
// Centred: this map spans -16..16 on X and Z.
val groundY = heightmap.heightAtWorld(0f, 0f) // NaN outside the map
val up = heightmap.normalAt(0, 0)
val mesh = heightmap.toPositionNormalColorMesh { _, _, height -> if (height > 1f) Color.White else Color.Black }
| Call | What it does |
|---|---|
heightAt(x, z) |
The raw sample at a grid position. |
heightAtWorld(worldX, worldZ) |
Interpolated world height, with scale.y applied. NaN off the map. |
normalAt(x, z) |
The surface normal at a sample. |
toPositionNormalColorMesh(color) / toPositionNormalColorMesh { x, z, height -> color } |
A lit mesh, uniformly coloured or coloured per sample. |
mutableCopy() |
An editable MutableHeightmap with the same origin. |
RawHeightmapCodec.decode(...) / decodeFromBitmap(...) |
Reads raw 8- or 16-bit heights, or a greyscale image. |
Heightmap is immutable. To sculpt, edit a copy and take a snapshot. Both keep the map's origin,
so a corner-anchored map stays where it was:
val editable = heightmap.mutableCopy()
val change = editable.setHeightAt(x = 1, z = 2, height = 3f) // null when nothing changed
val sculpted = editable.snapshot() // immutable again: build a mesh or collider from this
Collide and navigate
The same heights feed the other systems:
- Physics: build a
HeightFieldShapefrom them for a static collider. See Physics. A heightfield is square; a rectangular map needs tiling withheightFieldTile(...). - Navigation:
heightmap.bakeNavGrid()marks the walkable ground. See Navigation.
How it works
TerrainContentSystem draws every TerrainComponent with the built-in terrain shaders. A surface
names a TerrainSurfaceProvider that supplies different shaders and textures; if that provider is
not installed, the terrain draws with the built-in shaders and the scene keeps the surface data
unchanged. A project played with runProject gets it with nothing to wire: on a renderer, its
terrains draw, and the layered-terrain kit's provider is installed. A host that runs the scene's
systems itself passes more providers under CoreSceneContent.TerrainSurfaces. TerrainSurfaceProvider is in awake:terrain, which has no scene dependency, so a
provider is written against that module alone. awake:kit:terrain-layers ships one provider,
awake.terrain.layers, which blends textured layers from a palette and a control map. Each control
texel keeps its four strongest layers, or eight where more than four meet; an eight-layer map costs
about twice as much per pixel, so only terrains that need it pay for it.
For large terrains, TerrainClipmapSystem keeps concentric rings of geometry centred on the primary
camera, so memory stays constant however far the camera travels. TerrainClipmapConfig sets how many
rings there are and how fine they start.
One terrain per shader set
A content host accepts one terrain per shader set. A second terrain with the same shaders is logged and not drawn.
Collider and mesh must agree on the origin
Heightmaps and heightfields are centred by default. Content authored with the corner at the
origin passes GridOrigin.Corner to both, or the collider sits half a map away from the mesh.
See also
- Physics for heightfield colliders.
- Navigation for baking walkable ground.
- Large worlds for streaming terrain in cells.
- Lights and shadows for lighting it.