32 KiB
Terrain: next steps
A working brief for the next session on the heightmap generator. It exists so a cleared session can pick the
work up cold. Terrain.md is still the specification and the decision record; this is only
"where it stands, what looks wrong, and what to do about it". Fold anything settled back into Terrain.md
and delete the corresponding section here. Do not let the two drift.
Everything below is off the ladder (D-47). No gameplay code may reach into the generator, and nothing here
blocks a step in Steps.md.
1. Read this first
The generator is a Go CLI in Tools/Terrain/. It turns a seed and RawContent/World/World.json into a
heightmap, by building an uplift rate field and letting a stream-power erosion solve produce the terrain
from it. The noise is not the terrain; the noise is the tectonics.
cd Tools/Terrain && go build -o bin/terrain.exe ./cmd/terrain && go test ./...
# a preview run: ~4 minutes at 1400², the iteration loop
Tools/Terrain/bin/terrain.exe generate --size 1400 --out RawContent/World/Try1 --quiet
# another continent
Tools/Terrain/bin/terrain.exe generate --size 1400 --seed 9342 --out RawContent/World/Seed_9342 --quiet
--size keeps the map's physical extent and samples it more coarsely, so metres, uplift rates and the
stream-power constants all still mean what they mean. Do not tune on --size 512: the geology cell there
is 28 m against 8–10 m at full size, and steady-state slope goes as U/(K*A^m) with A = cell² at every
divide, so a 512 preview shows gentler ground than the real run for reasons that have nothing to do with the
change being judged.
Every bs.*-style knob in the manifest has a --flag override so an experiment does not need a file edit:
--intraplate, --intraplate-swell, --convergent, --k, --diffusion, --talus, --critical-m2,
--critical-slope, --slope-cap, --hillslope-substeps, --lithology-types, --fault-scale, --steps,
and for the coast --no-coast, --outline-octaves, --outline-gain, --shelf-km, --surf-reach,
--cut-fraction, --deposit-reach, --drift, --river-sediment.
What a run writes
| File | What it is for |
|---|---|
preview.png |
Hypsometric tint, hillshade, rivers. "Does this look like a landscape" |
preview_detail.png |
A crop at 2× vertical exaggeration. The whole continent at 1600 px cannot show whether lowlands read as hill country or as small mountains; this can. Move it with --crop-x/-y/-size. It cannot be rendered finer than the grid: a crop of 0.14 at --size 1400 is 196 cells, so that is the image, whatever Size asks for |
geology_height.png |
The 16-bit heightmap itself, encoded to the manifest's elevation range |
map_uplift.png |
The most useful diagnostic. Rock uplift in mm/yr — the field everything else is a consequence of. It and map_slope should be recognisably the same picture; when they are not, something downstream is overriding the tectonics |
map_slope.png |
Degrees, 0–45 |
map_relief.png |
Local relief over 500 m. Separates a 5 m hummock from a 500 m mountainside — both stand at 30° and the slope map cannot tell them apart |
map_erodibility.png |
The lithology multiplier on K. Where texture inside a range comes from |
map_exposure.png |
How open the water is in front of each stretch of shore, 0 sheltered to 1 open. Drawn only within a kilometre of the waterline, because past that it is a map of the continent's medial axis. The one to read when a beach turns up on a headland |
map_coast.png |
Everything the coastal pass moved, in metres: cool where the surf cut, warm where the sediment landed. The sea floor is excluded, or its few hundred metres would swamp the few the processes move |
map_flow.png, geology_flow.png |
Log drainage area: the rivers |
map_basins.png |
One colour per drainage basin, hashed from the basin root. The direct test of whether the solve made a network rather than scratches: basins must tile the land, sizes must span orders of magnitude, and divides must sit on the ridge crests. Confetti means the router is re-deciding where water goes every few cells |
meta.json |
The full manifest as resolved, plus every statistic |
How a run is judged
The printed summary is the verdict, and the block that matters most is the per-uplift-class breakdown — map-wide medians cannot answer "are the plains plains", which is precisely how the last problem stayed invisible. Current state, seed 7 at 1400²:
slopes: 54% under 15 deg, 77% under 30, 0.3% over 50, median 11.2 deg
slope-area: exponent -0.384 (expect -0.500), R2 0.225 over 6 bins
hypsometric integral 0.107 (concave: over-eroded); drainage density 0.48 /km
by uplift class:
plain 0.00..0.10 mm/yr 42% of land slope 0.8 deg median, 2.8 P90 relief 9 m/490 m at talus 1%
rolling 0.10..0.50 mm/yr 7% of land slope 7.5 deg median, 23.1 P90 relief 85 m/490 m at talus 1%
mountain 0.50.. up mm/yr 51% of land slope 28.5 deg median, 36.0 P90 relief 195 m/490 m at talus 33%
The coast prints its own block after it, and the numbers to read first are the sediment budget — it is the one part of the pass not derived from something already measured — and the exposure percentiles, which say whether the shoreline has any bays for the shelter to work with:
coast: 95 km of shoreline, 37% sea, shelf 86% of it; surf planed 2.8 km2 and cut 21.23 Mm3,
16 river mouths delivered 3.87 Mm3, 25.04 Mm3 laid (0% unplaced) as 2.92 km2 of new beach;
mean cliff 3 m, 0.00 km2 drowned; shore exposure 0.00 / 0.90 / 1.00 (p10/p50/p90)
An unplaced fraction above a few per cent means the sediment has nowhere to go and the deposition gates are wrong; a median exposure of 1.00 means the outline has no bays at all and neither the surf reach nor the shelter is doing any work.
The slope–area fit cannot judge one change on one seed, and it was nearly used to reject a good one. It is the number this document calls the proof that closes the work, and at five or six bins on a 1400 grid its seed-to-seed spread on identical code is larger than most changes: seed 7 gives −0.480 at R² 0.317, seed 9342 gives −0.698 at R² 0.704, and seed 67914 gives −0.575 at R² 0.944. Use it paired — the same seed before and after — and across at least two seeds, or raise the bin count before leaning on it.
2. What was just built, in one paragraph
The coast, which until now was a line in a mask: the sea floor dropped to a flat plane at −180 m in one step
and no process knew the shoreline was there. internal/coast adds three that do, each derived rather than
drawn — a continental shelf whose width is read off the relief standing behind each stretch of shore, a surf
that planes the land to a shore platform within a reach set by how open the water is (the cliff is the step
where the reach ends), and a sediment budget that carries what the surf cut along the shore and lays it in
sheltered shallow water, with river mouths delivering their own load. It runs after the fluvial solve, on the
terrain the solve produced, and it owns the sea floor outright: uplift.Result.Bathymetry is gone and ocean
cells stay at sea level for the whole solve. Measuring it then said something about the continent rather than
about the coast — the fetch reported the median stretch of shoreline as fully open, because five octaves of
outline noise over a 14 km map put the finest coastal feature at 450 m and a coastline is fractal. D-51 takes
the outline to 8 octaves at gain 0.62, which is 96 km of shoreline against 64.
Full detail, including four things that were wrong first, is in Terrain.md under What was built, and where
it differs.
3. Where this is going
Composition is parked. The mountain fraction, the range grain and the fault traces are all real and all still listed below, but they are tuning and the map is good enough to work against. Do not spend the next session on them.
The goal is: get generation working end to end, then make the world author-driven and scalable. Three things, in order.
3.1 Finish the pipeline (build-order step 6)
Passes 8–14 are unbuilt — upsample, detail noise, strata, particle, fine thermal, spawn pad, derive — so the
generator stops at the geology grid and L_World is still built by the numpy pipeline it was meant to
replace. Until this lands there is no full-resolution output and nothing to import, at any scale. It is also
the only work that changes how the terrain reads to a player standing on it: see §4.C.
3.2 Painted maps as the source
An author paints a world map; the simulation turns it into terrain. The manifest already anticipates a file
source — "source": {"kind": "file", "path": ...} is documented in RawContent/World/README.md and
field.ReadHeightmap exists — but nothing in the Go tool reads it: Source.Kind appears only in a
Describe() string, and the run always builds noise. So this is new work, not a re-wiring.
Paint the uplift, not the height. This is the one design decision that matters and it follows directly from D-47 and from everything measured this session. The architecture is noise becomes tectonics, and the solve makes the terrain; a painted heightmap would be handed to a solver that promptly erodes it into something else, throwing away the drainage network that is the entire reason the generator was rewritten. Painting uplift instead means an author draws intent — "a range here, lowlands there, coast like this" — and gets terrain with real rivers, real divides and real valley hierarchy honouring it.
Suggested channels, all optional, all falling back to the procedural field where absent:
| Painted layer | Feeds | Notes |
|---|---|---|
| Land / sea mask | uplift.Result.Land, Base |
The outline. Almost certainly the first thing anyone wants to draw |
| Uplift rate | Result.Rate |
The load-bearing one. Greyscale mapped to a mm/yr range from the manifest |
| Erodibility | Result.K |
Rock types. Cheap, and it is where texture inside a range comes from |
| Sea floor | Result.Bathymetry |
Cosmetic; it is put back after the solve and never erodes |
| Fault lines | buildFaults |
Later. A line layer, not a raster |
Rivers cannot be painted directly, and it is worth knowing why before someone tries: a river is an
output of the drainage solve. What does work is biasing — raise K along a painted line so the water finds
the soft rock, or drop the uplift slightly along it, or seed a shallow valley into the initial relief. The
solve then chooses to put a river there for its own reasons and the result is still a coherent network. A
painted line forced into the height directly would be cut apart by the first thousand steps.
The blend rule, which keeps painted maps from looking painted. A painted map is coarse — 2048 px across a 100 km world is 50 m a pixel, five geology cells. Upsample it smoothly and let procedural noise supply everything below its pixel size: the painted map owns wavelengths above its resolution, noise owns those below. Without that rule a painted world is visibly blocky at the paint resolution; with it, an author controls structure and the generator still supplies texture.
3.3 Scale, and why tiling is an architecture question
A world is big. Today's canvas is 14.28 km a side; the interesting sizes are 50–200 km. The numbers, measured and extrapolated from the 256 s full geology run:
| World side | Area | Geology cells at 8 m | Fluvial solve, 1000 steps | Grid memory |
|---|---|---|---|---|
| 14 km — today | 204 km² | 3.2 M | 4 min | ~150 MB |
| 50 km | 2 500 km² | 39 M | ~50 min | ~1.8 GB |
| 100 km | 10 000 km² | 156 M | ~3.5 h | ~7 GB |
| 200 km | 40 000 km² | 625 M | ~14 h | ~28 GB |
The fluvial solve cannot be tiled. Drainage area accumulates across the whole map and the priority-flood needs global connectivity, so a river crossing a tile boundary needs its upstream catchment from the next tile. Solving tiles independently gives wrong drainage areas and a discontinuity at every seam — and drainage area is the term the whole model is built on. Halo exchange between tiles would work in principle and is a large, iterative piece of work.
The detail passes tile perfectly, because every one of them is local: noise is pointwise, thermal weathering propagates a cell at a time, and a droplet travels at most its lifetime in cells.
So the architecture already contains the answer, and it is the two-grid split that is already there:
Solve the geology whole, once, at a fixed physical cell size. Tile only the detail.
That gives consistent relief for free, because the geology cell never changes — which matters more than it sounds, and §4.D.3 explains why. It makes maximum world size a memory-and-patience question rather than a correctness one: ~50 km is an hour, 100 km is an overnight bake, and beyond that the geology stage needs to go out-of-core. Since the goal is explicitly a batched, offline bake, that seems an acceptable trade — but it should be a decision made deliberately, with these numbers in front of whoever makes it.
Two rules that make tiles seamless, and are much easier to adopt now than to retrofit:
- Index every noise and every hash by absolute world coordinates, never by tile-local index. Both the
fBm lattices in
internal/noiseand the D8 router's jitter (internal/fluvial/jitter.go, D-50) currently key off grid index. Two tiles would then get different values for the same physical place and every seam would show. This is a small change now and a pervasive one later. - Every tile carries an overlap margin, discarded after the pass. Size it by how far the pass can move material: a few cells for thermal, the droplet lifetime (~40–64 cells) for particle, zero for pointwise noise. Cheapest correct approach; no inter-tile communication needed.
4. What looks wrong now
Ordered by how much it matters to the direction above, which is not the order of how visible it is on a preview image.
C. Detail — nothing exists at player scale · the blocker
Passes 8–14 of the pipeline table in Terrain.md are entirely unbuilt: upsample, detail noise, strata,
particle erosion, fine thermal, spawn pad, derive. The generator stops at the geology grid — 8 m cells at full
resolution — so at 2 m quads a player stands on a 4× upsample of a coarse grid with no detail added at
all. Ledges, scree, gullies, the strata shelves on a cut face: all of it lives in those passes, and every
one already exists as tuned numpy in Scripts/Authoring/heightmap_erosion.py waiting to be ported, not
reinvented. Carry its brakes across unchanged — the droplet slope gate, the per-step cut cap, the load cap,
the 3×3 cut brush and the own-cell deposit are each a lesson from the Worklog.
Budget from Terrain.md: upsample and detail noise 15 s, particle 90 s, fine thermal 20 s. The fluvial pass
is already 256 s against 120 s budgeted, so the five-minute bar is at risk before these land — and §3.3 says
the bar is probably the wrong constraint for a batch bake anyway. Worth deciding rather than drifting.
D. Scale-independence — the one that becomes load-bearing
D.3 Relief is resolution-dependent, and multi-scale makes that a blocker rather than a wart. Measured
earlier: 1020 m relief at 512² against 2605 m at 1786² on one seed. The cause is that with
critical_area_m2 at 0, stream power is applied down to a single cell, so the divide slope is
U/(K·cell^2m) — halve the cell and every divide steepens, for ever. As long as there was one canvas this was
a wart. The moment the same painted map is meant to produce worlds at different sizes or resolutions, it
means the same input gives a different landscape depending on grid size, which is fatal to the whole idea.
The textbook fix is a critical area, and it was re-measured this session and still fails (§6) because the hillslope it creates has no transport law strong enough to shed its uplift. So this is genuinely open, and the two candidate directions are:
- Fix the pairing: critical area plus a hillslope diffusivity that scales with cell size (effective
Dgrows roughly as cell², which is the standard sub-grid argument). Principled, and it makes the critical area work rather than fail. - Sidestep it: always solve the geology at one fixed physical cell size and never vary it, per §3.3. Free, correct by construction, and it costs the ability to trade resolution for runtime on a big world.
The second is the recommendation for now because it is free and unblocks everything; the first is what to build if the bake times in §3.3 turn out to be unacceptable.
D.4 Two open questions that are design, not defects. The hypsometric integral is 0.10 against 0.4–0.6 for
a mature landscape — no longer a bimodal uplift field, now simply that 42 % of the land is a near-sea-level
plain, which is a question for the continent block and will move when composition is tuned. And drainage
density is 0.41–0.48 /km, right on the 0.5 floor set as do-not-cross; it is measured at a 1 km² channel
threshold so it is not directly comparable to a field value, but if it drops further while tuning, raise the
intraplate rate to 0.05 before touching anything else.
E. The coast — what it built, and the four things it cannot do yet
E1. There are no sea cliffs — withdrawn, and the metric that said so is replaced. This entry read "mean
cliff height is 2 to 3 m, and the surf has nothing to cut". Both halves were wrong, and the way they were wrong
is the part worth keeping: the statistic measured the drop from a cell to its seaward neighbour, which is a
gradient. One cell of a 10 m grid at the angle of repose is 7 m, so the number could never have exceeded 7
whatever the coast did — it read 2 m on a plain coast and 3 m on a cliffed one because it could not tell them
apart. It is now backshore height: the land's elevation one to two surf reaches inland, median and P90. On that
metric the coast has always had cliffs (seed 7 P90 88 m, seed 9342 108 m, seed 67914 120 m); the median, 3 to
9 m, says the ordinary coast is a plain, which it should.
What the mask taper was actually doing is narrower and is now fixed anyway (D-52): it flattened the ~100 m strip the surf works in, so the cliff began a hundred metres inland instead of at the water. Removing it is worth 35 to 67 % more surf cut and a visibly steeper shore, and the principle stands — where the land ends does not decide how fast it is rising — but it was a sharpening, not the transformation this entry predicted.
E1b. The map margin draws one coastline in seven, and it draws it straight. Measured on three seeds:
14.1 %, 15.0 % and 14.0 % of the waterline sits inside the 4 % margin band that continentMask imposes to keep
land off the map border. The margin tapers by distance-to-edge, and a contour of distance-to-edge is a line
parallel to that edge, so wherever the continent would have run past the boundary it is cut off square. This is
pre-existing and it is not the frozen-rim failure the margin exists to prevent — TestBorderIsAlwaysOcean
confirms every border cell is still ocean on all three seeds, so nothing is frozen. It is cosmetic, and D-52
made it conspicuous: land inside the band now takes the full 2.0 mm/yr instead of a tapered rate, so the
straight-cut coast can be a mountain range rather than a low plain, which is exactly what seed 67914's southern
coast is. Cheapest fix, and it belongs with the outline work rather than with the coastal pass: perturb the
margin distance with a low-amplitude noise field so the cut follows a crenellated line instead of a ruled one.
E2. The shelter contrast is real but thin. Exposure comes out 0.00 / 0.90 / 1.00 at p10 / p50 / p90, so the distribution is one long tail: a handful of genuine embayments and a lot of open coast. A floor of 0.15 on shelter carries most of the deposition, which is deliberate — measured with no floor, 73 % of the sediment budget came back unplaced, because real exposed coasts do have beaches, they just have less sand than the bay next door. Re-measure this once the outline is painted rather than noised; it is the same question as E1 from the other end.
E3. The beach is a beach at 8 m, which is to say it is not one. The surf reach is 110 m, or 14 cells on the geology grid, and the berm, the wave-cut notch, the scree below a cliff and the sand itself are all finer than that. They belong in the detail passes (§4.C) — and note that the surf reach is one of the few lengths in the generator that is set by physics rather than by the canvas, so it does not scale with the map: at the 2 m detail grid it is 55 cells, which is enough for a real profile. The coastal detail pass is a natural addition to the list in §4.C rather than a separate piece of work.
E4. Deltas are placed, not tuned. river_m3_per_km2 is 1.2e5 with an exponent of 0.6, and both are guesses;
on seed 7 they put 3.9 Mm³ through 16 mouths against 21 Mm³ from the cliffs. The mechanism is right — the supply
concentrates at the mouth and the drift kernel spreads it into the shallows — but nothing has yet asked whether
what comes out reads as a delta. map_coast.png at a crop is where that is judged.
E5. Unverified: speckle at the finer canvas. The outline gain went from 0.50 to 0.62 on measurements taken
at --size 1400 (10.2 m cells). A finer grid resolves more of the threshold's wander, so the D-48 canvas at
8 m, and any painted map after it, could turn the same setting into a scatter of one-cell islands. Check the
shoreline length per unit land area and look at preview.png before assuming it carries over.
B. Texture — wrong at mid scale
B1. 33 % of the mountain class still sits within 2° of the repose angle, so a third of the mountains are
shaped by the clamp rather than by erosion. Down from 44 %, and the nonlinear diffusion pass after the clamp
keeps it from showing as hard facets. Levers, most principled first: raise max_hillslope_substeps and
slope_cap (costs runtime, buys real stiffening); raise diffusion_m2_yr (cheap, but past ~0.05 it smooths
away the landforms — measured before as "melted wax"); or accept it, since a belt rising at 2 mm/yr genuinely
is landslide-dominated in the real world and the clamp is the right model there.
B2. Multiple-flow-direction accumulation is unbuilt. The hash jitter recovered most of the damage (R²
0.055 → 0.459) but D8 still lets a cell drain to only one of eight neighbours, and some basin boundaries on
the plains in map_basins.png are visibly straight. The proper fix is Freeman/Quinn MFD for Accumulate
only, keeping D8 receivers for the implicit solve — Braun–Willett needs a single receiver per node for the
update, but the area can come from MFD. Cost: MFD needs its own processing order (descending elevation)
rather than the D8 stack.
B3. A ribbed, combed texture on the range flanks, regularly spaced, roughly perpendicular to the crest.
Not diagnosed. Candidates to check before changing anything: the ridged-noise initial relief showing through
where the solve has not had time to overwrite it; channel spacing locking to the grid at small drainage area;
or the crests cellular-edge field at crest_weight 0.12. Test with --stage uplift and compare the initial
relief against the final flanks.
A. Composition — parked, but recorded
All three are one-or-two-constant changes. They are listed so they are not rediscovered, not because they are next.
A1. Half the continent is mountain — 51 % of land on seed 7, 44 % on seed 9342, against nothing like that in reality. The cause is arithmetic:
// internal/uplift/uplift.go
rangeMask := percentileMask(band, cfg.Plates.LowUpliftFraction.Hi()*100, 86) // ramps 40th → 86th percentile
r := base + (convergent-base)*float64(rangeMask.Data[i])
With base ≈ 0.055 and convergent 2.0 a cell clears the 0.5 mm/yr mountain threshold at rangeMask 0.229,
which the smoothstep reaches at the 54th percentile — so 46 % of the map is mountain-class by
construction, matching the 44–51 % measured on land. Computed options:
| percentile ramp | exponent on the mask | mountain class, % of map |
|---|---|---|
| 40 → 86 | 1 — today | 46 % |
| 40 → 86 | 2 | 38 % |
| 40 → 86 | 3 | 34 % |
| 60 → 92 | 2 | 24 % |
| 70 → 92 | 2 | 19 % |
| 75 → 95 | 2 | 15 % |
Squaring the mask is not the same as narrowing the ramp: the foreland stays continuous, which is the
property the percentile ramp exists to provide. Note also that the manifest key low_uplift_fraction encodes
the spec's "20–40 % of the map at low uplift" constraint, which is satisfied trivially and always has been;
the constraint that actually binds is what fraction is high uplift, and nothing names it.
A2. Fault traces are drawn curves with stamped ends — visible in map_uplift.png as straight-edged
polygonal facets and an abrupt cut across a summit. Four causes, all in buildFaults: the trace is a single
8-point parabola (const segs = 8, one wander bow); signedDistance over 8 straight segments gives a
piecewise-linear distance field, hence polygonal contours; beyond the last segment inside is false and the
influence stops dead; and if r > convergent*1.6 flattens the strongest throws into plateaus. Fix: an
fBm-perturbed heading, ThrowM tapered to zero over the last ~15 % of length instead of cut at the tip, and
long faults broken into 2–3 overlapping en-echelon segments.
A3. Range grain runs as straight parallel bands — chains run NW–SE like corduroy on seed 9342.
bv.Data[i] = float32(0.5 + across*2.2 + float64(wy.Data[i]-0.5)*0.32) stretches the band 2.2× along one
angle with a single mild warp octave. Raise the warp, or warp with two octaves at different scales so chains
bend and bifurcate.
5. Suggested order
- Adopt the two seam rules from §3.3 now — world-coordinate indexing for all noise and hashes. It is a small change today and a pervasive one after the detail passes exist.
- Build the detail passes (§4.C), and put the coastal detail in with them (§4.E3). Port from the numpy, keep every brake, profile before any GPU work. This is the blocker for everything else and the only work that changes how the ground reads to a player — and the shore is where a player will stand first.
- Decide the scale question (§3.3 and §4.D.3) with the bake-time table in front of you: fixed geology cell and tiled detail, or critical area with cell-scaled diffusivity. The first is free; take it unless the bake times are unacceptable.
- Wire the painted-map source (§3.2). Mask first, then uplift, then erodibility — each independently useful, each falling back to the procedural field. Get the blend rule right from the start. Two of them now have consumers that did not exist before: the mask is the coastline the coastal pass works on, and the uplift is what decides whether the shore is a plain or a cliff (§4.E1).
- Then composition (§4.A), which by then can be judged against a real painted world rather than against noise.
Build-order steps 1 and 2 in Terrain.md — the editor viewport and the Generated edit layer — remain open,
remain first in that list, and are worth doing whatever happens here: a generator whose output cannot be seen
in the editor cannot be iterated on, and sculpting that does not survive a rerun makes the whole tool
one-shot. They matter more under this direction, not less, because an authored world is one somebody will
want to touch up by hand.
6. Do not redo these
Each was measured, not guessed.
critical_area_m2above 0, on its own. Re-measured after the uplift fix and it still fails: 1e4 sends the plains back to 7.0°, pins 49 % of the rolling class and 79 % of the mountains against the clamp, and collapses the slope-area fit to R² 0.001. The hillslope it creates has to shed its uplift by diffusion and at D 0.02 it cannot. It is not a tuning knob — it needs a transport law to pair with. See §4.D.3, where it comes back as a real candidate for a real reason.- Running the repose clamp only once at the end. A thousand steps of growth arrive together, it cuts deeply, and nothing runs afterwards to soften it. The grid facets came back in the summits.
fill_everyabove 1. The 50 was written to protect the time budget and it silently destroys the solve.- Raising the intraplate uplift rate to give the plains relief. That is the mistake this round undid. For
n = 1 the uplift rate alone fixes the hillslope angle;
Usets how high summits get, not how steep ground is. If plains need texture it comes from the detail passes or from lithology, never from U. - An elevation-gated talus ("below 150 m, don't dissect"). It would work immediately because the clamp binds everywhere, but elevation is the output of the solve, so gating a process on it is circular, it produces a visible shelf at the threshold, and it hides the cause.
- A percentile stretch for shore exposure, and casting fetch in every direction. Both measured, both
wrong, and both recorded in
Terrain.md: a percentile collapses on a coast that does not vary and is a global statistic two tiles would disagree about, and an all-directions fetch counts the land behind the shore as shelter, which made a straight open coast score as more sheltered than a bay. - One blur kernel for both the sediment and the carried per-shore values. The sediment balance needs a symmetric kernel, which means zero padding; a carried value needs edge clamping, or every shelf near the map border shrinks to nothing. There are two, and they share their arithmetic on purpose.
- Importing a painted heightmap as the terrain. See §3.2. The solve will erode it into something else and the drainage network — the reason the generator exists — is thrown away. Paint the uplift.
7. Traps
RawContent/World/World.jsonis still pre-D-48: 4081 vertices at 350 cm, elevation −460…2800, and it still carries the legacyerosionblock the tool warns about on every run. The Go defaults implement D-48 (7141 at 200 cm, −512…1536) and the manifest overrides them straight back. Migrating it is build-order step 8 and it changes every measured number in this doc, so either do it deliberately and re-baseline, or leave it alone.- Never run an authoring script while the editor has
L_Worldopen, and runcreate_world.pydetached, never under a tool timeout — a timeout killed one mid-import before. - The elevation ceiling is a hard clip in the 16-bit encoding, so a run reporting a clip fraction above
0.1 % is a failed run, not a rounded one.
U/Kis the one relief knob. Note this gets harder with painted uplift, where an author can ask for more relief than the range holds. - Determinism from the seed is cross-cutting rule 12 and it applies here completely. Anything random must
be a hash of (seed, position), never a stateful source: the value for a cell must not depend on how many
cells were visited first, which goroutine ran, or how many steps have passed.
TestDeterministicAcrossGOMAXPROCShashes the output at five values ofGOMAXPROCSand requires one hash. Under §3.3's rule 1 that becomes a hash of (seed, world position). - A test on a grid whose edge is an outlet must say which cells it is asking about. Two of the four new hillslope tests passed while measuring nothing: one read the fixed border cells back and called them the result, the other wrote its initial condition across the fixed border, which then re-injected it into the interior for ever.