448 lines
32 KiB
Markdown
448 lines
32 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`.
|
||
|
||
### What a run writes
|
||
|
||
| File | What it is for |
|
||
| --- | --- |
|
||
| `preview.png` | Hypsometric tint, hillshade, rivers. "Does this look like a landscape" |
|
||
| `preview_detail.png` | A crop at 2× vertical exaggeration. The whole continent at 1600 px cannot show whether lowlands read as hill country or as small mountains; this can. Move it with `--crop-x/-y/-size`. It cannot be rendered finer than the grid: a crop of 0.14 at `--size 1400` is 196 cells, so that is the image, whatever `Size` asks for |
|
||
| `geology_height.png` | The 16-bit heightmap itself, encoded to the manifest's elevation range |
|
||
| `map_uplift.png` | **The most useful diagnostic.** Rock uplift in mm/yr — the field everything else is a consequence of. It and `map_slope` should be recognisably the same picture; when they are not, something downstream is overriding the tectonics |
|
||
| `map_slope.png` | Degrees, 0–45 |
|
||
| `map_relief.png` | Local relief over 500 m. Separates a 5 m hummock from a 500 m mountainside — both stand at 30° and the slope map cannot tell them apart |
|
||
| `map_erodibility.png` | The lithology multiplier on K. Where texture inside a range comes from |
|
||
| `map_exposure.png` | How open the water is in front of each stretch of shore, 0 sheltered to 1 open. Drawn only within a kilometre of the waterline, because past that it is a map of the continent's medial axis. The one to read when a beach turns up on a headland |
|
||
| `map_coast.png` | Everything the coastal pass moved, in metres: cool where the surf cut, warm where the sediment landed. The sea floor is excluded, or its few hundred metres would swamp the few the processes move |
|
||
| `map_flow.png`, `geology_flow.png` | Log drainage area: the rivers |
|
||
| `map_basins.png` | One colour per drainage basin, hashed from the basin root. The direct test of whether the solve made a *network* rather than scratches: basins must tile the land, sizes must span orders of magnitude, and divides must sit on the ridge crests. Confetti means the router is re-deciding where water goes every few cells |
|
||
| `meta.json` | The full manifest as resolved, plus every statistic |
|
||
|
||
### How a run is judged
|
||
|
||
The printed summary is the verdict, and the block that matters most is the per-uplift-class breakdown —
|
||
map-wide medians cannot answer "are the plains plains", which is precisely how the last problem stayed
|
||
invisible. Current state, seed 7 at 1400²:
|
||
|
||
```
|
||
slopes: 54% under 15 deg, 77% under 30, 0.3% over 50, median 11.2 deg
|
||
slope-area: exponent -0.384 (expect -0.500), R2 0.225 over 6 bins
|
||
hypsometric integral 0.107 (concave: over-eroded); drainage density 0.48 /km
|
||
by uplift class:
|
||
plain 0.00..0.10 mm/yr 42% of land slope 0.8 deg median, 2.8 P90 relief 9 m/490 m at talus 1%
|
||
rolling 0.10..0.50 mm/yr 7% of land slope 7.5 deg median, 23.1 P90 relief 85 m/490 m at talus 1%
|
||
mountain 0.50.. up mm/yr 51% of land slope 28.5 deg median, 36.0 P90 relief 195 m/490 m at talus 33%
|
||
```
|
||
|
||
The coast prints its own block after it, and the numbers to read first are the sediment budget — it is the one
|
||
part of the pass not derived from something already measured — and the exposure percentiles, which say whether
|
||
the shoreline has any bays for the shelter to work with:
|
||
|
||
```
|
||
coast: 95 km of shoreline, 37% sea, shelf 86% of it; surf planed 2.8 km2 and cut 21.23 Mm3,
|
||
16 river mouths delivered 3.87 Mm3, 25.04 Mm3 laid (0% unplaced) as 2.92 km2 of new beach;
|
||
mean cliff 3 m, 0.00 km2 drowned; shore exposure 0.00 / 0.90 / 1.00 (p10/p50/p90)
|
||
```
|
||
|
||
An unplaced fraction above a few per cent means the sediment has nowhere to go and the deposition gates are
|
||
wrong; a median exposure of 1.00 means the outline has no bays at all and neither the surf reach nor the
|
||
shelter is doing any work.
|
||
|
||
**The slope–area fit cannot judge one change on one seed, and it was nearly used to reject a good one.** It is
|
||
the number this document calls the proof that closes the work, and at five or six bins on a 1400 grid its
|
||
seed-to-seed spread on *identical code* is larger than most changes: seed 7 gives −0.480 at R² 0.317, seed 9342
|
||
gives −0.698 at R² 0.704, and seed 67914 gives −0.575 at R² 0.944. Use it paired — the same seed before and
|
||
after — and across at least two seeds, or raise the bin count before leaning on it.
|
||
|
||
---
|
||
|
||
## 2. What was just built, in one paragraph
|
||
|
||
The coast, which until now was a line in a mask: the sea floor dropped to a flat plane at −180 m in one step
|
||
and no process knew the shoreline was there. `internal/coast` adds three that do, each derived rather than
|
||
drawn — a continental shelf whose width is read off the relief standing behind each stretch of shore, a surf
|
||
that planes the land to a shore platform within a reach set by how open the water is (the cliff is the step
|
||
where the reach ends), and a sediment budget that carries what the surf cut along the shore and lays it in
|
||
sheltered shallow water, with river mouths delivering their own load. It runs after the fluvial solve, on the
|
||
terrain the solve produced, and it owns the sea floor outright: `uplift.Result.Bathymetry` is gone and ocean
|
||
cells stay at sea level for the whole solve. Measuring it then said something about the *continent* rather than
|
||
about the coast — the fetch reported the median stretch of shoreline as fully open, because five octaves of
|
||
outline noise over a 14 km map put the finest coastal feature at 450 m and a coastline is fractal. D-51 takes
|
||
the outline to 8 octaves at gain 0.62, which is 96 km of shoreline against 64.
|
||
|
||
Full detail, including four things that were wrong first, is in `Terrain.md` under **What was built, and where
|
||
it differs**.
|
||
|
||
## 3. Where this is going
|
||
|
||
**Composition is parked.** The mountain fraction, the range grain and the fault traces are all real and all
|
||
still listed below, but they are *tuning* and the map is good enough to work against. Do not spend the next
|
||
session on them.
|
||
|
||
The goal is: **get generation working end to end, then make the world author-driven and scalable.** Three
|
||
things, in order.
|
||
|
||
### 3.1 Finish the pipeline (build-order step 6)
|
||
|
||
Passes 8–14 are unbuilt — upsample, detail noise, strata, particle, fine thermal, spawn pad, derive — so the
|
||
generator stops at the geology grid and `L_World` is still built by the numpy pipeline it was meant to
|
||
replace. Until this lands there is no full-resolution output and nothing to import, at any scale. It is also
|
||
the only work that changes how the terrain reads to a player standing on it: see §4.C.
|
||
|
||
### 3.2 Painted maps as the source
|
||
|
||
An author paints a world map; the simulation turns it into terrain. The manifest already anticipates a file
|
||
source — `"source": {"kind": "file", "path": ...}` is documented in `RawContent/World/README.md` and
|
||
`field.ReadHeightmap` exists — but **nothing in the Go tool reads it**: `Source.Kind` appears only in a
|
||
`Describe()` string, and the run always builds noise. So this is new work, not a re-wiring.
|
||
|
||
**Paint the uplift, not the height.** This is the one design decision that matters and it follows directly
|
||
from D-47 and from everything measured this session. The architecture is *noise becomes tectonics, and the
|
||
solve makes the terrain*; a painted heightmap would be handed to a solver that promptly erodes it into
|
||
something else, throwing away the drainage network that is the entire reason the generator was rewritten.
|
||
Painting uplift instead means an author draws intent — "a range here, lowlands there, coast like this" — and
|
||
gets terrain with real rivers, real divides and real valley hierarchy honouring it.
|
||
|
||
Suggested channels, all optional, all falling back to the procedural field where absent:
|
||
|
||
| Painted layer | Feeds | Notes |
|
||
| --- | --- | --- |
|
||
| Land / sea mask | `uplift.Result.Land`, `Base` | The outline. Almost certainly the first thing anyone wants to draw |
|
||
| Uplift rate | `Result.Rate` | The load-bearing one. Greyscale mapped to a mm/yr range from the manifest |
|
||
| Erodibility | `Result.K` | Rock types. Cheap, and it is where texture inside a range comes from |
|
||
| Sea floor | `Result.Bathymetry` | Cosmetic; it is put back after the solve and never erodes |
|
||
| Fault lines | `buildFaults` | Later. A line layer, not a raster |
|
||
|
||
**Rivers cannot be painted directly**, and it is worth knowing why before someone tries: a river is an
|
||
*output* of the drainage solve. What does work is biasing — raise `K` along a painted line so the water finds
|
||
the soft rock, or drop the uplift slightly along it, or seed a shallow valley into the initial relief. The
|
||
solve then chooses to put a river there for its own reasons and the result is still a coherent network. A
|
||
painted line forced into the height directly would be cut apart by the first thousand steps.
|
||
|
||
**The blend rule, which keeps painted maps from looking painted.** A painted map is coarse — 2048 px across a
|
||
100 km world is 50 m a pixel, five geology cells. Upsample it smoothly and let procedural noise supply
|
||
everything below its pixel size: **the painted map owns wavelengths above its resolution, noise owns those
|
||
below.** Without that rule a painted world is visibly blocky at the paint resolution; with it, an author
|
||
controls structure and the generator still supplies texture.
|
||
|
||
### 3.3 Scale, and why tiling is an architecture question
|
||
|
||
A world is big. Today's canvas is 14.28 km a side; the interesting sizes are 50–200 km. The numbers, measured
|
||
and extrapolated from the 256 s full geology run:
|
||
|
||
| World side | Area | Geology cells at 8 m | Fluvial solve, 1000 steps | Grid memory |
|
||
| --- | --- | --- | --- | --- |
|
||
| 14 km — today | 204 km² | 3.2 M | 4 min | ~150 MB |
|
||
| 50 km | 2 500 km² | 39 M | ~50 min | ~1.8 GB |
|
||
| 100 km | 10 000 km² | 156 M | ~3.5 h | ~7 GB |
|
||
| 200 km | 40 000 km² | 625 M | ~14 h | ~28 GB |
|
||
|
||
**The fluvial solve cannot be tiled.** Drainage area accumulates across the whole map and the priority-flood
|
||
needs global connectivity, so a river crossing a tile boundary needs its upstream catchment from the next
|
||
tile. Solving tiles independently gives wrong drainage areas and a discontinuity at every seam — and drainage
|
||
area is the term the whole model is built on. Halo exchange between tiles would work in principle and is a
|
||
large, iterative piece of work.
|
||
|
||
**The detail passes tile perfectly**, because every one of them is local: noise is pointwise, thermal
|
||
weathering propagates a cell at a time, and a droplet travels at most its lifetime in cells.
|
||
|
||
So the architecture already contains the answer, and it is the two-grid split that is already there:
|
||
|
||
> **Solve the geology whole, once, at a fixed physical cell size. Tile only the detail.**
|
||
|
||
That gives consistent relief for free, because the geology cell never changes — which matters more than it
|
||
sounds, and §4.D.3 explains why. It makes maximum world size a memory-and-patience question rather than a
|
||
correctness one: ~50 km is an hour, 100 km is an overnight bake, and beyond that the geology stage needs to
|
||
go out-of-core. Since the goal is explicitly a batched, offline bake, that seems an acceptable trade — but it
|
||
should be a decision made deliberately, with these numbers in front of whoever makes it.
|
||
|
||
**Two rules that make tiles seamless, and are much easier to adopt now than to retrofit:**
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
## 4. What looks wrong now
|
||
|
||
Ordered by how much it matters to the direction above, which is *not* the order of how visible it is on a
|
||
preview image.
|
||
|
||
### C. Detail — nothing exists at player scale · the blocker
|
||
|
||
Passes 8–14 of the pipeline table in `Terrain.md` are entirely unbuilt: upsample, detail noise, strata,
|
||
particle erosion, fine thermal, spawn pad, derive. The generator stops at the geology grid — 8 m cells at full
|
||
resolution — so at 2 m quads a player stands on a 4× upsample of a coarse grid with **no detail added at
|
||
all**. Ledges, scree, gullies, the strata shelves on a cut face: all of it lives in those passes, and every
|
||
one already exists as tuned numpy in `Scripts/Authoring/heightmap_erosion.py` waiting to be **ported, not
|
||
reinvented**. Carry its brakes across unchanged — the droplet slope gate, the per-step cut cap, the load cap,
|
||
the 3×3 cut brush and the own-cell deposit are each a lesson from the Worklog.
|
||
|
||
Budget from `Terrain.md`: upsample and detail noise 15 s, particle 90 s, fine thermal 20 s. The fluvial pass
|
||
is already 256 s against 120 s budgeted, so the five-minute bar is at risk before these land — and §3.3 says
|
||
the bar is probably the wrong constraint for a batch bake anyway. Worth deciding rather than drifting.
|
||
|
||
### D. Scale-independence — the one that becomes load-bearing
|
||
|
||
**D.3 Relief is resolution-dependent, and multi-scale makes that a blocker rather than a wart.** Measured
|
||
earlier: 1020 m relief at 512² against 2605 m at 1786² on one seed. The cause is that with
|
||
`critical_area_m2` at 0, stream power is applied down to a single cell, so the divide slope is
|
||
`U/(K·cell^2m)` — halve the cell and every divide steepens, for ever. As long as there was one canvas this was
|
||
a wart. The moment the same painted map is meant to produce worlds at different sizes or resolutions, it
|
||
means **the same input gives a different landscape depending on grid size**, which is fatal to the whole idea.
|
||
|
||
The textbook fix is a critical area, and it was re-measured this session and still fails (§6) because the
|
||
hillslope it creates has no transport law strong enough to shed its uplift. So this is genuinely open, and
|
||
the two candidate directions are:
|
||
|
||
- Fix the pairing: critical area *plus* a hillslope diffusivity that scales with cell size (effective `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.** Measured on three seeds:
|
||
14.1 %, 15.0 % and 14.0 % of the waterline sits inside the 4 % margin band that `continentMask` imposes to keep
|
||
land off the map border. The margin tapers by distance-to-edge, and a contour of distance-to-edge is a line
|
||
parallel to that edge, so wherever the continent would have run past the boundary it is cut off square. This is
|
||
pre-existing and it is *not* the frozen-rim failure the margin exists to prevent — `TestBorderIsAlwaysOcean`
|
||
confirms every border cell is still ocean on all three seeds, so nothing is frozen. It is cosmetic, and D-52
|
||
made it conspicuous: land inside the band now takes the full 2.0 mm/yr instead of a tapered rate, so the
|
||
straight-cut coast can be a mountain range rather than a low plain, which is exactly what seed 67914's southern
|
||
coast is. Cheapest fix, and it belongs with the outline work rather than with the coastal pass: perturb the
|
||
margin distance with a low-amplitude noise field so the cut follows a crenellated line instead of a ruled one.
|
||
|
||
**E2. The shelter contrast is real but thin.** Exposure comes out 0.00 / 0.90 / 1.00 at p10 / p50 / p90, so the
|
||
distribution is one long tail: a handful of genuine embayments and a lot of open coast. A floor of 0.15 on
|
||
shelter carries most of the deposition, which is deliberate — measured with no floor, 73 % of the sediment
|
||
budget came back unplaced, because real exposed coasts do have beaches, they just have less sand than the bay
|
||
next door. Re-measure this once the outline is painted rather than noised; it is the same question as E1 from
|
||
the other end.
|
||
|
||
**E3. The beach is a beach at 8 m, which is to say it is not one.** The surf reach is 110 m, or 14 cells on the
|
||
geology grid, and the berm, the wave-cut notch, the scree below a cliff and the sand itself are all finer than
|
||
that. They belong in the detail passes (§4.C) — and note that the surf reach is one of the few lengths in the
|
||
generator that is set by physics rather than by the canvas, so it does *not* scale with the map: at the 2 m
|
||
detail grid it is 55 cells, which is enough for a real profile. The coastal detail pass is a natural addition
|
||
to the list in §4.C rather than a separate piece of work.
|
||
|
||
**E4. Deltas are placed, not tuned.** `river_m3_per_km2` is 1.2e5 with an exponent of 0.6, and both are guesses;
|
||
on seed 7 they put 3.9 Mm³ through 16 mouths against 21 Mm³ from the cliffs. The mechanism is right — the supply
|
||
concentrates at the mouth and the drift kernel spreads it into the shallows — but nothing has yet asked whether
|
||
what comes out reads as a delta. `map_coast.png` at a crop is where that is judged.
|
||
|
||
**E5. Unverified: speckle at the finer canvas.** The outline gain went from 0.50 to 0.62 on measurements taken
|
||
at `--size 1400` (10.2 m cells). A finer grid resolves more of the threshold's wander, so the D-48 canvas at
|
||
8 m, and any painted map after it, could turn the same setting into a scatter of one-cell islands. Check the
|
||
shoreline length per unit land area and look at `preview.png` before assuming it carries over.
|
||
|
||
### B. Texture — wrong at mid scale
|
||
|
||
**B1. 33 % of the mountain class still sits within 2° of the repose angle**, so a third of the mountains are
|
||
shaped by the clamp rather than by erosion. Down from 44 %, and the nonlinear diffusion pass after the clamp
|
||
keeps it from showing as hard facets. Levers, most principled first: raise `max_hillslope_substeps` and
|
||
`slope_cap` (costs runtime, buys real stiffening); raise `diffusion_m2_yr` (cheap, but past ~0.05 it smooths
|
||
away the landforms — measured before as "melted wax"); or accept it, since a belt rising at 2 mm/yr genuinely
|
||
*is* landslide-dominated in the real world and the clamp is the right model there.
|
||
|
||
**B2. Multiple-flow-direction accumulation is unbuilt.** The hash jitter recovered most of the damage (R²
|
||
0.055 → 0.459) but D8 still lets a cell drain to only one of eight neighbours, and some basin boundaries on
|
||
the plains in `map_basins.png` are visibly straight. The proper fix is Freeman/Quinn MFD for `Accumulate`
|
||
only, keeping D8 receivers for the implicit solve — Braun–Willett needs a single receiver per node for the
|
||
update, but the *area* can come from MFD. Cost: MFD needs its own processing order (descending elevation)
|
||
rather than the D8 stack.
|
||
|
||
**B3. A ribbed, combed texture on the range flanks**, regularly spaced, roughly perpendicular to the crest.
|
||
Not diagnosed. Candidates to check before changing anything: the ridged-noise initial relief showing through
|
||
where the solve has not had time to overwrite it; channel spacing locking to the grid at small drainage area;
|
||
or the `crests` cellular-edge field at `crest_weight` 0.12. Test with `--stage uplift` and compare the initial
|
||
relief against the final flanks.
|
||
|
||
### A. Composition — parked, but recorded
|
||
|
||
All three are one-or-two-constant changes. They are listed so they are not rediscovered, not because they are
|
||
next.
|
||
|
||
**A1. Half the continent is mountain** — 51 % of land on seed 7, 44 % on seed 9342, against nothing like that
|
||
in reality. The cause is arithmetic:
|
||
|
||
```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.
|
||
|
||
**A2. Fault traces are drawn curves with stamped ends** — visible in `map_uplift.png` as straight-edged
|
||
polygonal facets and an abrupt cut across a summit. Four causes, all in `buildFaults`: the trace is a single
|
||
8-point parabola (`const segs = 8`, one `wander` bow); `signedDistance` over 8 straight segments gives a
|
||
piecewise-linear distance field, hence polygonal contours; beyond the last segment `inside` is false and the
|
||
influence stops dead; and `if r > convergent*1.6` flattens the strongest throws into plateaus. Fix: an
|
||
fBm-perturbed heading, `ThrowM` tapered to zero over the last ~15 % of length instead of cut at the tip, and
|
||
long faults broken into 2–3 overlapping en-echelon segments.
|
||
|
||
**A3. Range grain runs as straight parallel bands** — chains run NW–SE like corduroy on seed 9342.
|
||
`bv.Data[i] = float32(0.5 + across*2.2 + float64(wy.Data[i]-0.5)*0.32)` stretches the band 2.2× along one
|
||
angle with a single mild warp octave. Raise the warp, or warp with two octaves at different scales so chains
|
||
bend and bifurcate.
|
||
|
||
---
|
||
|
||
## 5. Suggested order
|
||
|
||
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.
|
||
|
||
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.
|
||
- **Importing a painted *heightmap* as the terrain.** See §3.2. The solve will erode it into something else
|
||
and the drainage network — the reason the generator exists — is thrown away. Paint the uplift.
|
||
|
||
## 7. Traps
|
||
|
||
- **`RawContent/World/World.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.
|