Generate heightfields, paint biomes, sculpt adjustments and preserve edits across regeneration.
Terrain assets and editing
A Terrain asset owns its generation graph, biome settings and manual edits. Scenes reference it through Terrain3D.asset and place it with Transform 3D. The editor generates and edits its preview; shipped games load prepared terrain data. This is an experimental finite-heightfield workflow; large-world streaming and volumetric caves are not yet available.
Generate a terrain
- Choose Assets → Create → Terrain... to create a Terrain asset. Double-click its source or manifest to open its own terrain stage, independently of the scene.
- Choose Open Terrain Graph. Its generation settings contain the X/Z size in world units, resolution in cells, seed and chunk size. Resolution determines surface detail; chunk size controls how the surface is split into meshes.
- Adjust landforms, biomes, rules, palette and connected graph modules there, then press Generate or Regenerate. Settings remain an unapplied draft until you do this.
- Inspect the result in the viewport. Generation runs in the background with progress and cancellation. The last completed surface remains visible while it runs.
- Save the asset normally. Undo/Redo restores its authored recipe and preview. Drag the asset from Assets into a 3D scene to place it; the scene stores a reference, not a recipe copy.
An empty Terrain component is a placeholder until an asset or an explicit procedural recipe is assigned. Terrain-local X/Z coordinates run from zero to the configured size; use the scene entity's transform to position it. Generated meshes and static triangle-mesh colliders are split into chunks and biome groups. One logical Terrain asset therefore has multiple native render/collision objects.
Select the terrain owner and press F over the viewport to frame its full surface. Generated chunks are hidden from the authored hierarchy. Generation configuration belongs to the Terrain Graph workspace; the Terrain component Inspector contains the viewport brush tools.
Edit a placed terrain
Select a scene entity with a Terrain3D.asset reference. Sculpt and paint it directly in the scene Viewport, using the Terrain component's brush controls. There is no need to open an isolated asset stage. Open Terrain Graph edits the same shared asset's generation draft.
Use Apply changes to asset to save the edited recipe and prepared surface. Edits affect other placements of that asset; their transforms remain independent. The scene continues to store its asset reference, without embedding an override recipe. Duplicate the source asset first if a placement needs a different terrain. The standalone Terrain asset stage remains available for working in isolation.
Selecting an unrelated scene object does not discard pending terrain edits. Before editing a different Terrain asset, apply or undo the current asset's changes and generate or discard its pending settings. Scene placement edits and terrain source edits have separate histories.
Terrain module graph
Try demi editor --project examples/terrain_graph_3d for a connected landscape example: mountain, foothill and lowland noise branches blend through elevation masks, followed by drainage-fed erosion, lake/river carving and biome rules. Its terrain can then be painted and sculpted in the Viewport. Connected water outputs now display transparent water surfaces.
Water positions are terrain-local coordinates. Moving the terrain entity moves its water; changing terrain dimensions does not scale water-body positions.
A lake fills the submerged region connected to its centre within its radius. If the centre is dry, the nearest wet terrain sample selects the basin. Separate depressions behind land at or above the water level stay dry; add another Water Body node to fill them. Oceans cover all submerged regions in their footprint, and rivers follow their authored paths. Connectivity is computed during generation or terrain edits and saved with prepared terrain, not searched again each rendered frame.
The graph lives in the Terrain asset's recipe.graph. Select its scene placement or open the asset, then choose Open Terrain Graph in the Inspector. Global recipe settings and node parameters are edited in this workspace, with one authoritative recipe rather than separate Inspector and graph copies. Use Starter graph for a working chain, or drag terrain cards from the independent Terrain Nodes panel onto the canvas. Each card has a stable type; moving a node changes layout, while connections and parameters determine generation.
Opening the graph pins it to that terrain's stable owner and source document. Changing Inspector selection does not switch the graph to another terrain or discard its draft. Graph shortcuts also work from Terrain Nodes. To open another terrain's graph, generate or discard the current draft and finish any pending generation or brush stroke first. Select the bound terrain when using its Viewport brushes; editing the graph does not force scene selection back to that terrain.
Connect an output pin to an input pin of the same kind. Pins carry a heightfield, mask, drainage result, water result or instance placements. An input accepts one connection, cycles and incompatible links are rejected, and required inputs must be connected before generation. The graph names the Terrain Output node by its stable node ID; an unconnected or invalid draft shows diagnostics. You can keep assembling an incomplete draft, use Undo graph or Redo graph, and press Generate when it is ready. Generation validates and evaluates the draft before replacing the committed terrain; the previous surface stays available if generation fails.
The registry currently provides these modules:
- Generators: Landform uses the recipe's landscape settings; Noise terrain makes a seeded heightfield; Flat terrain makes a constant-height base.
- Masks: Elevation mask and Slope mask select height or steepness bands. Connect a mask to Height Blend or Height Offset to limit its effect.
- Modifiers: Height Blend mixes two heightfields; Height Offset raises or lowers one. Their inputs must share the same terrain grid.
- Simulation: Drainage computes flow and basins for a surface; Erosion changes its heights and can take matching drainage as an optional input.
- Appearance and placement: Biome Rules assigns rule-based biomes to its connected field; painted region overrides are applied after graph evaluation. Scatter Palette generates deterministic instances from a resolved asset palette.
- Water and output: Water Body carves a finite lake, ocean or authored river path and produces a heightfield plus water data. Terrain Output publishes the connected heightfield and optional water and instances.
The graph produces the procedural base. The terrain's authored regions, sculpt edits, exclusions and protection stay in separate surface layers and are replayed when you Generate again; they do not turn into graph nodes. A size or resolution change with existing edits opens the editor's keep-or-clear decision, and protection snapshots cannot be moved to a different grid. Water Body computes carved terrain and water-surface data for finite authored bodies. Connect its water output to Terrain Output to display the surface.
When chaining Water Body nodes, connect both the terrain output and the earlier node's water output to the next node's Existing water input. This preserves the earlier bodies while carving the new one. Connect the final water result to Terrain Output. Connect the same Water output to Biome Rules and Scatter Palette's optional Water inputs. Biome Rules then derives wet substrate and shoreline distance from that finite body's resolved coverage; Scatter Palette rejects submerged candidates before spacing and placement budgets. Connected water must match the input terrain grid. If downstream erosion or height operations change its ground, the node refreshes coverage from the same water bodies without carving again. Without a Water connection, these nodes retain the recipe's inferred sea-level context. Terrain Output still needs its Water connection to publish surfaces, queries and final scatter filtering.
Scroll the mouse wheel over the canvas to zoom around the cursor; middle-drag pans. The zoom percentage is view state, not part of the terrain recipe. 100% resets the scale without changing authored node coordinates.
Terrain Nodes uses cards with an icon, title and description. Drag any part of a card onto the graph canvas and release to insert one node at that position; hovering does not change the graph. Dropping accounts for canvas zoom and panning, and insertion can be undone. The optional local node palette uses the same cards and also supports click-to-insert.
Configure connections and the active output
Each node separates Inputs, Parameters and Outputs. An input's source dropdown lists compatible outputs from other nodes. Choose a source to connect or replace a wire; choose None to disconnect it. Replacing a connection is one Undo step. Required inputs must be connected before Generate. Invalid cycles are rejected without changing the draft.
On a Terrain Output node, select Active terrain output to publish that branch. A node's port types are defined by its operation: a terrain surface connects to a terrain-surface input, while water and instance results use their matching inputs.
Create materials and palettes in the editor
- Choose Assets → Create → Material. Material sets and palettes are under Assets → Create → Terrain.
- A surface material starts with native defaults. Material sets require an initial surface material; palettes require a Model3D asset or a prefab with a compatible asset. The dialog offers project references instead of inventing a sample binding.
- Open the resulting asset. Its Editor tab provides color/scalar controls, texture-map slots, material-role bindings or full placement-role settings, including scale, spacing, collision, LOD and biome filters.
- Use Undo/Redo for edits and Save to update source and reimport its metadata. Native asset dependencies are derived during import/reimport; existing manual declarations remain intact. Prefabs remain prefab references, not asset-ID dependencies.
- Select a renderable Material asset or a scatter palette in Terrain Graph settings. The same Material asset can shade biome meshes and ordinary objects; compatible GPU terrain supports optional smooth surface blending.
If metadata reimport fails after the source saves, the editor reports Import pending. Correct the reported issue and retry Save; the pending state remains available for recovery.
Edit colors, biome rules and asset placement
- On Biome Rules, press Colors and biome rules.... The settings overlay opens the terrain's shared biome definitions and rules.
- Edit a color swatch or enter Hex RGBA, including alpha. Use the colour picker's RGB/HSV/Hex options for numeric channels.
- Press Generate to apply the draft to the preview. Connect Biome Rules into the branch you publish if its rules should assign the surface's biomes.
Scatter Palette → Choose asset palette... opens placement settings, and Landform → Landscape shapes... opens landform definitions. These controls edit the same recipe as All landscape settings; they do not create separate copies of colors, rules or palettes. Recipe sections collapse independently. Shared Material assets provide surface maps and projection settings; compatible GPU terrain supports blended material layers.
Open the example directly in this view:
./build/linux-release/demi-editor --project examples/terrain_graph_3d --terrain-graph
./build/linux-release/demi-editor --project examples/terrain_graph_3d --terrain-settings-node biome_rules
The graph toolbar uses action icons with descriptive tooltips and your current shortcut bindings. Ctrl-click or drag a selection box to select several nodes. Copy/paste and duplicate retain their parameters and internal links while assigning fresh IDs. Cut, paste, multi-delete and multi-node movement each form one graph-draft Undo step. Shortcuts act on the focused graph; text fields keep normal editing. Configure mappings and alternatives in Edit → Editor Settings → Keyboard shortcuts. See workspace shortcuts.
The canvas uses the whole Terrain Graph window width. Recipe settings float over it; collapse their header to leave only the title strip, or use Hide settings to remove them entirely. Scrolling over settings scrolls that panel without zooming the graph. Show local modules opens an optional local palette; the searchable Terrain Nodes window can be docked separately. Terrain Graph itself can sit beside Viewport so you can inspect the generated terrain while editing its graph. Node footers show the last generation's CPU time or whether its result was reused.
Source assets, caching and shipping
The editable source is a *.terrain.json document with format_version, a stable asset:// ID and a recipe. Its ordinary *.asset.json manifest uses type Terrain and importer terrain_heightfield. Generated previews live in the platform cache directory, not in the developer's asset folder.
{
"id": "landscape",
"components": {
"Transform3D": { "position": [0, 0, 0] },
"Terrain3D": { "asset": "asset://terrain/landscape" }
}
}
Several scene entities can reference the same Terrain asset while using different placement transforms. The recipe remains shared. Editing the asset does not add an override recipe to each scene instance.
Cook and package builds produce a *.terrain.bin payload and rewrite the cooked asset manifest to reference it. The payload includes the prepared surface, edit checkpoint, biome data, exclusions, water data and scatter placements. Runtime loading decodes that data and creates native render/collision resources; it does not evaluate noise, erosion, water carving or scatter selection. Missing or corrupt payloads produce an error rather than silently generating replacement terrain.
demi cook --project examples/terrain_graph_3d \
--output build/terrain-graph-cooked --platform linux
demi run --project build/terrain-graph-cooked
Development tools can prepare missing or stale source-asset caches before running a project. Warm source loads reuse prepared results; normal shipped projects load their binary payloads. Source edits and dependency changes invalidate preparation through the shared asset/cook services.
Terrain3D.lod_error_pixels sets the maximum projected surface error for render detail selection. It defaults to 2 pixels; smaller values retain more detail, and 0 disables render LOD. The value must be finite and nonnegative. Each camera reads the terrain owner's current value. Changing this setting preserves the generated heightfield and collision geometry; it does not change terrain residency or gameplay scatter. The Terrain Inspector exposes the same setting.
The allowance uses the originating camera's presentation viewport pixels. Shadow views use that camera's detail decision, and reduced render resolution does not loosen the allowance. Rough, sparsely sampled terrain may stay at full detail even at a distance. LOD does not extend the camera's far_clip: choose a far plane that covers the landscape you want to see. GPU mesh eviction only retires unused render resources; it does not remove nearby terrain or its collision.
On supported devices, compatible opaque terrain uses shared grid patches. Vertex shaders sample height and normal textures, and compatible patches are submitted in instanced batches. Solid palettes blend colors in linear light together with metallic, roughness and lit/unlit contributions. Mapped Materials use five packed texture arrays for albedo, normal, ambient-occlusion/roughness/metallic/height, detail normal and emissive. Triplanar projection covers steep faces; height maps bias existing layer weights, and macro variation modulates albedo. Neither changes the heightfield or collision.
Material IDs are decoded exactly before the four neighboring field samples contribute to shading. IDs and face-culling flags are never interpolated. Four neighboring contributors are not a four-role limit on a material set; source layers are constrained by actual device array limits and memory. The mapped path requires 12 sampler slots plus texture arrays, vertex texture sampling and instancing. Unsupported devices, custom shaders, transparency/cutout, exhausted budgets or unavailable resources use ordinary meshes with a reported fallback reason. Live RenderTarget maps and different image wrap modes within one Material also require fallback; separate Materials may each use repeat, clamp or mirror.
Select the terrain entity and use Inspector → Terrain 3D → Surface Blending to compare smooth and sharp boundaries. It defaults to enabled and applies to solid and mapped GPU layers. Smooth mode interpolates neighboring contributors over a terrain cell, with optional material height bias. Sharp mode selects one material per original cell triangle, using the majority of its three corner materials and the first corner for ties.
The scene/prefab field Terrain3D.surface_blending is a boolean on each placement, so two entities sharing an asset can choose different looks. Editing it is undoable and does not regenerate terrain, replace collision or change LOD. The ordinary mesh fallback remains discrete and renders each assigned PBR material; it does not reproduce GPU terrain height-weight blending or macro variation. Boundaries may therefore look different during preparation or fallback.
Field texture data and compatibility-path reduced meshes are prepared on worker threads. Until resources are ready, full-detail meshes remain visible. Environment3D.terrain_lod_cache_mb sets the reduced-mesh CPU payload budget and the separate GPU-field staging allowance in MiB (default 64). terrain_lod_prepare_jobs limits each preparation queue's outstanding and newly started jobs per presentation (default 2); zero pauses new preparation while valid ready resources remain usable. These are per-renderer queue/cache budgets, not a combined process-memory limit. Shared heightfields, full-detail meshes, collision and allocator overhead are outside the tracked payload budget.
Environment3D.terrain_gpu_cache_mb independently budgets GPU terrain field textures and shared patch buffers in MiB (default 128). Zero disables GPU patches and selects ordinary mesh rendering. Unchanged height/normal channels can be shared across appearance edits; changed channels are transactionally replaced. Field-size or palette limits follow actual device texture capabilities and fall back without imposing a smaller authored terrain limit. Source heightfields, picking and full-detail collision remain authoritative.
Material texture quality is separate from terrain geometry. Array slots with no source maps use 1×1 neutral layers; actual maps retain their selected resolution. Environment3D.terrain_texture_max_size defaults to 1024 and accepts powers of two from 64 to 8192. It caps array resolution without changing source images; actual image dimensions, smaller device limits and the complete array bundle’s memory further constrain the tier. terrain_texture_cache_mb budgets material arrays in MiB (default 128); zero disables arrays and selects ordinary mesh materials.
terrain_detail_distance is a positive finite world distance (default 200). Detail normals fade from half this distance to zero at the full distance. The nearest visible chunk requests the full texture tier within this distance, half the configured edge beyond it, and one quarter beyond four times the distance. Cameras sharing materials retain the highest demand from the current and preceding presentation, so a nearby view can keep a finer tier resident for a distant view. Ready tiers remain usable while replacements prepare; when no valid array tier is available, full-detail ordinary meshes remain visible. These settings do not reduce collision detail or impose a source image size limit.
For a deliberately procedural game, an entity can instead specify Terrain3D.recipe inline. That explicitly requests runtime generation. asset and recipe cannot be supplied together.
Landforms and biomes
A landform is the shape of the ground. A biome is how that ground looks. They are authored separately, so recolouring a surface never changes the terrain and reshaping a hill never changes its tint.
A landforms object is keyed by name. Each shape uses base_height, height_variation, feature_size, roughness and octaves, with optional noise-profile and coordinate controls below. The recipe selects its fallback shape with default_landform. The Noise terrain graph node exposes the same controls for its own source; a Landform node reads the recipe's named shapes.
| Control | Default | Meaning |
|---|---|---|
profile | fbm | fbm produces smooth fractal relief; ridged produces sharper ridge structures. |
warp_amplitude | 0 | Displaces sampling coordinates by this distance in terrain-local world units. Zero disables domain warp. |
warp_feature_size | 128 | Width of the coordinate-warp features in terrain-local world units; must be positive. |
stretch | 1 | Elongates features along the heading. Values above one stretch them; the value must be positive. |
heading | 0 | Angle from terrain-local +X toward +Z, in degrees. At zero, stretch acts along X. |
Domain warp runs before heading and stretch. These controls affect the shape, not its biome tint or material. Values must be finite; warp amplitude is nonnegative. Unsupported combinations of scale, warp and octave count report a sampling-domain diagnostic rather than producing invalid heights. Omitting the new controls preserves the existing FBm output; new controls at their defaults are omitted from canonical landform JSON.
{
"format_version": 1,
"default_landform": "mountains",
"landforms": {
"mountains": {
"profile": "ridged",
"height_variation": 450,
"feature_size": 650,
"roughness": 0.35,
"octaves": 4,
"warp_amplitude": 100,
"warp_feature_size": 500,
"stretch": 2,
"heading": 30
}
}
}
A biomes object is keyed by name and each entry takes a color, an optional ordinary material asset, a texture_scale, and an optional landform naming the shape its ground uses. Leave landform out to use the recipe's default. Several biomes can share one landform, and one landform can serve several looks, so a shared rocky shape costs nothing to reuse.
The two are edited in the Terrain Graph's generation settings: Landforms for elevation and relief, Biomes for tint and landform choice.
Textures, scenery and water: current support
In Terrain Graph settings, open a biome’s properties and choose Render material (Material) for an explicit surface override, or use a material set for automatic roles. Shared Materials provide albedo, normal, roughness, metallic, ambient occlusion, detail normal and emissive maps in preview, Game View and cooked runtime. Compatible terrain uses GPU arrays; ordinary mesh fallback retains discrete assignments. Biome color is still a tint: use [1,1,1,1] for the material’s appearance without tinting.
Automatic snow follows elevation and slope only where the biome permits it. Set allow_snow: false on warm or dry biomes to prevent automatic snow; the omitted default is true. This affects surface classification, not elevation. The Biome Inspector exposes the same checkbox. Explicit biome Material overrides remain authoritative.
texture_scale is positive finite repetitions per terrain-local unit and defaults to 1. A value of 0.5 repeats a texture every two units; 2 repeats twice per unit. Coordinates are continuous across chunk boundaries. Set the Texture2D asset's Wrap to Repeat for tiling; a clamped texture will not repeat. Scaling the terrain entity scales this authored texture pattern with it. Reset removes the scale override.
Material and tint changes preserve terrain geometry and collision; texture-scale changes update UVs and GPU geometry revision without rebuilding heights or colliders. Generate applies the draft through the existing update/history path, and prepared/cooked payloads retain material IDs and scales without needing the source recipe at runtime.
Prepared binary terrain is read-only in the editor. Open the source project to edit its graph or use terrain brushes, then cook the project again.
Terrain uses the same Material assets as meshes, including PBR maps and triplanar projection. A terrain_material_set binds those Material IDs to surface roles; assign a Material, not the set itself, to a biome override. The former separate terrain_material data format has been retired. Compatible GPU terrain supports smooth mapped layers and height-weight bias; the ordinary mesh fallback remains discrete.
Scatter Palette places models or prefabs on the generated surface. Those objects use their own Mesh Renderer materials. Connect Biome Rules before Scatter Palette when biome rules should control placement.
Water Body nodes carve finite lakes, oceans or river paths and produce surface/depth data that survives cooking. Connect the water result to Terrain Output: Generate publishes native water surfaces alongside the terrain, and cooked games load the same surfaces without carving again. Water follows the terrain placement and enabled state. It has no solid collider, so it does not act as a walkable floor.
For a lake or ocean, optional boundary XZ pairs constrain coverage to a simple polygon. Either winding and concave polygons are accepted; repeated closing vertices are optional. Zero-length edges, self-intersections and degenerate polygons are rejected. A lake's center must be inside its boundary. Rivers use their path and width instead.
Basin Water (basin_water) consumes a Drainage output and proposes connected lakes at the measured closed-basin spill levels. Defaults are minimum_depth: 1, minimum_area: 16, maximum_bodies: 16 and level_offset: 0. A nonpositive level offset lowers proposals below the spill level. The node reports when its body budget truncates eligible basins. An optional existing water input preserves those bodies. Proposal IDs use the node ID and the lowest sink's grid coordinates, such as lakes/basin/12_8, so changing the body budget does not renumber surviving lakes.
Water Edit (water_edit) takes matching field and water inputs. Its excluded_ids array removes selected stable body IDs; its overrides array patches bodies by id, preserving omitted fields. For example, {"id":"lakes/basin/12_8","level":3} adjusts one proposal. Unknown IDs, duplicate overrides and IDs that are both overridden and excluded are rejected. Proposals and edits resolve coverage on their input terrain without another carving pass; they do not simulate filling or spilling.
Water Body nodes expose Shallow RGBA, Deep RGBA, Absorption distance and Roughness. The alpha channels control shallow and deep opacity. Absorption distance is positive, in terrain-local units: at that depth the shallow contribution falls to about 37%. The water renderer combines depth-dependent absorption and colour with refraction of the captured opaque scene, Fresnel environment reflection and a directional-light highlight. Generated water meshes are native runtime resources, not authored entities or extra asset files.
Flow speed is a nonnegative prescribed current in terrain-local metres per second and defaults to 0 (still water). Rivers follow their nearest authored XZ path segment in source order; lakes and oceans use Flow direction XZ, default [1,0], normalized when sampled. A moving lake or ocean requires a nonzero direction. Follow-bed rivers follow the actual surface triangle tangent while preserving the prescribed local speed. River Route exposes the same speed along its generated path. Drainage accumulation does not invent or change gameplay current.
Matching wave settings share terrain-local ripple coordinates across water bodies, so a river/lake ownership boundary does not rotate or shift the normal-wave pattern. Currents still advect foam separately.
Surface-detail settings are wave_amplitude (default 0.05), wave_length (2), wave_speed (0.5), normal_strength (1), foam_width (0.5), foam_strength (0.5) and foam_color ([1,1,1,1]). Lengths and speeds use terrain-local metres and seconds; wave length must be positive, foam strength is between 0 and 1, and the other numeric values are nonnegative. Wave amplitude controls the slope of animated shading normals; it does not displace geometry, the queried surface, the base normal or the bed. Zero foam width disables the shallow-water foam band.
Shore triangles are clipped where the terrain meets the water plane rather than extending a full grid cell beyond it. Queries use the same terrain-triangle interpolation independently of the visible mesh. A bounded lake still needs banks at or above its chosen level: the engine warns after Generate if wet ground reaches the authored boundary below that level. It does not silently lower your level, raise banks or simulate spilling. Lower the water, enlarge the boundary, or sculpt a containing basin.
Dry shoreline corners retained for water triangulation do not count as submerged ground for wet materials, biome rules or scatter exclusions. Regenerate prepared terrain and recook existing projects after updating: terrain generator version 14 and prepared payload version 11 include the current water and scatter contracts.
Rivers also warn when their local water surface reaches a dry side below that level. Channel carving lowers ground; it cannot build retaining banks. Adjust the bed profile, depth or path, or sculpt banks. Open inlet/outlet ends and joins with another water body are allowed. Shore contact is antialiased over one screen pixel against the undistorted bank; water geometry and gameplay heights remain unchanged.
Press Generate after changing water appearance or geometry. Shore contours follow the terrain's resolution. Animated normals carry the prescribed current and wave speed; a shallow-depth foam band uses the authored foam settings. Waves change shading normals only, so collision, water queries and floating objects retain the generated level or follow-bed surface.
Reflection samples the scene environment; it does not reflect nearby objects through screen-space or planar reflection. Refraction samples the opaque scene captured before water and transparent objects, so transparent objects and other water surfaces are not refracted. Foreground-depth rejection prevents refracted foreground objects from smearing across the water silhouette. This optical approximation does not simulate fluid motion, displaced waves or caustics.
A camera inside prepared water receives depth-aware underwater absorption and tint, using the same water query and transformed surface as gameplay. An absent or dry water column leaves the camera unchanged. These effects follow the camera independently in the native editor's Game View and standalone runtime.
The renderer retains the main scene's 4x MSAA and samples a separate single-sample linear-depth prepass with an immutable opaque-colour capture per camera. Custom shader materials do not yet provide this depth-capture pass: the renderer reports an unsupported-capture diagnostic rather than silently using incompatible shader output.
Existing recipes that put base_height and friends inside a biome are rejected with a message naming the field and telling you to move it to a landform. They are not silently ignored, because a dropped height field would quietly flatten terrain that used to have relief.
Without a graph, a painted biome can also select its landform and change height. With a graph, painting overrides biome labels without replacing the graph's generated shape; use sculpting or graph modules to change height.
A recipe with no rules behaves as it did before rules existed. A biome referenced by the default setting, a rule or a painted region cannot be removed until those references are removed, and a biome naming a landform that does not exist is rejected.
Query water from Lua
Import local Water = require("demi.terrain.water") to query prepared water data. Water.sample(position, terrain_id) takes a world-space Vec3: a dense, one-based numeric array of exactly three finite numbers, {x, y, z}. The optional terrain_id is the stable entity ID of a terrain placement; omit it to query loaded, enabled terrains. Dry or unavailable water returns nil. A wet column can return a sample even when the point is above its surface or below its bed; check underwater for immersion.
A WaterSample contains:
terrain_id: stringandbody_id: stringidentify the terrain placement and its water body.kind: "lake" | "ocean" | "river"identifies the body kind.surface: Vec3is a world-space surface point, andnormal: Vec3is a world-space unit surface normal.depth: numberis the bed-to-surface depth in world units along terrain-local up.column_up: Vec3is the world-space unit axis from the bed toward the surface. It follows terrain placement, including a flipped local Y axis, and can differ from a sloped river's surface normal.flow_velocity: Vec3is the prescribed current in world units per second. Terrain scale and rotation transform the local velocity; no renderer or drainage query is needed.underwater: booleanis true when the queried point lies at or above the bed and below the surface. Points on the surface or below the bed are not underwater.
Queries follow translated, rotated and scaled terrain placements. The surface point projects the query along terrain-local up, which is not necessarily global vertical on tilted terrain. Sampling has no renderer dependency and does not generate terrain on demand.
Track point immersion
Water.tracker(terrain_id) creates an opaque WaterTracker with the same optional terrain filter. Call tracker:update(position) with a world-space Vec3 to receive an ordered array of WaterEvent values. Each event has phase: "enter" | "exit" | "stay" and sample: WaterSample. The tracker follows one point, not collider extents.
- The first update inside water emits
enter. - Remaining inside the same body emits
stayon every explicit update. - Switching bodies emits
exitfor the old body, followed byenterfor the new body. - Moving outside emits
exitonce; subsequent outside updates return an empty array.
tracker:current() returns the immersed sample retained by the latest update, or nil. tracker:reset() clears that state without an event. Terrain unload, disable and regeneration are reflected on the next update. Tracker identity includes the terrain placement, body and scene, so duplicate body IDs across placements or scenes do not mix. Scene identity remains private and is not returned in samples.
Call update from on_fixed_update or on_update to choose the cadence and implement gameplay mechanics. Events are returned to that call; there are no interrupt callbacks or automatic global listeners. Transform3D.get_world_position resolves parent transforms for the tracked entity:
local Water = require("demi.terrain.water")
local Transform3D = require("demi.transform3d")
local script = {}
function script:on_create()
self.water_tracker = Water.tracker()
end
function script:on_fixed_update(dt)
local x, y, z = Transform3D.get_world_position(self.entity_id)
if x == nil or y == nil or z == nil then
self.water_tracker:reset()
return
end
local position = {x, y, z}
for _, event in ipairs(self.water_tracker:update(position)) do
-- Handle enter, exit or stay here; event.sample describes the body.
self.last_water_phase = event.phase
end
self.underwater = self.water_tracker:current() ~= nil
end
return script
Use Water.sample(position) for a single query without tracking transitions. Native water supplies point immersion, depth and prescribed current; games opt into forces through reusable gameplay modules.
Buoyancy and swimming
Install demi.gameplay.water@1.0.0 through the package workflow. Its public modules are demi.gameplay.water.buoyancy and demi.gameplay.water.swimming; import only the modules the game uses. The package README describes the full options and examples/water_interaction_3d demonstrates their use.
local Buoyancy = require("demi.gameplay.water.buoyancy")
local Swimming = require("demi.gameplay.water.swimming")
function Behaviour:on_create()
self.float = Buoyancy.new("boat", {half_height = 0.5, density_ratio = 1.05})
self.swimmer = Swimming.new("player", {speed = 3, min_depth = 1.5, max_flow = 2.5})
end
function Behaviour:on_fixed_update(dt)
local floating = self.float:fixed_update(dt)
local swimming = self.swimmer:fixed_update(dt, {1, 0, 0})
end
Both modules apply forces to enabled dynamic Rigidbody3D bodies and preserve native collision contacts. Buoyancy measures a virtual upright segment along the sampled column_up; it does not integrate hull volume, torque or rotational stability. Drag acts relative to authored flow. Swimming accepts a world-space direction and game-owned depth/current thresholds. The game owns input conversion, animation, gravity policy and transitions between land and swimming movement. Call each controller once per physical step; dry, missing, static, kinematic or disabled bodies receive no forces.
Buoyancy's explicit gravity option defaults to {0,-9.81,0}; match it to the game's gravity and body gravity scale. Neither module changes body type, gravity, position or collider shape. The returned state is reused on the next update, so copy any values that must persist. Use Water.tracker for point enter/stay/exit events.
The graph example includes an optional Water Sensor script component in scripts/water_sensor.lua. Attach it to an entity to emit water_enter, water_exit and water_stay through demi.events, with entity_id and water in the payload. Configure offset_y for the sampled point and disable emit_stay when only transitions are needed.
Manual adjustments
- Raise / Lower: adds or subtracts height, weighted by the brush. Use a low strength for small adjustments.
- Flatten: captures the height where the stroke starts and moves affected samples toward that elevation.
- Smooth: averages neighbouring samples using the pre-edit surface for each stroke stamp.
- Protect: records absolute sample heights and keeps them through later regeneration and sculpting. Protection snapshots use the original grid.
- Paint exclusion: paints a separate 0–1 mask over the surface without changing any height. Choose Exclude to add mask weight or Erase to clear a subsection. Later generation stages use the mask to skip an area; the editor can preview it directly. Enable Preview exclusions to see it in the viewport.
The brush outline follows the actual surface. Release a drag to generate and apply the stroke as an undoable edit; Esc cancels the active stroke. Generate or discard pending settings before painting. Regeneration changes the procedural base and replays the stored edits; it does not discard them.
Buildings and other scene objects remain independent and are never deleted or moved by regeneration. Terrain changes may leave objects above or below the ground; repositioning them is an explicit editing action.
Layers
The recipe stores five named, toggleable layers by default: generation, biomes, sculpt, protection and exclusions. Each layer has a stable ID, a display name and an enabled toggle in the Terrain Graph's settings. The component Inspector selects which eligible layer receives a brush stroke.
Disabling a layer removes its strokes from evaluation without deleting them, so a stroke can be switched off and restored while you iterate. Strokes on disabled layers keep their authored data.
Two ordering rules matter:
- Base stages always evaluate before surface edits, regardless of the order layers appear in the recipe. Disabling the generation layer therefore freezes the base heights and lets you sculpt on top of them.
- Within a stage, layers and the strokes inside them keep their authored order. Later strokes in the same stage paint over earlier ones.
You can add extra layers to the biome, sculpt, protection and exclusion groups, and rename any layer. The generation layer is the base noise pass and cannot be removed. Layer names and toggles are draft settings: press Generate to apply them.
Every brush except Select targets a layer, chosen in the Target layer field of the viewport brush controls. The list only offers layers of the kind the brush writes to, and disabled layers cannot be selected. Flatten keeps a captured target height and raise/lower keeps a separate additive offset, so the two kinds of manual adjustment stay distinct when you retune strength and falloff.
What a brush stroke changes
Brush data stores shared parameters once and ordered stamp positions in a points list. A single radial operation can use center. Consecutive stamps with identical settings share one entry; different settings or protection snapshots remain separate. Repeated positions and overlapping stamps are retained because they affect brush strength. This changes the source representation, not the terrain result.
{
"type": "flatten",
"target_height": 12,
"radius": 8,
"points": [[24, 32], [25.6, 32], [27.2, 32]]
}
New viewport strokes use this representation automatically. For existing data, choose Compact brush data in the graph's edit settings, then Generate and save the terrain asset. The CLI can preview the same conversion with demi terrain compact path/to/landscape.terrain.json; add --write to save it, then reimport its asset manifest. Compaction preserves unrelated recipe fields and validates native operation equivalence before saving. It does not replace the recipe with baked heights or remove regeneration and layer editing.
Ordinary brush work is incremental. The generator keeps the generated base surface separately from the sculpted result, so a stroke recomputes only the samples the brush actually touched, plus the one-sample border that lighting and neighbouring smoothing depend on. Meshes and collision are rebuilt only for the affected chunks; untouched chunks keep their existing resources and are not re-uploaded.
Full regeneration also limits each sculpt stamp to its brush footprint instead of scanning the whole terrain for that stamp. Brush order, smoothing snapshots and protected samples keep their existing meaning.
A local edit produces the same heights as a full regeneration of the same recipe. History is stored as before/after sample values, so once a stroke is applied, Undo and Redo restore the recipe and the visible surface together. They stay unavailable while a stroke is still being dragged or a preview is pending, so that a half-finished stroke is never recorded as an edit.
Some changes genuinely invalidate the whole base and trigger a full regeneration instead: changing the seed, terrain size, resolution or chunk size, and editing landform generation parameters such as base height, height variation, feature size, roughness, octave count, profile, domain warp or directional stretch. A change to the grid asks how to proceed rather than silently discarding edits, as described under changing terrain dimensions below.
Editing only a biome tint is cheaper again: it recolours the existing triangles without rebuilding heights or colliders. Play mode never starts on a stale terrain surface, because Play waits for pending terrain work to finish, and a published stroke updates the visible mesh and its static collision together.
Biome rules
Biome rules assign biomes automatically from the finished surface, so a landscape no longer needs manual painting. Add a rules array to the recipe. Each rule names a target biome and may band the following conditions; an omitted condition is unconstrained.
elevation— world height, as a[minimum, maximum]pair.slope— surface steepness in degrees, 0 flat to 90 vertical.moisture— a stable climate field in[0, 1], independent of your biome list.water_distance— terrain-local distance to the nearest wet sample from connected water, or inferred sea coverage when no water is connected.substrate— any ofsoil,rock,sandorwet, derived from the surface rather than painted.
On conflict the highest priority wins. An equal priority is broken by blend weight, so a stronger transition outranks a weaker one, and a full tie keeps the earlier rule, so the result never depends on evaluation order. blend is a transition width in world units measured from the lower edge of the rule's elevation band. It softens how strongly a rule claims the ground just above that edge, and that strength is what breaks a priority tie. It does not cross-fade biome colours: surface tint is a discrete biome per cell, so a higher blend gives a rule the edge rather than a gradient. Use overlapping elevation bands with different priorities to shape a transition.
Painted regions always win over rules, so a manual stroke stays authoritative over automatic assignment. Disabling a biome layer removes its rules exactly as it removes its regions. Because rules read the whole surface, changing one rebuilds the terrain base; sculpting alone stays incremental.
With no rules array, the default biome and painted regions determine assignment. Rules are draft settings like every other recipe field: the Biome rules section of the Terrain Graph edits them, and they take effect when you press Generate.
In that section each rule exposes its ID, target biome, target layer, priority, blend, a checkbox and minimum/maximum pair per band, and a substrate multi-select. The layer list only offers enabled biome layers; a rule pointing at a disabled one is not evaluated, and the section says so. A band checkbox removes its key from the recipe rather than writing a null, which keeps the saved source free of conventions that mean nothing. Rule IDs are referenced by nothing except the rule itself, so renaming one is safe.
The Rule mask overlay selector in the viewport brush controls draws a mask over the surface: None, Biome, Elevation or Slope. The overlay samples the committed generated field, so it does not change until you press Generate, even if the draft rules differ. Elevation is normalized against the range the current field actually spans, and slope is read from the committed normals. There is no moisture overlay: moisture is derived in a transient pass over the field during generation and is not stored on the result, so drawing it would either cost a fresh pass every frame or show a fabricated zero.
Biome rules are an experimental authoring aid. Drainage now exposes flow as a graph mask. Biome-rule fields do not yet consume flow or basin membership directly.
Landscape presets
Every project has eight built-in starters: Flat Building Site, Desert Dunes, Rolling Hills, Mountain Range, Basin, Plateau, Coastal Island and Drainage River. They need no imported art. Each uses a 128 × 128 world-unit surface with 64 × 64 cells, biome tints and an editable graph. Comment nodes explain shape, appearance and output. They are learning examples, not simulations of geological dune or mountain formation.
Follow the graph left to right. Change a Noise node's Noise profile, Height variation and Feature size for relief. Domain warp bends the sampling pattern; directional stretch and heading organize elongated features. Biome Rules opens the shared appearance settings. Generate previews changes; Apply changes to asset saves a shared terrain. Undo terrain in the Presets tab restores an applied landscape; graph-draft edits use Undo graph.
Drainage River Route
River Route (river_route) consumes a Drainage node result, which retains the terrain field that produced it. Choose source_x and source_z in terrain-local world units; the source must lie inside the field and is snapped to the nearest terrain sample. The node follows drainage guidance only along nonascending terrain-triangle edges. It does not choose watershed sources automatically or invent uphill spill routes.
channel_width and channel_depth are finite positive world-unit values. The routed bed starts at the traced height minus channel depth. At a receiving water body, a constant bed floor prevents the river surface from falling below the joined surface; this can form a flat backwater reach without introducing an uphill bed. A sea-level endpoint uses the drainage sea level as that receiving surface. water_depth is finite and positive, no greater than channel depth; the water surface follows that bed at this depth. Defaults are source [64,64], channel width 4, channel depth 2 and water depth 1. The graph node ID becomes the stable water body ID.
Connect River Route's Field and Water outputs to Terrain Output. Its optional Existing water input accepts water that matches the Drainage node's source field. To route into a lake, ocean or another river, generate that body first, connect its Field output to Drainage and its Water output to River Route. The route stops at the first existing wet column and reports the joined body ID. An already-wet source retains existing water without adding a river. Short or rejected routes also retain existing water.
Select the River Route node to read its bounded endpoint status: sea level, field edge or an existing water body. Closed-basin and blocked-surface routes produce warnings and end there; they do not imply a valid sea outlet. A route shorter than two samples, or water that cannot be placed, leaves the input field unchanged and publishes no new river, with a diagnostic explaining why.
Water Body rivers accept editable XYZ control points in river_path. Set river_path_interpolation: "catmull_rom" for a uniform Catmull-Rom spline through those points; omitted or "linear" preserves exact straight segments. river_curve_steps defaults to 8 and accepts integers from 1 to 64, controlling sampled segments per control-point interval. The source graph retains control points; generation samples the curve once, and carving, current, queries and cooked rendering consume that same polyline. XYZ interpolation preserves bed elevation and exact endpoints; coincident control points remain valid. Curves can overshoot between controls, so review banks and bed grades after Generate. One step retains only the original controls.
Water Edit accepts these interpolation settings only together with an explicit replacement river_path; an appearance-only edit never resamples an existing prepared path. River Route remains a terrain-safe linear route and does not smooth across obstacles.
Authored Water Body river nodes also expose river_surface_mode: level is the default and uses Water level; follow_bed uses the authored XYZ path's bed elevation plus positive river_depth (default 1). Water level is ignored in Follow bed mode. Terrain-triangle water queries and cooked surfaces use the same resolved per-position surface. This is a prescribed channel and water surface, not a time-dependent flow simulation.
Drainage and erosion masks
Drainage exposes a Flow accumulation mask alongside its drainage data. Interior samples include all upstream contributions, normalized to the largest interior value; border outlets retain zero mask weight. Erosion exposes Deposited sediment and Eroded material masks. Each independently maps its largest positive net deposit or cut to 1, with zero where that kind of change is absent. Connect these mask pins to Height Blend or Height Offset to drive later graph stages.
Erosion retains signed height change per sample in its in-memory graph results: positive means net deposition, negative means net removal. This includes hydraulic and thermal changes; it is not a fluid-depth simulation or a new cooked gameplay query. Reapplying sculpt strokes reuses the generated base and does not silently restart global erosion.
Select an Erosion node to read its hydraulic transport totals: eroded, deposited, exported at open field edges and suspended. These are sums in discrete sample-height units, not physical sediment mass or volume. Suspended records unfinished bounded transport for this generation operation; it is not retained fluid state. The Deposition and Cut masks instead describe net height changes, including thermal relaxation, so they are distinct from these gross hydraulic totals. Final graph caches and prepared terrain assets retain the per-node ledger for inspection, including after local sculpt replay.
Generation quality
The graph toolbar offers Preview and Final generation. Final is the default and runs erosion at its authored iteration count, including thermal transport. The Erosion node’s iteration count controls hydraulic transport: zero skips droplets, while thermal strength independently controls Final thermal relaxation. Preview keeps the same grid, source noise and drainage but uses fewer hydraulic erosion iterations and droplets, shorter droplet walks and no thermal pass. A graph without erosion retains the same sampled shape in both modes.
Generation runs in the background with cancellable progress, including progress during erosion. The last completed surface remains visible while work runs. A completed Preview is labeled and shown temporarily; it does not commit the draft recipe or enter final generation caches. Generate Final before Apply, Save, Play or brush editing. Discard preview restores the previously applied surface while retaining the graph draft.
Quality is a transient work choice, not an authored terrain field. Source shape caches can be reused across quality modes; erosion and its downstream graph results cannot. Authored sculpt and exclusion strokes run after graph erosion, and ordinary local brush editing reuses that generated base rather than starting another erosion simulation.
Path Distance Mask
path_distance_mask takes a connected terrain field and outputs a mask around a polyline. Its points are at least two [x,z] pairs in terrain-local world units. The graph editor uses structured point rows with add, remove and reorder controls. Repeated points are valid. Coordinates outside the terrain are also valid, which lets a corridor enter and leave the field.
width is the fully weighted half width, not the total corridor width. A width of 35 gives a 70-unit core. falloff is the smooth transition beyond that core; zero gives a hard edge. Both are finite and nonnegative. Defaults are points [[0,0],[0,100]], width 20 and falloff 40. Each coordinate must fit the finite native float range.
For a valley, connect a mountain Noise source and a lower Flat terrain source to Height Blend's A and B inputs, then connect Path Distance Mask to its Mask input. A blend amount of 1 follows the mask completely. The mask changes blend weight; it does not carve or publish water. Use a Water Body node when the corridor also needs a river bed and water data.
Polygon Mask
polygon_mask takes a connected terrain field and outputs a mask for a closed polygon. Its points are at least three noncollinear [x,z] pairs in terrain-local world units. Each coordinate must fit the finite native float range. The graph editor uses the same structured point rows as Path Distance Mask. The last point connects to the first automatically; repeated points and an explicit closing point are valid.
The mask is 1 inside and on the boundary. falloff is a finite, nonnegative world-unit distance for a smooth transition outside the polygon; zero gives a hard edge. Either winding and concave polygons are accepted. Self-intersections use the even-odd fill rule: crossing an edge switches between inside and outside. Defaults are points [[0,0],[100,0],[100,100],[0,100]] and falloff 20.
For a basin, connect Noise to Height Blend's A input, a lower Flat terrain to B, and Polygon Mask to Mask. Use a higher flat height for a plateau. For an island, blend a low ocean-floor constant with elevated noise using the polygon mask. Set Height Blend's amount to 1 to follow the mask completely. The mask and blend nodes shape terrain; they do not publish water by themselves. The built-in Coastal Island preset also connects an Ocean Water node, with sea level zero and a shore biome.
Comment nodes
Drag Comment from Terrain Nodes → Notes onto a graph. Notes display wrapped text; expand Edit note to edit multiple lines and click outside to commit. Comments have stable node IDs and support move, copy/paste, duplicate, delete and graph Undo/Redo. They have no connection pins and cannot be a terrain output. Their text and position save with the graph but do not change generated terrain or its generation cache identity.
Built-in provenance uses builtin://terrain/flat-building-site, builtin://terrain/desert-dunes, builtin://terrain/rolling-hills, builtin://terrain/mountain-range, builtin://terrain/basin, builtin://terrain/plateau, builtin://terrain/coastal-island and builtin://terrain/drainage-river. Applied recipes contain their settings and graph in full; they do not require an external starter file. demi terrain presets <project> lists both built-in and project presets. The same IDs work with demi terrain apply-preset.
A landscape preset is a reusable, versioned data asset that configures the shared generator. The Terrain Presets tab beside Inspector and Terrain Nodes lists starter landscapes and imported project presets. It is available while a terrain graph is open; use View → Terrain Presets if hidden. Choose a preset and Apply and generate, then confirm replacement of the current generation settings and graph. Unapplied graph edits are replaced; compatible sculpt strokes and painted regions are retained. A grid change with existing edits is staged: press Generate in Terrain Graph to choose how to handle it. Applying is one undoable recipe edit; Undo and Redo move the recipe and visible surface together. The same operation is available from the command line as demi terrain apply-preset.
examples/terrain_3d includes Mountain Range, River Valley, Rolling Hills and Coastal Island presets. Rolling Hills sets gentle relief; River Valley uses channel and gravel-bar landforms with riverbank biome rules; Coastal Island sets an ocean-floor landform and shoreline biomes around higher island ground. These are terrain-shape and biome settings, not authored or rendered water surfaces.
A preset is a data asset whose manifest declares settings.content_type as terrain_palette's sibling terrain_preset, and it is referenced by a normal asset:// URI. The document may supply any of size, cells_x and cells_z (the preset names resolution per axis, while the recipe it produces uses resolution as one pair), chunk_cells, seed, default_biome, biomes, layers and rules, an optional graph using the normal versioned terrain graph format (including comment nodes), plus a required name, an optional label and description and a format_version of 1. Keys the preset omits fall back to canonical recipe defaults, so a preset that only sets a seed still produces a valid recipe. Rule objects inside a preset use exactly the same grammar and the same rejections as authored rules.
Applying a preset merges it into the author's recipe once and stamps preset_id and preset_version. Both keys are always recorded together; a half-stamped recipe is rejected, because a version without an id would make a recipe look applied when it is not. The recipe therefore remains self-describing: it regenerates identically from its own contents, and provenance is recorded rather than implied by a reference. Provenance is also part of the locality contract, so applying a different preset is treated as a real recipe change rather than a display preference.
Strokes are never templated. regions, edits and exclusions always come from the author's recipe, so applying a preset cannot destroy painting or sculpting. The merged document is then validated as a whole, which means your strokes are checked against the biomes they will actually be generated with, not against the ones the recipe had before.
That guarantee has a consequence worth knowing before you apply one to painted terrain. Because your strokes survive, a regions entry must name a biome the preset actually defines, and applying a preset whose biome set does not include your region names is refused with a clear error rather than quietly discarding the stroke. The fix is on your side: either pick a preset that defines those biome names, or rename the regions to match the preset. Renaming a region is safe, because a region stores the biome name and nothing else references it.
The reason a preset is applied rather than referenced is worth stating plainly: the recipe stays the single source of truth for generation. Incremental editing, the rebuild-versus-replay decision and undo/redo all operate on the recipe, so a runtime override would make each of those three answer a different question from the one the author asked. A preset applied once cannot also disagree with the terrain it was applied to.
Asset palettes
A terrain_palette DataAsset separates surface material bindings from object placement rules. Palettes use format_version: 3; manifests and material sets retain their own format versions.
{
"format_version": 3,
"name": "Meadow",
"material_set": "asset://terrain/material_sets/meadow",
"art_density": 1,
"tree_density": 1,
"placements": {
"pine_sparse": {
"prefab": "prefab://vegetation/pine",
"category": "tree",
"density": 0.02,
"candidate_spacing": 4,
"spacing": 12,
"scale": [0.9, 1.3],
"yaw": [0, 360],
"biomes": ["meadow"],
"slope": [0, 35],
"moisture": [0.2, 0.8],
"water_distance": [3, 100],
"cluster": {"size": 24, "strength": 0.7},
"collision": "static"
}
}
}
The IDs above illustrate the format; choose assets installed in your project. material_set is optional and references a terrain_material_set. Its surface bindings never produce scattered entities. placements is optional; a palette must supply a material set or at least one placement.
Each placement has an author-chosen stable ID using letters, digits, underscores or hyphens, and exactly one geometry reference: model to a Model3D asset, or prefab to an entity prefab. Prefabs own their materials and need no dummy asset reference. Multiple rules can reuse geometry with different settings. Renaming a rule changes its generated object identities.
density is the expected number of instances per square terrain-local unit before environment filtering, spacing and clustering. Each rule is independent. The palette's art_density multiplies every rule; grass_density, tree_density and rock_density multiply rules whose explicit category matches. All multipliers default to 1; category defaults to prop. A zero density disables that rule.
candidate_spacing defines a fixed physical candidate lattice, independent of terrain sample resolution. When omitted, it uses half the positive minimum spacing, or 0.25 units when spacing is zero. Requested density multiplied by the art and category controls must not exceed one candidate per lattice square; an actionable error asks you to reduce density or candidate spacing. Smaller values provide more potential sites, so grass density can exceed one instance per terrain sample. Density changes thin these fixed sites without moving surviving placements. spacing supplies minimum separation within a rule (default 0); rules do not compete with one another. Local priority against fixed potential sites prevents an exclusion edit from causing a chain of replacement placements outside the edited region. Spacing and filters can reduce the achieved density.
biomes is an optional biome-name list. Inclusive slope ranges use degrees from horizontal (default [0,90]); moisture uses normalized saturation (default [0,1]); water_distance uses terrain-local units to the nearest resolved wet sample. Connected Water coverage replaces inferred sea coverage. Water and authored exclusions always block placement. Protected areas, roads and building footprints use the same shared constraint query.
cluster.size sets the physical size of smooth density patches and cluster.strength blends uniform density (0) toward patches (1). Defaults are 16 units and 0. Clusters change site selection, preserving surviving sites. scale defaults to [1,1], yaw to [0,360] degrees, collision to static (none/static/trigger). Distance-based geometry LOD and culling are authored on the referenced model or prefab Mesh Renderer. The former palette integer lod was unused and is rejected in version 3. Direct model rules must explicitly use collision: none. Use a prefab with authored collider shapes for static or trigger collision; the engine does not invent box bounds for arbitrary models. Import/reimport records model and material-set dependencies; prefab source traversal owns prefab dependencies.
Use the palette editor's Surface Materials, Art density and Object Placements sections. Rule edits, removal and material-set changes use normal Undo/Redo and Save.
Migration: earlier palette versions are rejected explicitly. Version-2 placement weight has been replaced by physical density; choose density and candidate spacing for the intended landscape scale. For version-1 content, first move ground bindings into a material set and object entries into named placements. Reimport manifests and recook derived terrain payloads.
Automatic surface materials
A recipe's palette can bind a material set without any object placements. Generate resolves the final terrain's slope, moisture, drainage and water coverage into surface roles. Each triangle uses its vertices' majority role; when all three differ, the first vertex determines the role. The result uses indexed sections of the existing terrain mesh, not proxy entities.
{
"format_version": 1,
"name": "Landscape surfaces",
"roles": {
"ground": "asset://materials/soil",
"rock": "asset://materials/rock",
"underwater": "asset://materials/riverbed"
}
}
Automatic role classification uses the selected surface’s slope, signed erosion transport, moisture and resolved water coverage. Cuts expose rock; positive transport creates deposition sediment, with flow also identifying concentrated river paths. Flat moist soil can select grass, shorelines select wet ground or sand, and submerged beds select underwater. Snow additionally respects each biome’s allow_snow. Height Blend weights transport with the same amount and mask as its heights, so discarded branches do not paint the final terrain. Explicit biome Material assignments override automatic role bindings; the generated role classification and set bindings remain separate.
Import this document as a terrain_material_set DataAsset and reference it from the palette's material_set. Bind ordinary Material assets. Available roles are ground, grass, rock, cliff, sediment, sand, snow, wet_ground and underwater. Use the shared Material editor for PBR maps and projection; there is no separate Terrain Material source.
An explicit biome material takes precedence. Biome colors continue to tint the material. Missing role bindings, or samples without a winning role, use the biome tint; the Terrain Graph panel reports these cases. Raised lakes and rivers use resolved finite-body coverage rather than a global sea plane. The terrain_graph_3d example supplies all nine surface bindings without scatter objects.
Changing only material-set bindings retains heights, collision geometry and classification. Sculpting recomputes classification, including effects from nonlocal drainage, while publishing only changed surface groups. Save and cooked payloads preserve the same classification and bindings. Reimport changed source assets and regenerate the preview to apply them.
Rendering boundary: classification chooses source roles; compatible GPU terrain blends their mapped Materials across neighboring samples, with optional height bias. The ordinary mesh fallback uses discrete assignments. The material set itself has no four-role cap: four neighboring field contributors and the device’s array-layer limit are separate concerns. Existing authored document versions remain unchanged; regenerate derived terrain caches and recook shipped projects after generation or payload changes.
Scattering a palette onto terrain
Name the palette on the recipe with a palette field holding an asset:// id. A recipe without one scatters nothing, so the feature is opt-in. In the conventional pipeline, a palette change takes the full generation path rather than a local brush replay. In a terrain graph, a palette change invalidates the Scatter Palette branch and its downstream output; unchanged upstream nodes can be reused from a previous graph result.
In the conventional pipeline, scattering reads the finished surface after sculpting and uses the final biome, water and exclusion constraints. In a graph, Scatter Palette reads the heightfield connected to its input before the final authored region, sculpt and exclusion overlays. After graph output and local brush replay, retained candidates snap to edited heights and recheck final biome, slope, moisture, water distance, protection and exclusion constraints against the resolved palette. Filtering preserves the immutable base candidates, so undoing an edit restores their identities and variation. Refiltering never invents candidates that the upstream Scatter Palette node rejected. Use Generate to recompute graph candidates when changing their source inputs; a newly painted biome region alone cannot create candidates rejected by upstream biome rules. Connect Biome Rules before Scatter Palette when those rules should drive candidate selection.
Each rule requests a physical density before suitability filters, clustering and minimum spacing reduce the accepted count. Rule, category and global art-density controls remain independent. Candidate and spacing-check work budgets reject excessive work with actionable diagnostics; the placement ceiling reports truncation.
Placement draws from the scatter sub-seed combined with its physical lattice coordinates and stable rule ID. Changing terrain resolution changes environment sampling and height projection while retaining the same physical candidates, rotation and scale streams. Rule order and unrelated rule-density edits do not change another rule's placements. Density and local exclusion edits thin the fixed sites without causing spacing refills elsewhere.
When a scene loads and after terrain updates, placements reconcile beneath their terrain owner. Identity uses the owner, palette, rule and candidate X/Z coordinates; the nearest terrain sample is only an environment reference. Sculpting updates ground height without replacing a surviving placement. Multiple grass sites can share a sample while remaining distinct.
Select the terrain in the scene, expand Generated vegetation and detail in its Inspector, filter by placement rule and click Convert beside the desired terrain-local position. For an individual generated tree or rock, select it or one of its children and click Convert to Scene Object directly in the Inspector. Render batches instead link back to the terrain list. The editor selects the new object; move or edit it normally. Conversion and its suppression entry undo together. Converted prefab content keeps its materials, LOD and collision policy and can be unpacked through the ordinary prefab workflow.
Terrain3D.scatter_exclusions is an optional array of unique nonempty stable placement keys on a scene terrain placement. Conversion records the generated source key here and creates an independent authored object. The suppression list is reversible, reflected in the advanced Inspector and saved with the scene or prefab; it never enters the shared terrain asset or triggers regeneration. Two placements of one terrain asset can therefore retain different authored scenery. Runtime reconciliation filters these keys for both decorative batches and colliding prefab instances.
The examples/terrain_3d palette uses a clearly labelled cube prefab for its six vegetation/prop rules. Soil, sand, snow, wet ground and exposed-rock surface bindings live in its material set and create no proxy entities. Replace the remaining prototype prefab with role-specific art for a game. Failed prefab composition prevents the scene from becoming ready.
Inspecting terrain from the command line
demi terrain inspect <recipe.json> [--format text|json]
demi terrain explain <recipe.json> --at <x>,<z> [--format text|json]
demi terrain seeds <seed> [--format text|json]
These commands accept a recipe JSON document: the recipe object from a Terrain asset source or an explicit inline Terrain3D recipe. They do not accept the enclosing scene or Terrain asset document. inspect reads the recipe without generating a field, and reports size, resolution, sample count, seed, chunk size, default biome, derived sea level, the biome table, the layer tree, every rule with its conditions, stroke counts by kind and the derived sub-seeds.
Two more verbs apply a preset:
demi terrain presets <project>lists everyterrain_presetdata asset in the project with its name, description and a summary of its biomes and rules. A project with no presets prints an empty result and still succeeds.demi terrain apply-preset <project> --preset <asset://id>currently targets an inline Terrain3D recipe in the scene, writing it through the shared small-diff patcher. If the scene has more than one terrain, pass--entity <id>. For an asset-backed terrain, apply presets in the Terrain Graph; this CLI command does not yet follow the asset reference.
Your work survives either path: applying a preset keeps every regions, edits and exclusions entry. A preset that changes the terrain size or resolution is not applied blindly. With protected areas present the engine refuses it and says why, and otherwise it asks whether to keep your radial edits before generating at the new size.
explain answers the question a rule list cannot: why is this point this biome? It generates the field, then reports the assigned biome and where the assignment came from: a named rule, a painted region override, or the default biome. It prints the full rule context for that sample, and then either the conditions of the winning rule or, when nothing matched, every active rule with each band marked INSIDE or OUTSIDE against the sample value and the first condition that rejected it. Progress and diagnostics go to stderr, so stdout stays parseable for --format json.
$ demi terrain explain recipe.json --at 10,110
Position: 10.000,110.000 in terrain 128.000 x 128.000
Cell: 10,110 (sample 14200 of 16641)
Sea level: 8.500
Biome: sand
Source: rule "shoreline"
Context:
height 0.274
slope 6.638 deg
moisture 0.268
water distance 0.000
substrate wet
Rule "shoreline" biome=sand priority=30 blend=1.500
slope unconstrained
moisture unconstrained
water distance [0.000,6.000] sample 0.000 -> INSIDE
elevation unconstrained
substrate unconstrained
The tool reports band containment facts and takes the assignment from the runtime decision. It does not reimplement the runtime's blending weighting, so a blend ramp is visible as a matched band rather than as a fractional score. That is a deliberate limit: it can tell you a rule did or did not accept a sample, and it will not invent a number the runtime never produces.
seeds prints the derived sub-seed of every seed channel for a world seed. The channels are landform, landform_warp, erosion, hydrology, biome_placement and scatter. The separate warp channel keeps coordinate displacement independent of the elevation-noise sequence; disabling warp preserves the original landform sampling. Erosion, drainage and water processing, and scattering are implemented stages; which ones run depends on the recipe or connected graph nodes. The command reports stable channel values even when a particular terrain does not use a stage. Each sub-seed is derived by hashing the channel's name rather than its position in the enum, so adding or reordering a channel cannot change an existing channel's value.
The generator version is folded into the terrain generation cache key, alongside the canonical recipe. Graph node positions are excluded from that key; changing connections or parameters changes it. A resolved palette and its referenced asset hashes are included, so changing palette content cannot serve placements from an older input.
Changing terrain dimensions
If size or resolution changes while edits exist, the editor asks how to proceed. Radial edits can keep their terrain-local positions. Protection snapshots require the original grid: keep that grid or explicitly clear the edits before generating a resized terrain. Clearing is undoable. Cancelling generation or a failed generation leaves the previously committed terrain intact.
Saved source and generated geometry
A Terrain asset stores a versioned recipe, region strokes and manual edits; a scene placement stores its asset reference. Neither stores generated triangle lists or one authored entity per chunk. Generated child meshes and colliders exist in the loaded world. Explicit inline procedural recipes remain available in scenes and prefabs.
Development tools prepare uncached source assets; shipped games load cooked heightfields without rerunning generation. Chunk geometry and native resources are built from the loaded field. Background work covers generation, but scene publication, mesh construction and GPU/physics updates can still take time on large terrains. Render LOD selects detail per camera using the terrain owner's pixel-error setting. It does not implement large-world streaming.
Try the example
demi editor --project examples/terrain_3d
demi run --project examples/terrain_3d
Select Paint and sculpt this terrain in the hierarchy. The example has meadow, mountain and sand biomes, plus a flattened building site. Change the seed and regenerate: the flatten edit remains and the building stays in the scene.
Noise generation uses FastNoiseLite under its MIT license. The dependency is pinned in the engine build configuration.