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.
235 lines
12 KiB
Markdown
235 lines
12 KiB
Markdown
# 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 |
|
||
|
||
```bash
|
||
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.
|
||
|
||
```bash
|
||
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 scalar** — `y/MapSize` plus a ±0.1 low-frequency wobble
|
||
2. **island falloff/mask** — squircle + ellipse, 50/50, then `Pow(·, 2.5f)`
|
||
3. **edge roughness** — `noise(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 spine** — `SmoothAbs` 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.
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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`.
|