Terrain

Usage guide of world.terrain: a studio map or a seed, settings and defaults, material layers and ground textures, vegetation rules and living coverage, streaming and colliders, edits, service, the terrain.edit hook, scenes, engines.

Source: module-kits:kits/world_terrain/usage/en.mdView Markdown source

Also read offline — CLI: grip-forge docs world-terrainMCP tool: gripforge_docs { "topic": "world-terrain" }

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 #

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 #

KeyDefaultMeaning
seed1the generated world
size2048metres, square, centred on the origin
cellSize, chunkSize2, 64grid step and chunk side, in metres
loadRadius, chunksPerTick2, 2the built-in streamer (used when world.streaming is absent)
climatetemperatetemperate, alpine, desert, tundra, tropical: sets the default layer rules
heightScale, featureScale140, 700highest peaks, width of the big landforms
mountains, valleys, cliffs, roughness0.7, 0.5, 0.35, 0.5relief (cliffs make terraces and plateaus, good for movement.traversal)
rivers, riverWidth, riverDepth3, 14, 3rivers meander from inland to the edge and join the lakes
water, waterLevel, waterKindtrue, 22, fresheverything under the level is water; fresh or sea
layers[]ordered material rules (below); empty = the defaults of the climate
vegetation[]scatter rules (below)
vegetationCoverage1share (0 to 1) of trees, bushes and grass that shows
groundTextures{ blend: 3, triplanar: "auto", triplanarSlope: 32, layers: [] }texture look per layer
colliderstruea 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:<n>;
  • 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 <rule>:<i>:<j>; 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 #

WhatNameDetail
Providesworld.groundgroundY(x, z), bounds(), extent(), defaultSpawn()
Providesworld.terrainheightAt, 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()
Settingworld.terrain.settingskeys(), get(key), set(key, value): vegetation (0 to 1)
Hookterrain.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
Registryterrain_editsmoddable: a mod adds, patches or removes brushes
Eventsterrain.chunk.loaded { x, z, min, max, vegetation }, terrain.chunk.unloaded { x, z }, terrain.edited, terrain.refused, terrain.map.loaded, terrain.vegetation.changed { coverage }
Listensworld.chunk.loaded, world.chunk.unloaded, elements.burnt
{ "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 #

EngineHow the terrain is drawn
Web and native playerchunks with the layer shading, textures, living vegetation
Godota mesh with a layer-blending shader and a heightmap collider per chunk; vegetation as multimeshes; a water plane
Unityone terrain per chunk with layers painted from the rules and a terrain collider; vegetation as GPU-instanced meshes
Unrealprocedural 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 #

SymptomCause and fix
layers or waterLevel have no effecta map is bound: they only apply to the seeded world
A cleared camp grows backthe edit was added to the project after the chunk was cut, or clear listed other kinds
Characters fall throughcolliders: false, or the chunk is not loaded yet: listen to terrain.chunk.loaded
Plants of a scene are missingonly studio maps bring plants; a scene's vegetation is not read