Carve the water before you build anything beside it — a 7.7 km river, solved offline and cut into the terrain
The open-world map is an 8.1 km square of landscape. A town sits on it, roads run across it, and until this week there was no water anywhere. A river fixes several things at once: it gives the terrain a reason to have the shape it has, it gives the road network something to cross, and it puts a natural boundary in a map that otherwise just fades out at the edges.
So: one river, from a spring inside the map to the southern map edge. It came out at 7.67 km, of which 3.7 km falls inside the ring the player actually spends time in.
The method: solve it as data, then rasterise
Nothing about the river's shape is decided in the editor. The pipeline is four steps and one file that everything else reads:
- Survey. A script inside the editor line-traces the landscape along a hand-drawn corridor
— 5 m stations, cross-sections sampled every 10 m out to ±200 m each side, so the solver has room to move the line.
- Solve. A script outside the engine (the editor's embedded Python has no numpy) turns that
survey into a centreline, a water surface, a width and a depth per station. Output: 1,464 stations, and that file is the single source of truth.
- Decimate. A second offline pass reduces those stations to spline points.
- Build. In-editor scripts carve the channel, create the water bodies, dress the two ends
and then measure the result against the solved file.
Because the carve and the water read the same numbers, the trench and the water in it cannot drift apart. Re-running the whole chain takes about a minute, and every editor-side script defaults to a dry run that reports what it would do and touches nothing.
Two things in the solver are worth stealing.
The line-finding does not minimise elevation. The obvious formulation — walk the cross-sections and take the lowest ground — was tried and it is wrong: the line dives into whichever 30 m hollow happens to sit beside the corridor and stays there. It turned a 7.5 km river into 11.9 km of which 6.8 km was underground. What it minimises instead is the cut: how far the ground stands above the water surface you are trying to fit. Ground sitting up to one channel-depth below the surface costs nothing, because that is simply the channel. Surface and line are solved together, alternating until they stop moving, with a penalty per metre of sideways shift so the result is a river rather than a scribble.
The water surface is strictly monotonic downhill. It hugs the ground from below wherever it can and, where the ground rises, just keeps falling at 0.4 m per km. That is what makes the river obey gravity everywhere without anyone hand-checking it. Where the fitted surface ends up more than 2.5 m below natural ground, the river would need a trench rather than a valley — so it goes underground there instead. On this landscape that produced exactly one subterranean reach, 270 m long, under a ridge north-west of the town. A number decides where the river disappears, not a hand-drawn line, and that is why the surface river is two water bodies rather than one: a water body follows a single continuous spline.
Trap one: the landscape spline tool's side falloff is inert
Symptom to search for: the landscape spline falloff width does nothing; banks come out vertical; the affected band is always exactly the spline width.
I measured it: a falloff of 2 m and a falloff of 150 m produce byte-identical ground. The tool also sweeps a rectangle — square ends, no rounding — so you cannot get a graded bank or a circular pad out of it by asking nicely.
Everything sloped therefore has to be built from concentric passes. The channel bank is eight rings stepping down at three horizontal to one vertical; the outer floodplain blend is another eight rings at twelve to one, 6 m apart, giving half a metre of rise per ring over about 48 m. Step size is the whole game here — keep the rise per ring well under half a metre and the landscape's own interpolation rounds the rest into something that reads as a bank rather than as terracing. A disc, if you need one, is a spline tracing a circle of half the radius, swept at that same half-radius as its width.
Order matters inside the carve as well, and it is the same lesson as the day's headline: the passes run outside-in — blend, then terrace, then banks, then bed. Run them the other way and each pass fills in the one before it.
Two more things about that tool, both of which fail quietly. It writes absolute heights, and only the base landscape edit layer replaces terrain — every other layer stores offsets, so carving into the wrong layer adds your riverbed to the existing ground and lifts the river into the sky. And landscape edits are not undoable; there is no Ctrl+Z. The pre-carve survey is the only record of the original terrain, so the carve copies it into a snapshot folder before it touches anything.
Trap two: the river width property is full width, and the engine's own header says otherwise
Symptom: your river renders exactly half as wide, or exactly twice as wide, as the number you set.
The water spline metadata header comment describes river width as measured from the centre in each direction, which reads as a half-width. It is wrong. Every consumer inside the water body component divides by two, so the property is full width, in centimetres. Depth is not halved — it is measured downward from the spline. And the spline Z is the water surface, not the bed: the runtime query hands that Z straight out as the surface, and the mesh builder zeroes the vertical part of the bank offset so both banks sit exactly on it.
Related, and equally silent: spawning a water body from script does not run the actor factory, which is what normally assigns the water materials — spawn it from Python and every material comes out None. Assign them by hand.
Trap three: the water zone's texel size sets the minimum renderable river
Symptom: a narrow river draws as a dashed line, or a stretch of it does not draw at all, while everything about the water body looks correct in the details panel.
The water surface's silhouette is rasterised into the zone's info texture non-conservatively — a triangle lights a texel only if it covers that texel's centre. At the template's zone extent and render target resolution that worked out to 797 cm per texel, and anything under about 8 m wide came out as dashes. The first draft started the beck at 0.6 m wide and the top 350 m of the river simply did not exist.
The fix is to fit the zone to the corridor and raise the render target, which brought it to 144 cm per texel; the width curve then gets a floor of 2.2 m, about 1.5 texels, which is a renderer constraint rather than an aesthetic one. The same number governs how the two ends are finished: a river that stops dead at full width shows you the cut edge of a 37 m wide, 2 m thick slab, so the source tapers up from a trickle and the outfall is extended past the landscape edge — but still inside the zone that renders it — and narrowed away to that same floor. It can never taper to nothing: width clamps to a tiny non-zero value, and a depth of zero makes the water fall back to a fixed default.
The one that cost real time: a bridge stranded by a carve that came later
There was a test bridge over the river reach nearest the town, built the same week and verified properly at the time: three 15 m spans, the deck holding its height across the crossing, both approach embankments sitting 20.1–20.8 cm under the road, which is exactly the road painter's formation depth. It was correct.
After the channel was carved it was ruined, and had to be deleted.
The obvious diagnosis was wrong. I expected the bed to have dropped away under the deck. It had not — the ground under the bridge centre actually rose half a metre, from a surveyed −63 to a final −8, and the deck at 671 over water at 192 is a perfectly ordinary 4.8 m of clearance. The failure was at the abutments. The river's design corridor there is 19.8 m of water surface plus 23.7 m of berm plus 65.1 m of outer falloff — 98.7 m each side, well over four times the bridge's own 45 m length. The carve therefore re-graded the ground under both abutments and under both approach ramps, overwriting embankments that had been built correctly an hour or so earlier.
There is no clever fix. The rule is the ordering: carve the water first, then build anything that stands beside it. A water feature's footprint is its design corridor, not its wet width, and on this river the corridor is five times the water.
A related gap is still open and worth naming: the road builder's gate that refuses a road crossing water only knows about water body actors, so it does not yet refuse a road that drapes into a river which exists only as solved data.
The culverts, and why a quarter-scale arch is not a toy
The two ends of the underground reach needed a reason to exist. The first design was a stepped karst swallow hole and it was wrong twice over: nothing on the map justifies karst, and a 28 m bowl nearly 5 m deep leaves nothing a small bridge can stand on. The build script now fills that bowl back in before it builds anything — and fills it generously, because measured 10 m downstream the old outer ring was still 2.7 m below the new bank.
What is there now is a small brick bridge over a culvert mouth: the brook narrows, runs under a masonry arch and into a barred opening in the bank. It is a quarter-scale bay lifted from the road bridge kit, and that works for one specific reason — the kit's materials are instances of a world-aligned brick master, so texel size follows world size. Shrinking a 14 m road bay to a 3.5 m arch does not shrink the bricks. You get a small bridge built of normal-sized bricks, which is exactly what a real accommodation bridge is. Never "fix" this by scaling the UVs.
Three details that are not obvious:
- The parapet must not be scaled with the arch. At a quarter it is 27 cm tall and the whole
thing reads as a kerb. It gets its own scale so it stays about a metre.
- Do not reuse the kit's abutment module as an exposed headwall. It is a stepped wing wall
designed to be buried in a road embankment; standing in an open channel, its steps and its 5 m depth against a 3.13 m deck hang in the air as floating brick panels. Plain brick blocks instead.
- The banks have to converge on the arch, or the ground sits a metre or two below the deck
either side and the headwall stands proud like a retaining wall.
The grille is seven plain 10 cm bars at 50 cm centres, set 1.5 m back inside the arch. The previous version stretched an ornamental iron balustrade eight times vertically to do the same job, and the verdict on that was that the metal did not look right at all. The bars are decoration as far as physics is concerned — a hidden slab does the blocking, capped below the arch soffit so the bridge stays walkable.
Verification, which was wrong three times before it was right
The checker measures four things, in order of how much they matter: that the carved bed is still where the carve left it, that the finished spline follows the solved centreline, that width, depth and velocity read back correctly per point, and that the water is actually contained by the bank crests either side.
It passes. The spline tracks the solved centreline to 14 cm, width and depth error 0.00 cm, median freeboard 1.99 m against a design 2.0, and the terrain measures untouched by the water plugin.
Getting the checks right took three goes, and every wrong version failed in the direction of crying wolf:
- Compare against the polyline, not the nearest solved station. The spline is a curve through
the authored points and legitimately bows between them; the decimation tolerance bounds the polyline, not the curve.
- Skip the deliberate off-map overrun. The outfall is extended past the landscape edge on
purpose, so of course it does not match the solved course out there.
- Skip the broads in the freeboard test. Some stretches are meant to spread out rather than
be embanked.
- Count only bed fill as damage. Bed below the design line is a natural hollow left as a
pool, which was a decision, not an error.
That last one generalises: a verifier has to know which of your exceptions are deliberate, or it will keep reporting your design back to you as a defect.
The single check that earns its place is the first one. The water plugin's landscape brush defaults to on, and it re-arms itself: building the second water body re-templates the first one's component from the class defaults and switches it back on. Disarming per body cannot fix that, because the damage happens after the per-body work has finished — you have to disarm in a loop once every body exists, and then go and re-measure the ground to prove the brush never fired. There is a second carving path on the same component, also on by default.
What this does not yet prove
- Nobody has walked, swum or driven any of it. Every number above is an editor-side
measurement of geometry, not a playtest.
- The river can flood, and the mechanism is proven but wired to nothing. There is no runtime
water-height API in this engine version; the only lever is the actor transform, and it works — moving a body up 150 units moved the queried water surface by exactly 150 and restored cleanly. Both bodies sit at Z = 0 with all their geometry in their spline points, so a flood is a single location set with no base-height bookkeeping. Nothing drives it yet.
- A flood that tops the banks is a different job from a flood that reaches the town. The town's
roundabouts measure 4.8–5.1 m above the river water surface and 175–203 m from it, and a river body widens only symmetrically about its own spline — it can get wider, but it can never send a lobe down a street. Water standing in the streets needs a different kind of water body, one that fills to a level and conforms to whatever terrain is under it.
What to take from it
- Carve terrain-modifying features before you place anything that stands beside them, and
size the exclusion by the feature's design corridor rather than its visible width. Ours was 98.7 m each side of a 20 m river.
- When something that was built correctly is later broken, measure before diagnosing. The
bridge did not sag; the ground under it rose, and the real failure was 45 m away at the abutments.
- Solve the shape offline, rasterise it in the editor. One data file read by both the terrain
carve and the water means the trench and the water cannot drift, and re-running the whole chain costs a minute.
- Check whether the tool's parameters do anything at all. A falloff width that gives
identical output at 2 m and at 150 m is not a subtle bug; it is a parameter to stop believing in, and slopes then have to be built out of concentric passes.
- Renderer resolution is a design constraint, not a polish pass. If the info texture is
144 cm per texel, "how narrow can a stream be" has a hard answer, and it belongs in the width curve rather than in a bug report.
- A default that re-arms itself needs a loop and a proof, not a single write. Set it, do all
the work, set it again for every actor, then measure the world to confirm it never fired.
- Teach the verifier your deliberate exceptions, or it will spend three rounds reporting your
own design decisions back to you as failures.