Terrain Graph

A node-based procedural terrain
generator inside Unreal.

AI agents author terrain as a JSON DAG of 82 node types — noise, shape, coordinate, pattern, sampling, math, filter, compose, mask, derivative, erosion, output. The server evaluates the graph deterministically, writes a 16-bit heightmap and RGBA8 splat weight maps, and pushes the result through a Landscape's edit layer. One call from "describe the terrain" to "ALandscape actor renders in the viewport with auto-material."

See the workflow → Read the docs
82
Node Types
17
Built-in Presets
6
MCP Tools
D8
Real Fluvial Erosion

The Cycle

Six tools. One pipeline.

From an empty world to a textured Landscape actor in five MCP calls.

landscape_create terrain_preset_get terrain_preview terrain_apply_to_landscape terrain_create_auto_material
terrain_list_nodesEnumerate every node type and built-in preset for discovery.
terrain_preset_getFetch one of 17 starter graphs as fully-mutable JSON.
terrain_previewLow-res topographic + hillshaded PNG returned inline as a content block.
terrain_buildEvaluate a graph end-to-end and write G16 heightmap + RGBA8 splat assets.
terrain_apply_to_landscapePush the result through a Landscape edit layer; auto-derives heights from the actor's footprint.
terrain_create_auto_materialGenerate a Landscape Material that consumes the splat weight map and assign it to the actor.
texture_inspectRead back any UTexture2D and assert per-channel statistics for verification.

Design

Why a graph, not a switch.

LLMs author JSON DAGs natively

The recipe is a single saveable, diff-able artifact. An agent emits the whole graph in one tool call, mutates the graph between iterations, and never has to "drag wires" or hold node-editor state across rounds.

Composable beyond preset switches

A monolithic generator can only express what its switches expose. A graph composes primitives — route this mountain's slope mask into that erosion's strength, share flow accumulation across a fluvial pass and a splat layer, stack three biomes through a layer-stack macro.

Deterministic evaluation

Every node carries an explicit or stable per-id seed. Same graph + same seed = bit-identical output across runs and machines. Adding or removing unrelated nodes does not shuffle other nodes' seeds.

CPU-tiled, no GPU dependency

ParallelFor inside each node, refcounted intermediate buffers, deterministic per-node RNG. 1009² runs in seconds, 4097² in tens of seconds. No RHI compute, no shader compilation hiccups.

Verifiable output

texture_inspect reads back the generated assets and reports per-channel min/max/mean/stddev/distinct count + a 16-bucket histogram. Tests assert on observable terrain statistics — the framework catches "tool returned ok but the texture is flat" on every run.

One-call end-to-end

From AI prompt to a colored, eroded Landscape actor: terrain_apply_to_landscape evaluates at the landscape's vertex resolution, auto-derives proportional heights from the footprint, and writes through the edit layer. terrain_create_auto_material generates and assigns a splat-driven Landscape Material in the same flow.

Node Taxonomy

More families. 82 nodes. Infinite combinations.

Each node has a typed input set and produces a single buffer. Connect them however the terrain requires.

Sources

Generate from nothing

noise.fbm · fractal Brownian motion
noise.ridged · sharp peak ridges
noise.billow · soft fluffy bumps
noise.worley · cellular (cells / distance / edge)
noise.domain_warp · twist any input
shape.cone · radial cone falloff
shape.dome · smoothstep dome
shape.crater · bowl + raised rim
shape.plateau · flat-topped mass
shape.ridge_line · linear ridge between two UV points
shape.island_falloff · radial mask
shape.gradient · directional
shape.constant · solid value
import.heightmap · existing G16/RGBA16/G8/BGRA8 texture
Filters

Reshape a buffer

transform.remap · range remap
transform.curve · 1D LUT, sorted point list
transform.terrace · stepped plateaus
transform.power · gamma curve
transform.clamp · hard clamp
transform.invert · 1 − x
filter.blur · separable Gaussian
filter.normalize · percentile clip + remap to [0,1]
derive.false_color_split · preview ramp channels as masks
Composers

Combine buffers

compose.add · weighted sum
compose.multiply · per-pixel product
compose.subtract · clamped difference
compose.lerp · scalar or buffer-driven blend
compose.max / min · per-pixel envelope
compose.overlay / screen · Photoshop blend modes
compose.mask_blend · the workhorse: lerp(a, b, mask)
compose.layer_stack · macro: TerraForge3D-style biome stack
Masks

Buffers → weight maps in [0,1]

mask.altitude · height band with falloff
mask.slope · steepness in degrees
mask.curvature · concave (valleys) vs convex (ridges)
mask.flow_accumulation · D8 routing → river-bed mask
mask.combine · and / or / min / max / xor
Erosion

Geological realism

erode.hydraulic · particle-based runoff
erode.thermal · talus-angle slope stabilisation
erode.fluvial · flow-accumulation channel carving
erode.coastal · smooths only inside a sea-level band
Outputs

