islaApocalypse-v2/Tools/README.md
beezm b1cf282295 Phase 1: shaded relief and the Hispaniola palette — the grin gate
Presentation only. Nothing here touches Topography, the noise, or any generation
constant; the renderer is handed a height field LOADED from a .f32 dump and has no
way to produce one, so a look change provably cannot move the terrain.

Hillshade: Horn 3x3, cell size 1, edges clamped (never wrapped — the Trench border
is a wall, not a seam). ZExaggeration is required rather than optional: measured
median land slope is 0.22 degrees unexaggerated, so the island shades as a flat
plane. Chosen from a measured slope table — zex 75 gives 16/28/37 deg at land
median/p90/p99, which reads as natural relief. It is a look dial; raw units are not
metres and no code may read it as if they were.

Palette: stops placed on the MEASURED height distribution, not spread linearly.
Land is bottom-heavy (median 0.456, p99 1.113, max 1.415 over 8.6M land columns
across four seeds), so a linear ramp would spend 99% of its range on 1% of the land
and the map would read as green with a few white dots. Bathymetry is compressed by
d/(d+0.35) so the shallow shelf gets the range and the featureless abyss flattens.
Anchors are absolute and fixed across the batch — a map coloured on its own min/max
cannot be compared with its neighbour, and comparison is the point of a batch.

Blend: the naive tint x hillshade darkens everything by 29% before any slope is
involved, because flat ground shades to sin(altitude). So shade is normalized by
its flat-ground value first — flat terrain keeps its true tint and only SLOPE moves
the colour — then shadows multiply while highlights screen toward white. That
asymmetry is the difference between a colour ramp and a map you would frame.

Relief fades to zero with depth below sea. Not only taste: the deep floor is the
Trench, a synthetic wall whose gradient is ~1.5x the p99 land gradient, so shading
it faithfully draws a bright rim around the map and lights the abyss with noise
mottle. The fade puts relief on the near-shore shelf where the bathymetry is real.

Three named looks (atlas / relief / dusk), gated like any other change and kept
deliberately few — iteration fatigue on a subjective gate is a real failure mode.
Each pair isolates one question.

HeightMapRenderer.cs is retired: its raw I/O moved to HeightField.cs and its ramp
to ReliefPalette.cs, so there is one palette everywhere and the generator's own
quick-look is coloured identically to a beauty render.

No biome words, no classification, no water. The 0.15 line is a colour boundary.
2026-08-19 21:58:44 -04:00

12 KiB
Raw Blame History

Tools — the offline generator

Holds: the world generator and its diagnostics. Everything that produces a blueprint, and nothing that consumes one at play time.

Boundary — the tools wall:

⚠⚠ TOOLS MAY NOT REFERENCE Client/. THE DEPENDENCY RUNS ONE WAY, OR THE WALL IS NOT A WALL.

No UI, no player controllers, no shaders, no rendering code. Tools/ may use Core/.

Two reasons, both from Design - Tooling - Offline Generation.md: a lighter shipped client, and — less obviously — not handing players the world-generation logic. Shipping the generator lets it be reverse-engineered into an unfair map preview, with the whole island's layout, every settlement and every route known before anyone sets foot on the beach.

Tools emit data; they never write to a live save. A generator that could touch live server state is a corruption risk for no benefit.

What is here

The generator — pass 1 (Phase 1)

Ported from the reference's MapGenerator.GenerateTopography pass-1 loop at tag pre-rewrite-reference (ab78883). This is the crown-jewel port: working code and its tuned constants, carried over verbatim — not re-derived from a design summary (→ D-050, Design - Rewrite - Extraction Manifest.md).

File What it is
Scripts/TerrainNoise.cs The FastNoiseLite config, every fractal property pinned explicitly
Scripts/Topography.cs Pass 1 — the six elements, in the reference's execution order
Scripts/IslandFalloff.cs SmoothAbs + CREST_EPSILON (pass-1 half of the reference file)
Scripts/Pass1Result.cs The height field and the Phase-2 seams
Scripts/TerrainGenConfig.cs Config + the per-element ablation toggles
Scripts/HeightField.cs Raw .f32 save/load — the generation/presentation seam
Scripts/TerrainGenTool.cs Batch entry point (ladder + seed batch + INDEX.md)
Scenes/TerrainGenTool.tscn Run this

The relief render — presentation (Phase 1, the look)

