Skip to content

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.

hill.scene.json
{
  "version": 1,
  "nodes": [
    {
      "name": "hill",
      "components": [
        {
          "component": "terrain",
          "width": 4,
          "depth": 4,
          "scaleX": 2.0,
          "scaleY": 1.0,
          "scaleZ": 2.0,
          "samples": [
            0.0, 0.0, 0.0, 0.0,
            0.0, 1.5, 1.0, 0.0,
            0.0, 1.0, 0.5, 0.0,
            0.0, 0.0, 0.0, 0.0
          ],
          "tilingScale": 16.0
        }
      ]
    }
  ]
}
Kotlin
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(...).

  1. In the Hierarchy, click Add entity and choose Environment & World: Terrain Heightmap. Studio adds a 32 × 32 heightmap.
  2. 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.

Kotlin
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:

Kotlin
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 HeightFieldShape from them for a static collider. See Physics. A heightfield is square; a rectangular map needs tiling with heightFieldTile(...).
  • 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