Devlog

From a flat plane to a terraced, eroding world

An aerial view of eroded highlands under fog — a river winding between rocky cliffs, green and autumn-orange tree canopies marking different biomes, and snow-capped peaks fading into the distance
An aerial view of eroded highlands under fog — a river winding between rocky cliffs, green and autumn-orange tree canopies marking different biomes, and snow-capped peaks fading into the distance

Terrain generation started as fifty lines of code: spawn a flat plane, bake a NavMesh — the mesh colonists actually path across — over it, done. That was the entire world, for a while.

It isn’t anymore. Here’s where it actually stands: the pipeline that replaced the plane, the two bugs that shaped its newest chapter, and what still doesn’t hold up under a real playtest.

An early build: a hand-placed river across flat ground, a single low-poly building, and a treeline backing onto one modelled mountain — no erosion, biome or generation data behind any of it
Where this started.

What actually generates now

Every new world runs the same nine-stage pipeline from a single seed: layered noise shapes the raw landmass, a hydraulic and thermal erosion pass carves that into ridges and valleys instead of leaving it looking like noise, a moisture layer and the erosion’s own runoff feed a classifier that reads height, slope and wetness to decide what’s alpine snow, rocky highland, open meadow, swamp, river, lake or ocean at every point on the map — and that classification drives both the ground texture and what’s allowed to spawn there: a resource node, a species of wildlife, a tree.

That same biome data also drives the visual pass on top of it — cel-shaded ground textures and per-biome grass and tree colour, which is most of why the screenshot at the top of this post looks nothing like the build above.

Getting the rivers to actually connect

Rivers come from the same erosion pass, but getting them to look like rivers took longer than generating them in the first place. The first version placed a river anywhere a cell’s flow accumulation (how much upstream area drains through that cell) crossed a fixed threshold, and it rendered as scattered circular holes instead of winding channels — every small dip in the noise trapped its own tiny puddle of “river” that never connected to anything.

The fix is a standard hydrology trick this project didn’t have yet: depression filling. Before working out which way water flows, a priority-flood pass raises every low pit up to its lowest escape point, so every cell gets a guaranteed, if occasionally tiny, downhill path to the map edge. Flow accumulation runs on that filled copy; the actual carving still reads the real, unfilled terrain, so the fix only changes which cells count as “river,” not the land’s shape.

That connected the network, which immediately exposed the next problem: a fixed flow threshold tuned for one map either drowned a better-connected map in minor trickles or missed a smaller one entirely. The threshold is now computed fresh every generation — a percentile (99.7 by default) over that specific world’s own flow distribution, so it never needs retuning per map.

The terracing rewrite, and a test that lied

The newest piece landed this week: instead of a handful of mesa and mound plateaus dropped at scattered sites, the whole map is now partitioned into a mosaic of terraced regions — jittered, noise-warped Voronoi cells (regions grown outward from scattered seed points, a bit like a shattered-glass pattern), each quantized onto a terrace whose step grows with elevation, from 3.5 m in the lowlands to 20 m near the peaks. Neighbouring regions fuse or stay separate depending on how close their heights land, so deck size varies on its own instead of being hand-authored.

Worth saying plainly: there was never flat ground on this heightmap to go and find. Measured directly, the largest connected region that passes a real plateau’s own flatness and prominence gates tops out at 915 m² against the 12,000 m² a lowland site needs — and even loosening the slope gate to 22° (a hillside, not a plateau top) only reaches 10,826 m². Layered noise plus erosion puts slope everywhere, and erosion actively removes flats. So the generator doesn’t detect plateaus — it authors them, directly, as part of the terracing itself.

The rewrite also broke something the old plateau system never had to worry about: whether the map stayed one connected piece of walkable ground. Every region got a ramp down to a lower neighbour, which sounds like it should guarantee a path to the bottom — until a region turns out to be ringed entirely by higher ground. That’s a basin: nothing forces a way out of it, and everything draining into it is stranded. Measured on a real baked NavMesh: 21.7% of the map was reachable from any given point. The fix — a pass that unions regions by adjacency and adds the cheapest possible crossing between separate groups until only one is left — needed just 3 to 6 extra connections per world to take that to 96.4%.

The instructive part is what didn’t catch it first. A plain 45°-slope flood fill over the heightmap reported 99.4% of the map as one connected piece before any of this was fixed — because it tests the terrain, and the terrain was never the problem; the NavMesh was. The only test that actually caught it was asking the NavMesh itself to path between points and counting what it refused.

Dressing the cliffs the terracing creates

A world full of terraces is a world full of cliff faces, and until this week those faces were bare. The new pass finds them by reading the heightmap directly rather than guessing from “ground steeper than X”: a slope field gets thinned down to ordered, smoothed lines running along the top of every rim, and rock formations get built out along those lines. One generation measured 7.99 km of rim across 193 separate lines, with 287 rock formations placed along the 4.8 km of it worth dressing.

Two mistakes on the way there, both worth remembering. First: thinning a rim by comparing every cell against all eight neighbours deletes almost the whole thing, because height changes smoothly along a rim — on one map that rule left 1,582 scattered survivors traced back to a single 24 m fragment out of 8 km of edge. It has to compare only against the uphill side. Second: the rock formations pivot at their own visual centre, not their base — treating that pivot as ground level buried every block roughly 42% into the terrain before anyone noticed. Formation size itself comes from measuring the art directly: hand-placed reference rocks scale as roughly 6.6 × √(face height), and non-uniformly — the tall ones stretch far more than they widen.

Where it honestly stands

The core pipeline — noise, erosion, rivers, biomes, texturing — has been running and played with for a while. The two pieces above are new enough that “measured” and “playtested” are still different claims: both the terracing rewrite and the cliff dressing have been run and checked directly, numbers and all, but neither has been through a real playthrough yet.

What’s still open:

  • Cliff faces smear their texture. The terrain shader doesn’t sample sideways onto vertical surfaces, and the engine projects ground textures straight down onto the terrain — a face steep enough to be worth calling a cliff stretches by construction. That’s a shader fix, not a terrain one.
  • A handful of sharp spikes remain. A cascading smoothing pass took the worst offenders down substantially — one measured spike dropped from 39.7 m of prominence to 27.1 m — but the single worst case still pokes up about 23 m over a 31 m radius, and the exact remaining cause isn’t pinned down yet.
  • The cliff dressing doesn’t run automatically. It’s triggered by hand today, not as part of world generation itself. Folding it into the automatic pipeline is next.

What’s next

Sideways texture sampling for cliff faces, closing out the last of the spikes, folding the rock dressing into the automatic pipeline — and then an actual playthrough of the combined terraced, cliff-dressed world, rather than just measuring it in the editor.

If there’s a piece of this worth digging into further, say so on Discord.

Read more in the wiki