Presentation only (→ Design - Rendering - Roughness Is Presentation.md): it changes how height is shown, never how it is made. It cannot change the terrain — it is handed a height field loaded from a .f32 dump and has no way to produce one.

File What it is
Scripts/Hillshade.cs Horn 3×3 shaded relief; the ZExaggeration slope table
Scripts/ReliefPalette.cs The hypsometric palettes, stops placed on measured percentiles
Scripts/ReliefRenderer.cs The tint + hillshade blend
Scripts/LookConfig.cs The look dials and the three named variants
Scripts/ReliefRenderTool.cs Taste-gate batch runner
Scenes/ReliefRenderTool.tscn Run this
Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
  --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/ReliefRenderTool.tscn

ISLA_LOOKS=atlas,relief,dusk · ISLA_SOURCE (batch to read .f32 from) · ISLA_MAPSIZE · ISLA_SEEDS · ISLA_BATCH · ISLA_DUMP_RAW=1

Three looks, deliberately — a subjective gate drowns in a wall of near-duplicates, and each pair isolates one question: atlas vs relief asks how strong the relief should be (same palette and light); atlas vs dusk asks about palette and sun angle.

⚠ Vertical exaggeration is required, and it is a LOOK dial — not a physical claim.

Height is in raw noise units over a 1 m grid, so the true per-pixel gradient is tiny: measured median land slope is 0.22°. Un-exaggerated, the whole island shades as a flat plane. The raw units are not metres — the metres-per-unit conversion is Phase 2's, not this renderer's.

The blend, and why it is not a multiply. Hillshade on flat ground is sin(altitude) ≈ 0.71, so a plain tint × shade darkens the entire map by 29% before any slope is involved and the hypsometric tints are never actually seen. So the shade is normalized by its flat-ground value first — flat terrain keeps its true tint, and only slope moves the colour — then shadows multiply while highlights screen toward white. That asymmetry is the difference between a colour ramp and a map you would frame.

⚠ Relief fades out with depth below sea. Not only taste: the deep floor is dominated by the Trench, a synthetic additive wall whose gradient is ~1.5× the p99 land gradient. Shading it faithfully draws a bright rim around the whole map and lights up the abyss with base-noise mottle — relief on terrain nobody is meant to look at. The fade puts relief where the bathymetry is real (the near-shore shelf) and lets the abyss lie flat, which is the cartographic convention anyway.

Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
  --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/TerrainGenTool.tscn

ISLA_MAPSIZE (default 2048) · ISLA_SEEDS (comma-separated, positive) · ISLA_BATCH · ISLA_SKIP_RAW=1 · ISLA_LADDER=0

The six ported elements, in order — the order is load-bearing, not an implementation detail:

  1. wobbled latitude scalary/MapSize plus a ±0.1 low-frequency wobble
  2. island falloff/mask — squircle + ellipse, 50/50, then Pow(·, 2.5f)
  3. edge roughnessnoise(x·2.5, y·2.5), modulated by the squircle so it bites only at the rim
  4. southern sinker — bottom 25%, before the power (its effect is superlinear)
  5. the Trench — map-anchored, additive on both axes, after the power
  6. mountain spineSmoothAbs crest, cubic, southern fade, amplitude 0.6f

⚠ The latitude scalar is NOT climate temperature.

The reference called it temperature and let biomes read the same array — a large part of how climate and terrain got fused. Here it is a stage-1-local latitude field that terrain geometry reads, and nothing else. Climate is stage 3 and classifies finished shape (D-049 §2, D-056). Do not alias, store, or rename this into a climate map.

Deferred to Phase 2, with the seam already open: the submarine coast shelf and the offshore islets (reference ~:621-664). Both act only below sea level and are judged once water renders. Pass1Result.PreTrenchFalloff is captured at the exact point they consume, and Pass1Result.HMaxSeed is carried for the seed-dependent redistribution curve.

Not here at all: the curve, erosion, rivers, water bodies, the crater, biomes, roads, the mesher.

⚠ The render writes a Godot.Image directly — not the capture path

The reference captured maps through a SubViewport + a TextureRect._Draw, because it composited vector overlays (roads, town markers, river polylines) onto the raster. That machinery awaits render frames — which is exactly why unattended runs need xvfb-run and why --headless hangs on it.