Side effects, no buffer

output.heightmap · G16 UTexture2D
output.splat · RGBA8 weight map (up to 4 layers)
output.landscape_apply · direct to Landscape edit layer
output.preview · base64 PNG returned inline

From Noise to Place

Four nodes flip output from "grey lump" to "real terrain."

These are absent from monolithic noise+erosion tools. They turn a heightmap into a geological heightmap.

mask.curvature

Ridges vs valleys

Discrete Laplacian: negative = convex ridge tops, positive = concave valley floors. Routes splat layers correctly so snow lands on peaks and grass settles in concavities — not painted by altitude alone.

mask.flow_accumulation

Where water pools

D8 steepest-descent routing computes the volume that drains through each cell. Channels saturate to 1, ridges stay at 0. Drives river-bed splat layers, sediment darkening, and lush vegetation placement.

erode.fluvial

Real river networks

Uses flow accumulation to carve channels proportional to pow(flow, flow_power) × carve_strength. The result is real river valleys following the topography, not surface scratches from particle simulation. This is what makes terrain look geologically continuous.

output.splat

Per-pixel material weights

Even a great heightmap looks grey. output.splat composes up to 4 mask buffers into an RGBA8 weight map your Landscape Material samples for grass / rock / snow / sand blending. Sum normalised to 1.0 per pixel.

Built-in Presets

17 starter graphs. Always mutable.

Each preset returns a complete graph the agent can mutate node-by-node before applying — change parameters, swap node types, add layers.

island

Radial falloff × ridged FBM, hydraulic + thermal + coastal erosion. Splat sand / grass / rock / snow.

mountain_range

Diagonal ridge line × domain-warped ridged FBM, fluvial erosion produces real river networks. Splat grass / rock / snow.

canyon

Plateau base, aggressive fluvial carving along noise-warped flow seeded from the high ground. Splat river / sandstone / floor / grass.

archipelago

Worley-cellular islands blended with island falloff per cell, sea-level coastal smoothing. Splat sand / grass / rock / deep.

alpine

Ridged FBM with heavy thermal + light fluvial; high snowline, exposed cliff faces. Splat grass / rock / snow.

desert_dunes

Directional billow with shallow gradient bias, minimal erosion to preserve dune crests. Splat lit-sand / shaded-sand / rock / dust.

river_valley

Two opposing plateaus with a deep fluvial valley between them. Splat water / grass / rock / dirt.

End-to-End

From prompt to colored mountains in five calls.

The complete sequence an agent runs to produce a 2 km mountain landscape with auto-derived heights and a splat-driven material — every step verified.

# 1. Spawn a 2 km landscape (32×32 components × 63 quads = 2017 verts/side). landscape_create(component_count_x=32, component_count_y=32, sections_per_component=1, quads_per_section=63, z_scale=100.0) # → "Landscape" actor: 2016 m × 2016 m, max height 256 m # 2. Fetch a starter graph at the matching resolution. graph = terrain_preset_get(name="mountain_range", resolution=2017, seed=12345)["graph"] # Optional: mutate the graph here. graph["nodes"]["thermal"]["iterations"] = 12 # softer thermal # 3. Preview before committing assets — returns inline base64 PNG. terrain_preview(graph=graph, size=512) # 4. Evaluate at landscape resolution and push through the edit layer. # Heights auto-derive: 30% of footprint -> 605 m max above sea here, # so a 2 km landscape gets proportional mountains, not a tower. terrain_apply_to_landscape(graph=graph, landscape="Landscape") # 5. Generate a splat-driven Landscape Material and assign it. terrain_create_auto_material( path="/Game/Terrain/M_AutoMR", splat_path="/Game/Terrain/Splat_MR", landscape="Landscape") # → assigns sand / grass / rock / snow blend in one call.

Five MCP calls. No editor clicking. The agent never leaves the JSON-RPC loop, and every step is independently verifiable via texture_inspect.

Verification

Statistics, not vibes.

Every generated heightmap and splat is round-trip-inspectable via texture_inspect. The shipped test suite asserts these statistics on every preset, every run.

Preset (513² heightmap)RangeStdDevDistinct uint16Histogram bins ≠ 0
island0.850.11524,39714 / 16
mountain_range0.370.07622,1667 / 16
canyon0.360.07222,2817 / 16
archipelago0.860.22544,32614 / 16
alpine0.380.09424,9077 / 16
desert_dunes0.310.05417,4846 / 16
river_valley0.390.07324,6687 / 16

Real terrain has high distinct-value count, non-trivial stddev, a wide range, and populates many histogram buckets. A flat field would score zeros across the board. The harness shipped at overnight/terrain_tests.py covers the presets plus determinism, custom-graph, and four error paths.

Author terrain at the speed of prompt → preview → commit.

Get Rekall UE on FAB and let your AI agent start sculpting Landscapes from natural language.

Get Rekall UE on FAB →