890 lines
69 KiB
Markdown
890 lines
69 KiB
Markdown
# 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`](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`](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.
|
||
|
||
```bash
|
||
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`.
|
||
|
||
### The planet, which is the other half of the tool now
|
||
|
||
A painted template is a different command and a different manifest. `RawContent/World/Planet.json` points at
|
||
an image and a legend; `RawContent/World/Templates/README.md` is how to paint one.
|
||
|
||
```bash
|
||
# four seconds: read the painting, cut the planet into regions, solve nothing.
|
||
# map_class.png and map_regions.png are the two pictures that decide whether a bake is worth starting.
|
||
Tools/Terrain/bin/terrain.exe plan
|
||
|
||
# about two hours at 100 km round. Run it detached, never under a tool timeout.
|
||
Tools/Terrain/bin/terrain.exe bake --out RawContent/World/Bake --jobs 4
|
||
|
||
# one landmass, short, for tuning the legend's numbers
|
||
Tools/Terrain/bin/terrain.exe bake --only 11 --steps 200 --out /tmp/try
|
||
|
||
# the detail passes over a bake, a batch of tiles at a time. About twelve seconds a 5 km tile.
|
||
Tools/Terrain/bin/terrain.exe tiles --bake RawContent/World/Bake --only 11,7,13,8
|
||
```
|
||
|
||
**Every number in this section is pre-D-53 and has not been re-measured.** The router jitter moved from a
|
||
hash of the grid index to a hash of the world position, which changes the square canvas's output everywhere;
|
||
the summary block below was measured before it. Re-baseline before comparing anything against it.
|
||
|
||
### 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.** Fault traces are stroked over it in cyan - the line rather than the rate it contributes, because even after D-62 a fault is a few kilometres wide and the map is a hundred kilometres across | 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 - on a painted planet that is each class's `k_mult` times the planet's rock field, which the seed re-rolls (D-58) |
|
||
| `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 |
|
||
| `map_overlay.png` | The annotation layer over a dimmed class map, when the planet has one. A mark means nothing on its own and everything against the coastline it was drawn along |
|
||
| `overlay.json` | Every mark's area and piece count, and every feature in **world metres**: a centre, area, radius and extent per painted blob, an ordered polyline per path. What the engine places things from; nothing in the generator reads it back |
|
||
| `meta.json` | The full manifest as resolved, plus every statistic |
|
||
|
||
### How a run is judged
|
||
|
||
**The class table prints two angles now and the second one is the one to read (D-57).** `divide` is
|
||
`U/(K·A^m)` at a single cell, which is exact and is the *steepest* ground a rate can make; `typical` is the
|
||
median over the class, measured at a third of it in tangent and flat across a factor of twenty in rate. Almost
|
||
none of a map is divide. The old single column is why a legend could be set two or three times too hot and
|
||
still look reasonable on paper.
|
||
|
||
**A planet bake prints this block now (D-59).** Until then it printed its elevation range and nothing else,
|
||
which is why two questions this session - "are the lowlands hilly" and "do faults leave scarps" - had to be
|
||
answered by hand in Python off a PNG. A partial run (`bake --only`) marks itself PARTIAL: its extent is the
|
||
whole cylinder and its ground statistics are only the landmasses that were solved.
|
||
|
||
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
|
||
|
||
Painted planets (D-53). The source stops being a seed: an author paints a flat cylindrical world map, a JSON
|
||
legend beside it says what each colour means in uplift and erodibility, and the simulation makes the terrain.
|
||
X wraps and Y does not, so a landmass may straddle the seam and comes out whole. The geology is solved **one
|
||
landmass at a time**, which is exact rather than approximate because ocean cells are fixed at sea level for
|
||
the whole run and nothing in the solve can move them, so no flow path crosses open water; the coastal pass and
|
||
everything else run once over the whole cylinder, because the coast costs 26 ns a cell against 80 ns a cell
|
||
*per step* for the solve and cutting it up would truncate the fetch across every strait and split the sediment
|
||
budget. `terrain plan` reads the painting and cuts the planet into regions in four seconds, without eroding
|
||
anything; `terrain bake` solves it. The router jitter moved to a hash of the world position at the same time,
|
||
which is rule 1 of the tiling plan and re-baselines §1.
|
||
|
||
The detail passes came with it: 8 to 12 and 14 are built and tiled, so there is a full-resolution output for
|
||
the first time — 5 km tiles of 2500 samples at 2 m, twelve seconds each, with a margin **measured** at three
|
||
droplet lifetimes rather than reasoned at `rounds × lifetime`. The droplets had to become a pure function of
|
||
world position for that to close, which is rule 1 arriving where it was always headed.
|
||
|
||
Full detail, including the eight things the shape of the work revealed, 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 on a painted world two of the three are the author's job now.
|
||
Do not spend the next session on them. One item that looked like composition was not and is closed: a class
|
||
was one rate and therefore one landscape, which is §4.A0 and D-55.
|
||
|
||
The goal is: **get generation working end to end at player scale.** Three things, in order.
|
||
|
||
### 3.1 Finish the planet's own passes
|
||
|
||
Two things the planet needed that the square canvas did not, and neither was optional for a bake to be judged.
|
||
Both are closed:
|
||
|
||
- ~~**The coastal pass has to wrap.**~~ **Closed (D-60).** Four primitives wrap now, the abyss is a field so
|
||
the derived slope meets the painted ocean depth rather than stepping to it, and `Geometry.Ref` holds a
|
||
waterline slot so `supply` is a few hundred thousand entries instead of 608 MB. `Measure` holds one distance
|
||
transform at a time. Measured: 7.9 s over the whole 76 M cell cylinder, and the seam step in the sea floor
|
||
went from a mean of 9.1 m to 0.32 m, which is what an ordinary interior column is. The pass is now the
|
||
memory peak of a bake, about 8.3 GB against the solve's 3.6.
|
||
- ~~**`internal/stats` does not survive 28 M land cells.**~~ **Closed (D-59).** Fixed-bin histograms
|
||
replaced every sort and `field.LocalRelief` replaced the O(radius²) window, but the change that mattered was
|
||
not either of those: a histogram **adds**, so a planet's statistics are now *pooled* from its regions rather
|
||
than computed on a grid that never exists. Each region accumulates while its own grid is alive and they
|
||
merge in region order; the extent is measured once on the composited cylinder, because regions carry
|
||
overlapping ocean margins and pooling their cell counts would double-count the water between them.
|
||
Measured: 1.09 s for a 9 M cell region, about 120 ns a cell, so a whole planet is a few seconds at the end
|
||
of a two-hour bake. A bake prints the full block now, and a *partial* one says so rather than letting the
|
||
whole world's extent be compared with three islands' worth of ground.
|
||
|
||
And a third, smaller, which is both a cost and a correctness wart:
|
||
|
||
- **`DiffuseNonlinear` bounds its sub-step count with the steepest slope on the whole grid**
|
||
(`hillslope.go:62`). That is where the five-fold cost of mountains comes from and most of it is honest work
|
||
— the sub-steps buy stability, and truncating them checkerboards the surface a few hundred steps later. But
|
||
the bound is a *global statistic of the grid it is given*, so the steepest cell anywhere in a region sets
|
||
the diffusion for every plain in it, and two different decompositions of the same world would differ
|
||
slightly. It is the one place where the per-landmass split leaks into the answer, which is why the margin
|
||
and the minimum landmass size are in the manifest and in `meta.json`. A per-band bound would close it and
|
||
would be cheaper; whether it changes anything visible has not been measured.
|
||
|
||
### 3.1a The painting has a tool now
|
||
|
||
`terrain studio` (D-56) is the loop for everything in this section that is *authoring* rather than physics:
|
||
brushes that carry the legend's numbers, a seam-aware canvas, and `plan` as a button. It has two sheets now
|
||
(D-57): `classes` is the geology and `overlay` is the annotation layer, whose brushes are its marks and whose
|
||
`coast_jitter` marks are the only thing on it any pass reads. The two measurements
|
||
below - the 9.4 % seam disagreement and the JPEG halo - are both things it exists to stop happening again, and
|
||
the first is a thing it can fix by painting.
|
||
|
||
**Plan and Bake apply the panel before they run.** They always pushed both paintings first and never the
|
||
numbers beside them, and since both read the legend and the manifest off *disk*, an edit still sitting in the
|
||
rail was an edit the prepare never saw. A re-rolled seed was the case that showed it: the plan key did not
|
||
change, the cached prepare came back, and the uplift and erodibility maps were identical - which looks exactly
|
||
like a generator that ignores its seed. Both buttons now flush the planet block, the class legend and the
|
||
overlay legend, in that order, and the report says what it applied; a `unsaved:` note under the Plan bar says
|
||
what is pending before you press anything. Bake also pushes the paintings, which it never did: it solves the
|
||
server's copy, so a stroke made since the last plan was two hours of answering the wrong question.
|
||
|
||
**The region map is numbered.** Region hues came from a hash of the index, and independent hues collide - the
|
||
closest pair of the first twenty was 8.5 apart in RGB, which nobody can distinguish on a map whose whole job is
|
||
"is that one landmass or two". They walk by the golden angle now, with saturation and value on a 3 and 2 cycle:
|
||
44.0 at twenty regions, 41.9 at twenty-six, 37.7 at forty. Colour alone still cannot carry forty regions, so
|
||
`planet.RegionLabels` returns the centroid of each region's land and the studio writes the id over the map in
|
||
screen-space text - crisp at any zoom, and tiled across the seam like everything else. The mean across is
|
||
circular, because a landmass at `x = 0` and `x = W-1` has an arithmetic mean on the far side of the planet.
|
||
These are the ids `bake --only` takes.
|
||
|
||
**A finished bake lets go of the screen.** Its status and stamp outlive the run, so the first poll of every page
|
||
load re-opened the preview of a bake that had ended hours ago - over the painting, blocking the brush, and
|
||
reloading the page put it straight back. A load that finds nothing running now adopts the stamp instead of
|
||
drawing it; a bake that finishes while somebody watches still lands its final preview. Every map view also has a
|
||
visible way out now rather than only Escape, and the bake block has a Preview button to bring the last one back.
|
||
|
||
### 3.1b The first template does not wrap, and that is the input rather than the tool
|
||
|
||
Measured by `terrain plan` on `Map3.jpg`: the left and right edges, which are the same meridian, **disagree on
|
||
9.4 % of rows, 261 of them land against water**. The crater island crosses the seam perfectly — heights run
|
||
continuously from the last geology column into the first — but islets drawn touching `x = 0` have nothing to
|
||
meet them at `x = W-1`, so the world has a 400 m cliff down the seam wherever that happens. There is also a
|
||
two-pixel JPEG halo on the outermost columns which classifies as shelf, putting a 400 m ledge the height of
|
||
the map down the same line.
|
||
|
||
The tool is right and the painting is not, so nothing here is a defect to fix in code. What was added is the
|
||
measurement, because it is the one defect an author cannot see by looking at their own picture: the two edges
|
||
are as far apart on screen as they can be. The fix is to paint round the edge and export PNG.
|
||
|
||
### 3.2 The coastal detail, which is the last pass with nothing built
|
||
|
||
Passes 8 to 12 and 14 are built (see `Terrain.md`), so there is a full-resolution output: 5 km tiles of 2500
|
||
samples at 2 m, about twelve seconds each. What is missing at player scale is now only the *shore*, and it is
|
||
no longer blocked: `internal/coast` wraps (D-60), so there is a shelf, a shore platform and a beach for a
|
||
detail pass to refine. Section 4.E3 is still the shape of it - the surf reach is 110 m, which is
|
||
55 detail cells, enough for a real berm, a wave-cut notch and a scree apron below a cliff - and the tile bake
|
||
is where it goes.
|
||
|
||
Two smaller gaps in pass 14: the weightmaps are not derived (nothing imports them yet), and pass 13, the spawn
|
||
pad, is deliberately skipped because a planet has no single centre.
|
||
|
||
### 3.3 What is left of tiling
|
||
|
||
Both halves are built. The geology solve is decomposed per landmass and everything else runs whole (D-53);
|
||
the detail passes tile, with a margin **measured** rather than reasoned - three droplet lifetimes plus the
|
||
brush, which is 122 detail cells at the defaults, about five per cent of a 5 km tile on each side. Rule 1 is
|
||
done for the router, for the painted path and for every detail pass; the one place still on map-relative
|
||
coordinates is the square canvas's own `uplift.Build`, and that is deliberate, because there the noise *is*
|
||
the continent.
|
||
|
||
**How big can a world be, now.** The binding number is no longer the planet but its largest landmass, because
|
||
the solve is per landmass. Measured on the 100 km template: 18 regions, 49 M cells of 76 M, the largest 14 M
|
||
at under a gigabyte, and the detail another forty minutes for all 200 tiles, fully batchable. A 200 km world
|
||
with landmasses of the same *shape* is four times that; the case to watch is one landmass four times as wide,
|
||
because that single region is the peak.
|
||
|
||
**The wall time is set by the largest single region, and that region runs at about one core.** Measured on
|
||
the second bake of the 100 km template: seventeen regions finished in 11 586 s of solve with four in flight,
|
||
and the eighteenth - the 14 M cell central lowland - then ran alone for over 90 minutes at 1.0 to 1.1 cores.
|
||
That is not a defect, it is the shape of the solver: `Terrain.md` records that most of the runtime is the
|
||
stack walk and the priority-flood's cursor, and neither parallelises *within* one grid. Running regions
|
||
concurrently hides it while there are several left and hides nothing at the end.
|
||
|
||
Two consequences. The `--jobs` throughput number is not the wall time: a template whose land is one big
|
||
landmass gets almost no benefit from it. And **parallelising the stack update by basin** - which `Terrain.md`
|
||
already lists as option 2 for the time budget, and which is where the cores would actually go - has moved
|
||
from "a real gain, bounded" to the only thing that would shorten a bake of this shape. Disjoint basins are
|
||
independent; only the walk within one is sequential.
|
||
|
||
**And the cost per cell depends on the uplift rate, by a factor of eighteen.** Measured on the same bake at
|
||
1000 steps with four regions in flight:
|
||
|
||
| class | rate | cells | time | per million cells |
|
||
| --- | --- | --- | --- | --- |
|
||
| lowland | 0.08 mm/yr | 14.0 M | 1014 s | 72 s |
|
||
| highland | 0.90 mm/yr | 4.0 M | 1394 s | 350 s |
|
||
| crater | 1.60 mm/yr | 1.4 M | 1829 s | 1278 s |
|
||
|
||
It is not the stream power; it is the hillslope. `DiffuseNonlinear` sub-steps to stay stable, the count rises
|
||
with the steepest slope on the grid, and it saturates at `max_hillslope_substeps` — 24 by default. Steep
|
||
ground pays all 24 every step and a plain pays one. Three consequences: `terrain plan`'s estimate is
|
||
calibrated on the plains and is a **floor**; raising an `uplift_mm_yr` changes the bake time as well as the
|
||
terrain; and the wall time is set by the single slowest region, not the total, so one small steep landmass can
|
||
be the whole tail.
|
||
|
||
## 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 — built, and what it left behind
|
||
|
||
~~Nothing exists at player scale~~ — closed. Passes 8 to 12 and 14 are built and tiled; see `Terrain.md` for
|
||
what the port cost and the four things that were wrong on the way. What is left of this entry is three
|
||
narrower items, none of them a blocker:
|
||
|
||
- ~~**The shore is still a step.**~~ Closed (D-60): the coastal pass wraps, so a planet has a shelf, a
|
||
shore platform and a beach for the detail passes to refine. §4.E3 is now unblocked and is the next thing.
|
||
D-56's coast mask is a different thing and does not close this: it decides *where the waterline is*, at the
|
||
paint's own resolution, before anything is solved. `internal/coast` is what puts a shelf and a shore
|
||
platform under it, and that still has to wrap.
|
||
- **The weightmaps of pass 14 are not derived.** The rules are in the numpy and they are ported unchanged when
|
||
something imports them; flow, wear and deposit already come out per tile, which is what those rules read.
|
||
- **Nothing has been judged on the ground.** The tiles look right in a hillshade and the seam is measured, but
|
||
the question the whole pipeline exists to answer — does this read as ground to somebody standing on it — has
|
||
not been asked, because nothing imports a tile yet.
|
||
|
||
### A0b. What the lowlands are actually doing — measured, and mostly the author's numbers
|
||
|
||
Raised again as "the lowlands still by default become super hilly". Measured at last, on a 600² grid of 8 m
|
||
cells with the manifest's own constants at 1000 steps and a uniform rate:
|
||
|
||
| U mm/yr | divide | median | P90 | over 3° | max elevation |
|
||
| --- | --- | --- | --- | --- | --- | --- |
|
||
| 0.012 | 1.7° | 0.58° | 1.00° | 4 % | 32 m |
|
||
| 0.045 | 6.4° | 2.12° | 2.85° | 8 % | 38 m |
|
||
| 0.080 | 11.3° | 3.72° | 4.85° | 75 % | 54 m |
|
||
| 0.250 | 32.0° | 11.13° | 14.03° | 99 % | 143 m |
|
||
|
||
Three things follow and the first is the answer to the complaint.
|
||
|
||
**The massif floor already is a plain, and the real continent confirms it.** Region 12 baked whole — 45.9 ×
|
||
19.8 km, 9.0 M land cells, 1000 steps, 27 minutes — comes out **0..47 m** with a slope distribution of p50
|
||
0.61°, p90 1.22°, 4.4 % over three degrees and **nothing at all over eight**. The controlled run at a uniform
|
||
0.012 mm/yr gives 0.58° and 4 %, so the two agree. There is no missing process here and no fine dissection to
|
||
remove; D-55 did what it said.
|
||
|
||
**What made it look hilly was `preview.png`.** The hypsometric ramp's top is a *percentile of the world being
|
||
drawn*, so the whole ramp — green, tan, rock, snow — was stretched over this continent's 32 m, and its 40 m
|
||
hills came out with the white caps a 2800 m range would get. Redrawn against a fixed 400 m ceiling the same
|
||
heightmap is a flat green plain with four pale massifs on it. Closed: `palette.land_top_m` is an absolute
|
||
ceiling, and every run now prints which ceiling its preview was drawn against. The percentile stays the
|
||
default, because an absolute ramp over a world with no mountains is a green shape with nothing legible on it.
|
||
|
||
**What makes a painted lowland read as hill country is its massif share and the rate the massifs reach.** At
|
||
`fraction` 0.16 the ramp opens at the 76th percentile of the planet, so about a *quarter* of the class is off
|
||
the floor, and the class rate it climbs to — 0.08 mm/yr — is a 3.7° median, which is continuous rolling
|
||
ground. Both numbers are the author's. The lever for "more flat ground" is a smaller `fraction`; the lever
|
||
for "gentler hills where they are" is a lower `uplift_mm_yr`.
|
||
|
||
**And the number they were steering by was wrong by a factor of three**, which is D-57 and is closed: the
|
||
table printed the divide angle, which is the steepest place in a catchment, as if it were the landscape.
|
||
|
||
What is *not* closed, and is the real version of "lowlands should not consider mountainous erosion": every
|
||
cell of the world runs one process with one diffusivity, one critical slope and one channel threshold, and the
|
||
only per-class levers are `U` and `k_mult`. At a fixed cell those two set relief and steepness together
|
||
(§4.B0), so "flat but with real relief" is not expressible. The principled fix is the pairing in §4.D.3 — a
|
||
critical area with a hillslope diffusivity to match — and the thing that makes it newly plausible is that it
|
||
could be **per class**: §6 rejects it because D large enough to shed the uplift "smooths away every landform",
|
||
which is a fatal objection on a mountain and a *description of a plain*. Cost is the obstacle, not principle:
|
||
`DiffuseNonlinear` sub-steps on `D·dt/dx²`, so D 0.3 on a lowland region is 36 sub-steps against 3, and the
|
||
14 M cell region that already takes an hour and a half would take most of a day. Implicit diffusion, or a
|
||
per-band sub-step bound, is what would make it affordable. **Not started, and not to be started without
|
||
measuring the sub-step cost first.**
|
||
|
||
### 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 `D`
|
||
grows 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 — on the square canvas only.**
|
||
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.
|
||
|
||
**On a painted planet this is closed rather than deferred (D-53):** there is no `continentMask` and no map
|
||
border, because the outline is the paint and a region's edges are open ocean by construction. The cheapest fix
|
||
for the square canvas is still the same one — perturb the margin distance with a low-amplitude noise field so
|
||
the cut follows a crenellated line instead of a ruled one — and it now has one fewer consumer.
|
||
|
||
**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.
|
||
|
||
*First painted reading (D-60), and it is not yet the answer:* a partial planet bake came out 0.96 / 1.00 /
|
||
1.00, which is not a thinner contrast than the square canvas had but a flatter world - the run solved one
|
||
landmass, so most of the painted coast was still unsolved ground sitting at sea level with nothing behind it
|
||
to shelter anything. Exposure is measured on the waterline and the shape of the land behind the shore is half
|
||
of what sets it. Read this off a whole bake before touching the floor.
|
||
|
||
**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.
|
||
|
||
**E6. The shelf break is fixed, and the whole-planet measurement is owed (D-64).** The near-shore sea was
|
||
30 m deep everywhere because `BreakM` came from `pipeline.continent.sea_floor_m`, a square-canvas default; it
|
||
is `pipeline.coast.break_m` now and a planet gets 130 m. The mechanism is measured at unit scale — a painted
|
||
512 m sea goes from 33 % to 8 % shallower than 50 m — but **the re-bake that would give the planet numbers
|
||
was killed by memory pressure before it wrote anything.** What to run, and the numbers to put beside
|
||
`Bake_020`'s:
|
||
|
||
```bash
|
||
Tools/Terrain/bin/terrain.exe bake --out RawContent/World/Bake_D64 --jobs 4 # detached; ~13 min, peaks near 8.3 GB
|
||
```
|
||
|
||
Read back the depth histogram of `planet_height_low.png` against `Bake_020`'s, which was `-520 m 11.7 %`,
|
||
`-20 m 26.8 %`, `0 m 23.6 %`. What should have changed: the −20 m spike disperses across 0…−130 m, the share
|
||
deeper than 400 m rises towards the 55.9 % the legend paints, and `preview.png` gains a bathymetric gradient
|
||
where it had one flat mid-blue halo — the preview ramps `sea_shallow`→`sea_deep` linearly over the deepest
|
||
sea, so 20 m out of 520 was the first colour and nothing else. `--jobs 2` if memory is tight.
|
||
|
||
**E7. `elevation_m` is three times wider than any world that has been baked, and it is the author's key.**
|
||
−1024…2048 against data of −521…+340: the planet uses 28 % of its 16-bit ramp and its land 7 %, which is most
|
||
of why the exported heightmap reads as a flat grey picture with no coastline in it. A bake prints this now.
|
||
It is not a defect — headroom is a legitimate choice and D-64 deliberately did not touch `Planet.json` — but
|
||
about −576…320 is 3.4× the contrast and 3.4× the vertical resolution for the compositions built so far. **The
|
||
trap if it is changed:** `tiles` decodes `planet_height.png` through the *current* manifest, so a bake made
|
||
under one range and tiled under another is silently wrong by the difference. Re-bake, or do not change it.
|
||
|
||
### B. Texture — wrong at mid scale
|
||
|
||
**B0. The clamp ceiling has a number now, and a painted legend walks straight into it.** Steady state is
|
||
`S = U/(K·A^m)` applied down to a single cell, so at a divide `A` is one cell squared and `A^m` is the cell
|
||
size. Setting that equal to the angle of repose gives the rate above which the clamp does all the shaping:
|
||
|
||
> `U_max = tan(talus) · K · cell` — at 35°, K 5e-5 and an 8 m cell, **0.280 mm/yr**.
|
||
|
||
The first painted legend put `highland` at 0.9 mm/yr, which is **66° at a divide**, 3.2 times over. Baked, the
|
||
geology comes out as flat polygonal faces with hard 45- and 90-degree edges - the D8 clamp, visible at a
|
||
glance once the detail passes are stripped off with `terrain tiles --no-detail`. Not 33 % of the class shaped
|
||
by the clamp: all of it.
|
||
|
||
`terrain plan` now prints the implied divide angle for every land class and says which are clamped, which is
|
||
four seconds against an hour and a half. The deeper point is the one §4.D.3 is about: **at a fixed cell,
|
||
relief and steepness are the same knob.** `U/K` sets both, so there is no setting that gives a 700 m range
|
||
with hillslopes below repose - 0.28 mm/yr on a 20 km island is about 350 m. Getting more relief than that out
|
||
of erosion-shaped ground needs the channelization threshold to work, which is exactly the open problem in
|
||
§4.D.3 and §6.
|
||
|
||
**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~~ - built (D-65), and it turned out to be B3's
|
||
cause rather than a refinement of the plains.** This entry read "some basin boundaries on the plains in
|
||
`map_basins.png` are visibly straight" and treated MFD as tidying. It is not: on a planar hillslope the
|
||
correct specific catchment area is the same at every point along a contour, and D8 cannot say so - every
|
||
cell picks the same steepest neighbour, the flow lines run exactly parallel and never converge, and a cell
|
||
either sits on a line and carries the whole tube or sits off one and carries a single cell for ever.
|
||
Measured on a ramp at an aspect of 22.5 degrees with no erosion at all, one fill and one accumulate
|
||
(`internal/fluvial/flow_test.go`): the most-drained cell in a contour band carried **769 times the median**
|
||
and **29.5 % of the grid drained nothing**. At an MFD exponent of one the same numbers are **1.34** and
|
||
**0.4 %**.
|
||
|
||
Built as planned - Freeman/Quinn/Holmgren partition for `Accumulate` only, D8 receivers kept for the
|
||
implicit update, because Braun-Willett walks one receiver chain and has no unconditionally stable
|
||
multi-receiver form. Two things this entry had wrong. **The processing order is not descending elevation**:
|
||
Kahn's algorithm over the flow graph is exact, O(n), and needs no elevation comparison at all - count each
|
||
cell's strictly higher neighbours, release on zero. A bucket sort by elevation would have been worse than
|
||
useless, because the queue quantises to a centimetre while the flood's epsilon ladder across a filled flat is
|
||
a millimetre a cell, so ten cells of one descending chain share a bucket and every lake bed would leak its
|
||
area. And **float32 is enough**: a cell's accumulator takes at most eight contributions, each already an
|
||
aggregate, so the drift is a random walk over the flow path and measures 2.4e-9 relative over a closed basin.
|
||
|
||
**The bake-scale verification is owed.** Everything above is measured on the router in isolation and on the
|
||
square canvas. The comparison that matters - region 8 of `Planet.json`, the streaked left continent, the same
|
||
painting and seed - has a D8 baseline in `RawContent/World/R8_d8` (15m40s, 922 s of solve, land relief 195 m,
|
||
slope-area exponent -0.509 at R² 0.966, drainage density 0.43 /km, and 31 sources to 13 confluences in the
|
||
12.8 km window at 1391,2625) and **no MFD twin**: that run was killed by memory pressure about 70 % through
|
||
and wrote nothing. Until it exists, what is established is the mechanism, not the cure - and one result
|
||
argues for caution rather than optimism, which is that after three hundred steps of solving a planar ramp the
|
||
leaf fraction converges (D8 8.2 %, MFD 8.9 %), because a dissected landscape's own divides dominate that
|
||
count. Run:
|
||
|
||
```bash
|
||
Tools/Terrain/bin/terrain.exe bake --only 8 --steps 1000 --jobs 1 --out RawContent/World/R8_all # detached, ~20 min
|
||
```
|
||
|
||
and read the hillshade of the same window against `R8_d8`'s first, the sources-per-confluence second, and the
|
||
slope-area exponent third - it should move *towards* -0.5, which is the number that says the fix is physics
|
||
rather than a smoother.
|
||
|
||
The cost is real and it is the argument against, if there is one: **101 ns a cell against D8's 17**, measured
|
||
on a 1024² ramp, and the walk is serial where `ComputeReceivers` and the hillslope law are not, so it lands on
|
||
wall clock rather than on cores. `pipeline.fluvial.mfd_exponent` is 1 by default and 0 is the old behaviour,
|
||
so the A/B is one flag. **`secondsPerCellStep` in `internal/planet/planet.go` is still the D8 number**, so
|
||
`terrain plan`'s estimate now reads low; recalibrate it from the first full MFD bake rather than guessing, and
|
||
note that its comment already says it is a floor.
|
||
|
||
**B3. ~~A ribbed, combed texture on the range flanks~~ - the diagnosis in this entry was wrong, and the
|
||
cause is B2 (D-65).** It read "the ridged-noise initial relief showing through where the solve has not had
|
||
time to overwrite it", and the reasoning that ruled out the alternative does not hold: it dismissed grid
|
||
locking because "the ribs are oblique, not axis-aligned", but D8's parallel-flow grooves run in whatever
|
||
direction the slope faces, so obliqueness is the expected appearance and not a counter-indication.
|
||
|
||
What the grooves are, measured on `Bake_x4` and `Bake_020` - the same painting and seed at 32 m and at 8 m,
|
||
both 12500 x 6076, so the same window in cells compares directly:
|
||
|
||
- they are in `map_flow.png` as parallel high-accumulation lines, so they are **channels**, not surface
|
||
texture, and they are absent from `map_uplift.png`;
|
||
- the network re-derived from `planet_height.png` is **pinnate** - ruler-straight parallel trunks with short
|
||
barbs joining at a near-constant angle - not dendritic. In a 12.8 km window at a 1 km² channel threshold:
|
||
25 sources and **0 confluences** at x4, 30 and 11 at 100 km, against about one source per confluence for a
|
||
dendritic network; the largest catchment in a 164 km² window is 4.07 km²;
|
||
- the D8 receiver histogram over that window is anisotropic: 17.3 % on one diagonal, 14.3 % on its opposite,
|
||
the other six 10.7-12.4 %, against 12.5 % uniform;
|
||
- and the pitch is **the same 18 cells in both bakes**. That is the decisive one. Every physical candidate -
|
||
the ridged fBm this entry blamed (250-300 m), the massif fabric, the fault grain - is fixed in *metres* and
|
||
would change its pitch in cells by four. Only a grid-scale mechanism survives it.
|
||
|
||
The spacing is set by the ±0.05 % tie-break jitter at `fluvial.go`, which is a **static** field - the same
|
||
hash at step 1 and at step 1000 - so the rare merges it allows are re-carved a thousand times instead of
|
||
averaged out. The ridged fBm may still contribute; it cannot make grooves that are strictly downslope,
|
||
strictly parallel, and visible in the flow map.
|
||
|
||
**B4. `ClampToRepose` left grid-aligned facets, and `bucketPQ` broke ties in raster order** - fixed (D-65), and the fix is worth less than it looks.
|
||
|
||
*The entry as written, which is still the right description of the mechanism:* Checked
|
||
while chasing B3 and **ruled out as its cause** - B3's ribs are oblique - but real and worth not
|
||
re-deriving. `internal/fluvial/repose.go` pushes every cell in flat-index order and lowers neighbours in
|
||
place, so which neighbour gets cut is decided by pop order; `bucketpq.go` pops LIFO within a 1 cm bucket,
|
||
so ground flat to within a centimetre propagates consistently along −X within a row. `hillslope.go`'s own
|
||
comment admits the signature: "pyramids with faces aligned to the grid - the blocky, ruler-cut facets". The
|
||
designed mitigation is `DiffuseNonlinear` running after it, which bails entirely at `diffusion_m2_yr` 0.
|
||
Visible today only as a fine chevron texture inside B3's ribs. The related hazard - `bucketPQ` collapsing
|
||
everything above `SetElevationRange`'s ceiling into one bucket processed in strict reverse row-major - is
|
||
**not** firing: `ClipFrac` is 0 in every region of every bake measured.
|
||
|
||
*What the fix did, and what it did not.* The clamp now jitters both its pop order and its allowance with the
|
||
same world-keyed hash `ComputeReceivers` uses, through a `pushJittered` that scatters a cell over sixteen
|
||
buckets rather than one. Half a bucket was tried first and is not enough - it splits a tie across two buckets
|
||
and halves the correlation instead of removing it. Sixteen is safe for a reason worth keeping: the clamp's
|
||
order can only matter between two cells whose heights differ by about the talus allowance, which is *metres*,
|
||
so reordering cells that are centimetres apart cannot break a constraint that only bites metres apart. The
|
||
bound is `talus*cell/2`, 2.8 m at 35 degrees on an 8 m cell. Measured: the pop order's rank correlation with
|
||
the flat index went from **-1.000 to -0.025**.
|
||
|
||
But the isotropy test built for it (`TestClampToReposeIsIsotropic`) reads **identical** with the jitter, without
|
||
it, and with either half alone - 0.97 % four-fold and 2.39 % eight-fold on a clamped cone. On a cone no two
|
||
cells share a bucket, because the surface falls twenty metres a cell against a one-centimetre bucket, so the
|
||
ordering bias has nothing to bite on. The residual octagon is geometry, not order: a path to a point at 22.5
|
||
degrees is built of cardinal and diagonal steps and the octile distance it accumulates exceeds the straight
|
||
line by up to 8 %, so an eight-connected clamp cuts an octagon out of a cone whatever order it works in. That
|
||
is irreducible without a wider neighbourhood. **And the clamp is a small actor anyway** - `near_talus_fraction`
|
||
is 0.32 % of mountain cells - so this was housekeeping, not the fix.
|
||
|
||
**B5. The hillslope smoother transported across four faces while the clamp cut across eight** - fixed (D-65).
|
||
`Run`'s design is that the clamp cuts and `DiffuseNonlinear` rounds off what it cut before the next step sees
|
||
it, and a five-point stencil cannot transport across a diagonal face at all, so a diagonally-cut facet was
|
||
left standing by construction. That was a hole in the stated design rather than a refinement of it. The
|
||
stencil is nine-point now, weights 4/6 cardinal and 1/6 diagonal - the isotropic nine-point Laplacian, which
|
||
on `h = (a/2)(x²+y²)` gives `(1/6)(8ad² + 4ad²) = 2ad²`, exactly what the five-point gave, so `coeff` is
|
||
unchanged. A diagonal face carries its own critical height difference, `sc*dx*sqrt(2)`, or every diagonal
|
||
would read as 1.41 times its true S/Sc. Stability improves and pays for the extra faces: the checkerboard
|
||
amplification goes from `1 - 8*coeff` to `1 - 5.333*coeff`, so the limit moves 0.25 -> 0.375 and the sub-step
|
||
target moves 0.2 -> 0.3 at the same 1.25x margin. Making either change without the other is a scheme that
|
||
checkerboards a few hundred steps in, which is why they are one commit and why the stability test now runs
|
||
2000 steps rather than 500.
|
||
|
||
**B6. There is an edge-preserving smooth now, and it is off.** `field.SmoothEdgePreserving`, ported from the
|
||
World Orogen browser generator, which has one for exactly this reason - to blend its own routing artefacts
|
||
without rounding the landforms off with them. `w = 1/(1 + |dh|/(d*slopeRef))` over eight neighbours, land
|
||
only, waterline locked, run once after the solve and never inside the step loop: it conserves nothing and has
|
||
no time in it, so per-step it would act as an uncontrolled extra diffusivity, and that moves the steady-state
|
||
slope, which is `U/K`, which is the one knob the generator's relief hangs on. The deviation from the
|
||
reference is units: a sensitivity in 1/m is a height threshold and means something four times as aggressive on
|
||
an 8 m cell as on a 32 m one, which is exactly what section 4.D says the generator lives or dies by, so it is
|
||
a slope. Measured on a synthetic: 77 % of a 4 m ripple removed, 98 % of a 300 m cliff kept. `pipeline.smooth.passes`
|
||
is **0** by default - turning it on is a decision to hide something rather than fix it, so it is a decision
|
||
somebody makes in a file - and a run with it on has to match a run with it off on the slope-area exponent,
|
||
the drainage density and the per-class median slopes, or it is shaping terrain rather than polishing it.
|
||
|
||
**And measured against those gates it fails, which is the point of having them.** On the square canvas at
|
||
`--size 500`, 300 steps: two passes at `slope_ref` 0.3 take the slope-area exponent from **-1.02 to +0.36**
|
||
with the fit collapsing from R² 0.92 to 0.36, the channel cells from 772 to 566, and the mountain class's
|
||
median slope from 15.3° to 11.6°. Backing off does not rescue it - one pass at 0.02, which is a one-degree
|
||
reference, still lands at **+0.16** and 14.4°. Drainage density is the one thing that does not move (0.43 /km
|
||
throughout). So this is not a free polish at any setting: it is a filter, it changes the slope-area relation
|
||
the solve exists to produce, and what it is for is somebody deciding in a file that they want the look more
|
||
than they want the statistic. It is not a substitute for B2, and it was not turned on to get B2's result.
|
||
|
||
### A. Composition — parked, but recorded
|
||
|
||
**A0. ~~One class is one landscape~~ — closed (D-55), and it was not a tuning item at all.** This entry used
|
||
to be absent and the defect it names is the one a person spotted by looking at the bake: every landmass came
|
||
out *uniformly* dissected, coast to summit, with no flat ground on any of them. `n` is 1, a class was one rate,
|
||
and D-49 says the rate alone fixes the hillslope angle — so one painted colour was one landscape, at whatever
|
||
angle its rate named, over every cell of it. A class now carries `massif: {floor_mm_yr, fraction}` and cuts one
|
||
planet-wide upland fabric, so a painted lowland is a plain with hill masses standing out of it. See `Terrain.md`
|
||
for the threshold problem, which is the part with a wrong answer available: a percentile of the region would
|
||
have made two regions disagree along every boundary.
|
||
|
||
Two numbers from it worth keeping here. **The fabric wavelength has to sit well below a landmass** — 12.5 km
|
||
against islands of 20–45 km put one island entirely above the cut, which is this same defect one size down; 7 km
|
||
is what the current template uses. And **`internal/stats`' "plain below 0.1 mm/yr" is a reporting bucket, not a
|
||
description of terrain**: 0.1 mm/yr is a fourteen-degree hillslope, and reading that line as guidance is how the
|
||
legend's plains were set ten times too hot. The buckets are unchanged — they are an axis with a run of measured
|
||
numbers behind them — but `terrain plan` now prints what a class reads as from its *angle*, which is the number
|
||
an author is really choosing.
|
||
|
||
The three below are one-or-two-constant changes on the *procedural* path. 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:
|
||
|
||
```go
|
||
// 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.
|
||
|
||
**A2a. ~~A fault is a welt with a cliff down the middle~~ - closed (D-62).** The cross-fault profile put
|
||
the whole throw either side of the trace one cell apart (89 degrees) inside a 600 m flank - narrower than the
|
||
1.1 km hillslope the drainage density implies, so nothing could dissect it and the uplift profile printed
|
||
onto the surface as a smooth ruled ridge. Continuous, kilometres wide and bell-tapered along strike now; see
|
||
Terrain.md. The general lesson is in section 6.
|
||
|
||
**A2. ~~Fault traces are drawn curves with stamped ends~~ - closed on the painted path (D-58), still open on
|
||
the procedural one.** The painted implementation is a separate file written against this list rather than a
|
||
port of the code below: a walked heading-perturbed trace, a throw tapered over the last sixth at each tip,
|
||
en-echelon segments past twelve kilometres, and no clamp to a fraction of a global rate. The procedural
|
||
`buildFaults` is untouched and still has all four. What follows is that list, kept because it is what the new
|
||
one was written against.
|
||
|
||
*The original entry, against the procedural `buildFaults`:* 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~~ - closed on the painted path (D-58).** The painted
|
||
fault set takes its strike from a grain field sampled as a vector through `atan2`, so traces are sub-parallel
|
||
within a province and the set swings across the world. Sampled as an *angle* it would have been worse than one
|
||
global angle: a value lattice runs 0..1 and jumps a whole turn along its own wrap. The procedural path below
|
||
is unchanged.
|
||
|
||
*The original entry, against the procedural path:* 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
|
||
|
||
1. ~~**Make `internal/coast` wrap.**~~ Done (D-60). The mass-balance tests still pass unchanged on a flat
|
||
grid and have cylinder twins.
|
||
2. ~~**Fix `internal/stats` for planet scale.**~~ Done (D-59). A bake prints the whole block, pooled from
|
||
its regions.
|
||
3. **Then the coastal detail (§3.2, §4.E3)**, which by then has a shelf and a shore platform to refine and is
|
||
where a player will stand first.
|
||
4. **Then composition (§4.A)**, which by then can be judged against a real painted world rather than against
|
||
noise — and on a painted world two thirds of it is the author's job, not the generator's.
|
||
|
||
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_m2` above 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_every` above 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; `U` sets 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.
|
||
- **Giving each region enough margin to run the coastal pass inside it.** It needs `shelf_km` plus `slope_km`,
|
||
4.6 km, which nearly doubles every region; and it buys nothing while costing the fetch across every strait,
|
||
the sediment budget's conservation proof, an ocean-ownership rule where two margins overlap, and pooled
|
||
coastal statistics. The pass is 26 ns a cell. Run it once on the cylinder.
|
||
- **Clustering landmasses by overlapping dilated bounding boxes.** Box overlap is transitively closed and one
|
||
long thin landmass has an enormous box; on the first real template — where one landmass is 70 km of a 100 km
|
||
circumference — it collapses the planet into a single region. Dilate the mask itself with the distance
|
||
transform and connected-component the result, which groups exactly those landmasses within a margin of each
|
||
other and is the same code the stroke fill already needs.
|
||
- **Hanging a world origin on `field.Field`.** A `Field` is used for masks, coordinate pairs and scratch,
|
||
`field.New` has no origin to give them, and every one of them would quietly claim to sit at (0, 0). A
|
||
wrong-by-default origin cannot be seen; a missing argument is a compile error.
|
||
- **Taking the massif threshold as a percentile of the region.** It is the obvious implementation and it is
|
||
the one thing D-53's decomposition forbids: `uplift.Build`'s percentile band is a global operation over the
|
||
grid it is given, and two regions taking quantiles of their own extents put the same physical hillside on
|
||
different sides of the cut, so the planet disagrees with itself along every region boundary. The threshold is
|
||
a quantile of the **planet**, from a fixed probe, identical in every region by construction.
|
||
`TestTwoFramesAgreeAboutTheSameGround` is the negative control.
|
||
- **A massif fabric per class.** One field for the whole planet, cut at a different level by each class. A
|
||
fabric per class makes a highland belt and the hills in the lowland beside it two unrelated noises meeting at
|
||
a painted edge, when what they should be is the high and low parts of one structure.
|
||
- **A massif wavelength near the size of a landmass.** Measured: 12.5 km against islands of 20–45 km put one
|
||
whole island above the cut, which is the defect the fabric exists to fix, one size down. Several blocks per
|
||
continent, so well under a landmass.
|
||
- **Reading a hypsometric preview as a statement about scale.** The ramp is normalised to the world in
|
||
front of it, and it has to be - absolute over a world with no mountains is a green shape with nothing
|
||
legible on it. `palette.land_top_m` is there for when absolute is what you want, and the run summary
|
||
names the ceiling either way. The judgement of steepness belongs to `map_slope.png` and the plan's
|
||
`typical` column.
|
||
- **Porting `uplift.Build`'s lithology or faults as they stand.** The first ends in `f.Percentile()` of the
|
||
grid it is handed and the second places trace centres at `Float()` pairs read as fractions of it, so on a
|
||
decomposed planet both give a different answer in every region. D-58 rewrote both against world coordinates
|
||
and a planet quantile; `TestTwoFramesAgreeAboutTheSameRock` and `TestTwoFramesAgreeAboutTheSameFaults` are
|
||
the negative controls.
|
||
- **Blurring a rock boundary to soften it.** A blur is a neighbourhood operation and one near a region's edge
|
||
reads cells another decomposition would not have given it. The softening is pointwise, in rank space, which
|
||
needs only the cell's own value and grades the boundary by the fabric's own gradient.
|
||
- **Cutting a fault's influence off where the exponential is still worth something.** At three gentle lengths
|
||
it is 5 % of peak, which on a 400 m throw is a fifth of a lowland's whole uplift rate - a step at a line
|
||
six kilometres out that the solve carves into a straight scarp. Superseded by D-62, which drops the
|
||
exponential for an envelope that reaches zero *with zero gradient* at its own width - the cut-off is then
|
||
the support of the function rather than a truncation of it, and there is nothing to renormalise. The
|
||
lesson is the general one and it is why the old profile's near end was worse than its far end: **any step
|
||
left in the uplift field, anywhere, is a straight scarp the solve cannot undo.**
|
||
- **Reading the divide angle as the landscape.** It is exact and it is the steepest ground a rate can make,
|
||
because `A` is smallest at the top of a catchment; the median is a third of it in tangent and almost none of
|
||
a map is divide. Both print now (D-57). This is the same class of mistake as reading `internal/stats`'
|
||
"plain below 0.1 mm/yr" as terrain, and it cost the same thing: every rate in the first legend set two or
|
||
three times too hot.
|
||
- **A reserved background colour for the overlay's blank.** Alpha already says it, every editor produces it,
|
||
and a colour would be spent on nothing and lost the moment somebody exported with a white matte.
|
||
- **Snapping an unmatched overlay pixel to its nearest mark.** That is right on a class template, where every
|
||
pixel must become something, and wrong here, where most of the sheet is nothing: it turns a JPEG halo round
|
||
a road into road. Drop it and count it.
|
||
- **One mask image per overlay mark.** Marks cannot overlap - one painting, one colour a pixel - so an 8-bit
|
||
index raster holds 254 of them in the space one boolean mask would take.
|
||
- **Treating an unmarked cell as `coast_jitter` 1.** A mark lands on whichever side of the waterline the
|
||
author's hand was on, so an uninstructed cell has to take its instruction from the far side or a stroke on
|
||
the land is overruled by the water beside it.
|
||
- **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
|
||
|
||
- **`map_flow.png` at planet scale is aliased, and it looks exactly like broken drainage.** It point-samples
|
||
every fourth cell (3000 px on a 12500 grid), so a one-cell channel survives about a quarter of the time and
|
||
the network reads as disconnected yellow stubs, while the divides - which are broad - come through whole.
|
||
Bake_001 shows the identical pattern, so it is the diagnostic and not the terrain. Judge the network from
|
||
the preview's drawn rivers, or crop the map at 1:1.
|
||
- **Max elevation cannot see a change to the *distribution* of uplift.** D-55 dropped the lowland continent
|
||
from 71 m to 41 m while leaving the rate its massifs reach untouched, because relief is the integral of
|
||
slope along the whole flow path: a trunk crossing a plain climbs where the massifs are and nowhere else.
|
||
The statistic to read is the slope distribution, which `internal/stats` still cannot produce at planet
|
||
scale - it had to be taken off the heightmap by hand for that measurement.
|
||
- **Rendering a diagnostic map smaller does not make `plan` quicker.** Measured: prepare is flat at about
|
||
6.5 s from a 400 px map to a 2400 px one, because the cost is `region.Build` at **2.68 s** over the 76 M
|
||
cell planet grid - classify 0.09, dissolve strokes 0.93, despeckle 1.52, the coast mask 1.62, project 0.05.
|
||
Neither the size of the painting nor the size of the maps touches the big one. What does work is caching:
|
||
the studio reuses the whole prepare when only the legend's *numbers* changed, 7.0 s to 0.37 s.
|
||
- **`terrain plan`'s `divide` column is not the ground.** See §6. Read `typical`.
|
||
- **`preview.png`'s tint is relative and says nothing about scale.** The ramp's top is a percentile of
|
||
the world being drawn, so a 47 m lowland continent whose median slope is 0.61° comes out with the bare
|
||
rock and white caps of an alpine massif. That is where "the lowlands are hilly" came from, twice. Every
|
||
run prints the ceiling it used; read that line, or read `map_slope.png`, or set `palette.land_top_m`.
|
||
- **A manifest key with a default is not a feature.** `coast_jitter_px` was declared, documented, defaulted
|
||
and *never read by anything* from D-53 until D-56, and the only reason it was found is that somebody asked
|
||
why the coastlines looked drawn. A grep for a key's own name is three seconds and it is worth doing before
|
||
tuning one.
|
||
- **A planet bake is not a rerun-while-you-judge loop.** It is about two hours at 100 km round. `terrain plan`
|
||
is four seconds and settles the two decisions that can waste those two hours — how the legend read the
|
||
painting, and how the planet was cut up. `--only <id> --steps 200` is the loop for tuning the legend's
|
||
numbers; the elevation range is confirmed from that measurement, not guessed before it.
|
||
- **The seed alone no longer names a painted world.** The ocean margin and the minimum landmass size decide
|
||
how the planet is cut into regions, and the priority-flood's epsilon ladder across a flat depends on the box
|
||
it is flooding. All three are in `Planet.json` and all three are recorded in `meta.json`; a change to any of
|
||
them is a change to the world.
|
||
- **A noise period that does not divide the circumference breaks every noise field at the seam.**
|
||
`noise.Lattice.Sample` wraps modulo its cell count and `WorldUV` divides world metres by the period, so `u`
|
||
returns to the same lattice point at `x = W` only when the circumference is a whole number of periods.
|
||
`world.Planet.Validate` refuses anything else, and `TestNoiseBreaksWhenThePeriodDoesNotDivide` is the
|
||
negative control that keeps the positive test honest.
|
||
- **`RawContent/World/World.json` is still pre-D-48**: 4081 vertices at 350 cm, elevation −460…2800, and it
|
||
still carries the legacy `erosion` block 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_World` open**, and run `create_world.py` detached,
|
||
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/K` is 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.
|
||
`TestDeterministicAcrossGOMAXPROCS` hashes the output at five values of `GOMAXPROCS` and 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.
|