Tooling
This commit is contained in:
+585
-143
@@ -36,6 +36,30 @@ Every `bs.*`-style knob in the manifest has a `--flag` override so an experiment
|
||||
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 |
|
||||
@@ -43,18 +67,31 @@ and for the coast `--no-coast`, `--outline-octaves`, `--outline-gain`, `--shelf-
|
||||
| `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_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 |
|
||||
| `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²:
|
||||
@@ -93,134 +130,237 @@ after — and across at least two seeds, or raise the bin count before leaning o
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
Full detail, including four things that were wrong first, is in `Terrain.md` under **What was built, and where
|
||||
it differs**.
|
||||
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 the map is good enough to work against. Do not spend the next
|
||||
session on them.
|
||||
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, then make the world author-driven and scalable.** Three
|
||||
things, in order.
|
||||
The goal is: **get generation working end to end at player scale.** Three things, in order.
|
||||
|
||||
### 3.1 Finish the pipeline (build-order step 6)
|
||||
### 3.1 Finish the planet's own passes
|
||||
|
||||
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.
|
||||
Two things the planet needed that the square canvas did not, and neither was optional for a bake to be judged.
|
||||
Both are closed:
|
||||
|
||||
### 3.2 Painted maps as the source
|
||||
- ~~**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.
|
||||
|
||||
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.
|
||||
And a third, smaller, which is both a cost and a correctness wart:
|
||||
|
||||
**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.
|
||||
- **`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.
|
||||
|
||||
Suggested channels, all optional, all falling back to the procedural field where absent:
|
||||
### 3.1a The painting has a tool now
|
||||
|
||||
| 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 |
|
||||
`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.
|
||||
|
||||
**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.
|
||||
**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 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.
|
||||
**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.
|
||||
|
||||
### 3.3 Scale, and why tiling is an architecture question
|
||||
**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.
|
||||
|
||||
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:
|
||||
### 3.1b The first template does not wrap, and that is the input rather than the tool
|
||||
|
||||
| World side | Area | Geology cells at 8 m | Fluvial solve, 1000 steps | Grid memory |
|
||||
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 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
**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:**
|
||||
|
||||
1. **Index every noise and every hash by absolute world coordinates, never by tile-local index.** Both the
|
||||
fBm lattices in `internal/noise` and 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.
|
||||
2. **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.
|
||||
|
||||
---
|
||||
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 — nothing exists at player scale · the blocker
|
||||
### C. Detail — built, and what it left behind
|
||||
|
||||
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.
|
||||
~~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:
|
||||
|
||||
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.
|
||||
- ~~**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
|
||||
|
||||
@@ -267,16 +407,20 @@ strip the surf works in, so the cliff began a hundred metres inland instead of a
|
||||
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.
|
||||
**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
|
||||
@@ -285,6 +429,12 @@ budget came back unplaced, because real exposed coasts do have beaches, they jus
|
||||
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
|
||||
@@ -302,8 +452,51 @@ at `--size 1400` (10.2 m cells). A finer grid resolves more of the threshold's w
|
||||
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
|
||||
@@ -311,23 +504,165 @@ keeps it from showing as hard facets. Levers, most principled first: raise `max_
|
||||
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.
|
||||
**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 %**.
|
||||
|
||||
**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.
|
||||
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
|
||||
|
||||
All three are one-or-two-constant changes. They are listed so they are not rediscovered, not because they are
|
||||
next.
|
||||
**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:
|
||||
@@ -356,7 +691,20 @@ property the percentile ramp exists to provide. Note also that the manifest key
|
||||
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
|
||||
**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
|
||||
@@ -364,7 +712,13 @@ influence stops dead; and `if r > convergent*1.6` flattens the strongest throws
|
||||
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.
|
||||
**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.
|
||||
@@ -373,20 +727,14 @@ bend and bifurcate.
|
||||
|
||||
## 5. Suggested order
|
||||
|
||||
1. **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.
|
||||
2. **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.
|
||||
3. **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.
|
||||
4. **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).
|
||||
5. **Then composition (§4.A)**, which by then can be judged against a real painted world rather than against
|
||||
noise.
|
||||
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
|
||||
@@ -394,8 +742,6 @@ in the editor cannot be iterated on, and sculpting that does not survive a rerun
|
||||
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.
|
||||
@@ -421,11 +767,107 @@ Each was measured, not guessed.
|
||||
- **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
|
||||
|
||||
Reference in New Issue
Block a user