Phase 1 has no overlays. A heightmap is a raster, so it is written per-pixel into an Image and saved: no viewport, no frame awaits, no display. Do not reintroduce the capture path by habit — it becomes correct the moment something vector needs compositing, and not before.

Diagnostics

  • Scenes/UserDirProbe.tscn — confirms this project's user:// is its own, not the old prototype's. Exits non-zero on a clash.
  • Scenes/NoiseDefaultsProbe.tscn — prints this engine's FastNoiseLite constructor defaults and checks them against the recorded baseline. Run it after any Godot upgrade. It cannot change the terrain (TerrainNoise pins every value); it makes a drifting default visible instead of silent.

The conventions Phase 1's generator inherits

These were earned, not assumed — each one came out of the prototype's terrain arc. → Design - Tooling - Iteration and Batching.md.

1. xvfb-run for unattended generation — NOT --headless

--headless HANGS on a real generation run.

The snapshot capture awaits render frames, and there is no frame loop without a display. Measured, not assumed. A run left on --headless does not fail loudly; it sits there.

xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 --path ~/celerNexus/islaApocalypse-v2 <scene>

A windowed run on the desktop also invites being closed mid-run by whoever is using the machine.

The one exception is a job that awaits no frames — like UserDirProbe, which is why that one documents --headless explicitly:

Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
  --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/UserDirProbe.tscn

If a job awaits a frame, it needs xvfb-run. When unsure, use xvfb-run — it is correct in both cases and costs nothing.

2. Environment overrides — safe BY CODE, not by care

Every path a tool reads or writes resolves through Core/Scripts/ToolingPaths.cs, and each has an environment override, so a batch cannot read over or write to the developer's live files:

Variable Overrides Default
ISLA_CONFIG_PATH the generation config file user://config.json
ISLA_BLUEPRINT_PATH blueprints read/written user://blueprints
ISLA_OUTPUT_DIR generation output (batches live under it) user://output

The point is that it is enforced by CODE, not by care. A rule that depends on an executor remembering it will eventually meet an executor who does not.

There is no second way to obtain these paths. Do not add one.

3. ⚠ File safety — permanent rules

Enforced by Core/Scripts/FileSafety.cs, which throws rather than advises.

  1. No deletion in the runtime root or anywhere under batches/.
  2. Intermediates persist in a scratch/ subfolder that is never cleaned.
  3. The developer's placed files are never touched.
  4. Any deletion is named explicitly in the run report.

These came from a real incident: an executor admitted it had been deleting the developer's staged test blueprint. It was owned and fixed in code. That is why these are rules and not guidance.

4. Batch layout

batches/NN_<name>/<seed>_<variant>/
batches/NN_<name>/INDEX.md
batches/NN_<name>/scratch/          ← persistent; never cleaned

A/B comparisons are browsed by a human, and a flat directory of same-named PNGs is not browsable. The INDEX.md is what makes a batch readable a week later.

Batches are OUTPUT. They live under ISLA_OUTPUT_DIR (i.e. under user://), not in this repo. The batches/ folder here carries the convention, not the data. No batches are run this phase.

5. Iteration levers, for when the generator exists

  • Skip the road pass for all terrain and water iteration. In the prototype this cut a run from ~26 min to ~80 s — a 19× cut, and the difference between iterating on terrain and not iterating on terrain. Blueprints from such a run carry present-but-empty road sections, which is legitimate, not corrupt.
  • Build the oracle before the taste-iteration, not after. Because the classify path was pinned to uncurved height, every shaping iteration had a hard automatic correctness check (biome and water maps md5-identical). Five rounds of taste-iteration were safe because correctness was not being judged by eye. Where a future phase has a subjective gate, ask first what the automatic invariant is.
  • Config-gate every shaping change, with the legacy behaviour surviving on the other side. It keeps changes comparable as an A/B pair, and it means a rejected change costs a flipped default rather than a reverted commit.
  • The lever is variants per batch, not speed per pass. Two Phase B tasks ran ~2 hours each, which looked like the pipeline getting slower. It was not — a single pass stayed at ~2 minutes. The cost was A/B/ablation multiplication. When a batch is genuinely wide, say so up front.

6. Scaling discipline

Nothing uses a raw pixel number. Every distance, radius, threshold and noise frequency comes from Core/Scripts/GenerationScale.cs. Read its header before adding any constant — including the tightening on normalized additive noise offsets, which is a forward-guard for the Phase 1 noise port. → Design - Tooling - Scaling Discipline.md.