# Large-world terrain (world.terrain) A large open landscape streamed in chunks around the player, with static colliders: from a map made in the terrain studio, or generated from a seed. Readable offline: `gripforge docs world-terrain`, or the MCP tool `gripforge_docs` (topic `world-terrain`). ## What it is for The kit builds the ground of a game: heights, material layers (grass, rock, snow, sand…), lakes, rivers and sea, scattered vegetation, roads and spawn points. Height, slope, layer and water can be asked anywhere. The same world comes out on the web, Godot, Unity, Unreal and the native player: the height function is ported with integer hashing and no trigonometry, and a baked map is read with the same arithmetic, so heights match across engines. Two ways to make the world: - **a map**: made in the terrain studio (or the terrain node of a scene) and baked into one file the kit plays; - **a seed**: mountains, valleys, cliffs, rivers, lakes and vegetation generated by the kit. This is the world when no map is bound. ## Turn it on ```text gripforge_gamekit_install { project_id: "gkp_…", id: "world.terrain" } gripforge_gamekit_configure { project_id: "gkp_…", id: "world.terrain", config: { "seed": 21, "climate": "alpine", "mountains": 0.9, "waterLevel": 18 } } gripforge_terrain_map_use { project_id: "gkp_…", map: "lvl_…" } # play a studio map instead of the seed ``` It requires `physics.port` and `game.engine`. Only the keys that differ from the defaults need to be written. The full reference is `gripforge kit api world.terrain`. ## Settings and defaults | Key | Default | Meaning | | --- | --- | --- | | `seed` | 1 | the generated world | | `size` | 2048 | metres, square, centred on the origin | | `cellSize`, `chunkSize` | 2, 64 | grid step and chunk side, in metres | | `loadRadius`, `chunksPerTick` | 2, 2 | the built-in streamer (used when `world.streaming` is absent) | | `climate` | `temperate` | `temperate`, `alpine`, `desert`, `tundra`, `tropical`: sets the default layer rules | | `heightScale`, `featureScale` | 140, 700 | highest peaks, width of the big landforms | | `mountains`, `valleys`, `cliffs`, `roughness` | 0.7, 0.5, 0.35, 0.5 | relief (cliffs make terraces and plateaus, good for `movement.traversal`) | | `rivers`, `riverWidth`, `riverDepth` | 3, 14, 3 | rivers meander from inland to the edge and join the lakes | | `water`, `waterLevel`, `waterKind` | `true`, 22, `fresh` | everything under the level is water; `fresh` or `sea` | | `layers` | `[]` | ordered material rules (below); empty = the defaults of the climate | | `vegetation` | `[]` | scatter rules (below) | | `vegetationCoverage` | 1 | share (0 to 1) of trees, bushes and grass that shows | | `groundTextures` | `{ blend: 3, triplanar: "auto", triplanarSlope: 32, layers: [] }` | texture look per layer | | `colliders` | `true` | a static collider per chunk through `physics.port` | A mod can change the generator and look keys (`seed`, `size`, `climate`, `heightScale`, `featureScale`, `mountains`, `valleys`, `cliffs`, `roughness`, `rivers`, `riverWidth`, `riverDepth`, `water`, `waterLevel`, `waterKind`, `layers`, `vegetation`, `groundTextures`, `vegetationCoverage`). With a bound map, `layers`, `vegetation`, the relief options and `waterLevel` are not used; `cellSize`, `chunkSize`, `loadRadius`, `water` and `colliders` still are. ## A map from the terrain studio `gripforge_terrain_map_use` takes `{ map: "lvl_…" }` (a studio map) or `{ scene, terrain }` (a scene terrain node). The map is baked once and bound to the `terrain_map` slot; the project keeps a small reference in `terrain_maps` (bounds, grid, roles, source, pipeline version). The file carries: - **heights**: the studio grid (16-bit samples), read with bilinear interpolation; the world has the map's rectangular bounds; - **ground**: the two dominant roles of each sample and their blend (`layerAt` answers with the layer that draws the role); - **water**: a depth per sample, so lakes and rivers keep their own heights (`waterAt` gives the surface); - **vegetation**: the studio's placed instances on the `tree`, `tree_alt`, `bush`, `rock` and `grass` slots, ids `map:`; - **roads** (polylines with a width) and **spawn / objective points**: `mapInfo()` returns them and `world.ground.defaultSpawn()` is the map's first spawn point. Not carried yet (counted in `terrain_maps[].notCarried`): buildings and other props, architecture meshes, terrain holes and the studio's textures. Regenerate the file after changing the map. At run time `loadMap(bytes | base64)` swaps the world for another baked map (`terrain.map.loaded`). ## Material layers and textures `layers` is an ordered list `{ id, minHeight, maxHeight, minSlope, maxSlope, relativeToWater }`: the first rule that holds paints the ground. Temperate defaults: snow above 95 m over the water, rock on slopes over 38 degrees, sand on the shores, grass elsewhere. Bind a seamless texture per layer to the slot `ground`, variant = the layer id (`ground:sand`, `ground:rock`…). A layer without a texture keeps its colour. `groundTextures.layers` takes `{ id, tileSize, macroScale, macroBlend, bump, roughness, tint }` per layer (`tileSize` 4 m, `macroScale` 5, `macroBlend` 0.5, `bump` 0.35, `roughness` 0.95); `blend` is the width over which two layers fade; `triplanar` projects steep faces from three sides so cliffs are not stretched. Up to 8 textured layers. The low graphics preset draws one sample per layer. Ground textures are drawn by the web and native players; Godot, Unity and Unreal keep their own layer shading. ## Vegetation Rules `{ id, slot, kind, density, layers, maxSlope, minHeight, maxHeight, scale, inWater, material }` scatter instances on a jittered grid, kept when the layer, slope, height and water filters hold. Slots take models from your locker or the community (procedural shapes otherwise); several models on a slot are mixed. Each instance has a stable id `::`; `material` tells `world.elements` what it is made of (wood burns, stone does not). `vegetationCoverage` shows a share of the plants, chosen by a fixed rank per instance, so a land greens gradually and in the same order. It is a live setting (`world.terrain.settings`, key `vegetation`): `world.bindings` can drive it from a variable. ## Streaming and colliders With `world.streaming` installed the terrain follows its chunks; without it the built-in streamer uses `chunkSize`, `loadRadius` and `chunksPerTick`. Each loaded chunk gets a static collider, so characters, vehicles and raycasts stand on it. ## Editing - In the project, the `terrain_edits` collection holds brushes `{ id, op, at, radius, amount, falloff, kinds }` applied in order: `raise`, `lower`, `flatten` (to `amount` metres), `clear` (removes vegetation, optionally only some kinds). `falloff: 0` makes a sheer edge. - At run time `edit(edit)` and `removeEdit(id)` (craters, cleared camps, burnt forests) rebuild the chunks under the brush and emit `terrain.edited`. Run-time edits are saved. ## Service, hooks, events | What | Name | Detail | | --- | --- | --- | | Provides | `world.ground` | `groundY(x, z)`, `bounds()`, `extent()`, `defaultSpawn()` | | Provides | `world.terrain` | `heightAt`, `normalAt`, `slopeAt` (degrees), `layerAt`, `waterAt`, `sample(x, z)` (height, normal, slope, layer, water), `chunks()`, `loaded()`, `vegetationNear(pos, radius)`, `edit`, `removeEdit`, `edits`, `loadMap`, `mapInfo`, `vegetationCoverage(share?)`, `groundTextures()` | | Setting | `world.terrain.settings` | `keys()`, `get(key)`, `set(key, value)`: `vegetation` (0 to 1) | | Hook | `terrain.edit` | `{ id, op, at, radius, amount? }`; a tap changes `radius` and `amount`, or cancels (`terrain.refused` with the reason). Asked only for run-time edits, not for project edits, a loaded save or `removeEdit`. Web and Rust | | Registry | `terrain_edits` | moddable: a mod adds, patches or removes brushes | | Events | `terrain.chunk.loaded { x, z, min, max, vegetation }`, `terrain.chunk.unloaded { x, z }`, `terrain.edited`, `terrain.refused`, `terrain.map.loaded`, `terrain.vegetation.changed { coverage }` | | | Listens | `world.chunk.loaded`, `world.chunk.unloaded`, `elements.burnt` | | ```json { "id": "sanctuary", "hook": "terrain.edit", "if": [{ "var": "zone.protected", "op": "==", "value": true }], "cancel": true, "reason": "protected" } ``` ## With scenes A scene whose `world` is `{ "kit": "world.terrain", "config": { "seed": 21, "climate": "alpine" } }` plays that world, the scene config going over the kit config. When it differs from the world in place, loaded chunks are released, the edits and burnt ground of the old world are kept for when a scene brings it back, and the new chunks load around the player. A bound map stays the ground of every scene (one map slot per project). ## Engines | Engine | How the terrain is drawn | | --- | --- | | Web and native player | chunks with the layer shading, textures, living vegetation | | Godot | a mesh with a layer-blending shader and a heightmap collider per chunk; vegetation as multimeshes; a water plane | | Unity | one terrain per chunk with layers painted from the rules and a terrain collider; vegetation as GPU-instanced meshes | | Unreal | procedural mesh sections with collision per chunk; vegetation as hierarchical instanced meshes | The map file is delivered next to the kit on each engine. Living vegetation and ground textures are web and native features: the engine deliveries show all the vegetation. The `terrain.edit` hook runs on the web and Rust; the older engine files apply every edit. ## When it does not do what you expect | Symptom | Cause and fix | | --- | --- | | `layers` or `waterLevel` have no effect | a map is bound: they only apply to the seeded world | | A cleared camp grows back | the edit was added to the project after the chunk was cut, or `clear` listed other `kinds` | | Characters fall through | `colliders: false`, or the chunk is not loaded yet: listen to `terrain.chunk.loaded` | | Plants of a scene are missing | only studio maps bring plants; a scene's vegetation is not read |