Phase 1: port the crown-jewel noise and pass-1, and render the island
Ports MapGenerator.GenerateTopography's pass-1 loop (reference ~:553-619 at tag
pre-rewrite-reference / ab78883) into Tools/. Working code and its tuned constants
carried over verbatim, read from source rather than re-derived from a design
summary (D-050). Six elements, in the reference's execution order — the order is
load-bearing: the sinker lands before the Pow (superlinear), the Trench after it
(a raw additive wall), preTrenchFalloff captured between them.
The fractal config is now PINNED. The reference set only NoiseType/Seed/Frequency,
so the island's entire fractal character was Godot 4.7.1 constructor defaults that
nothing recorded. Measured on 4.7.2 (NoiseDefaultsProbe): Fbm / 5 / 0.5 / 2.0 / 0.0
— identical to the 4.7.1 values, so pinning reproduces the look with no delta. The
terrain no longer depends on an engine default, and the probe makes a future drift
visible instead of silent.
Two deliberate departures from the reference's literals, both flagged in-code:
- the latitude noise offset is expressed in map widths, not the reference's raw
+1000 px, so a resize no longer samples a different slice of the noise field
(chat1/00 §4.2). Pinned to 1000/8192 so the 8K realization is unchanged.
- the `if (temperature < 0.65f)` spine gate is dropped as the provable no-op it
is — southernFade already reaches exactly 0 at 0.65. Bit-identical.
The reference's "temperature" is ported as a latitude field and deliberately NOT
named temperature: it is stage-1-local input to terrain geometry, and climate is
stage 3 (D-049 §2, D-056). The ±0.1 wobble is kept — it is what stops the spine's
southern terminus being a ruler-straight line.
Deferred to Phase 2 with the seam open: coast shelf and offshore islets (below-sea,
judged once water renders). Pass1Result exposes PreTrenchFalloff at their exact
consumption point, plus HMaxSeed for the seed-dependent curve.
Render is a direct Godot.Image, not the SubViewport capture path — that machinery
exists to composite vector overlays and is why --headless hangs; Phase 1 has none.
Hypsometric above 0.15, bathymetric below, fixed ramp anchors so seeds and ablation
rungs are comparable. No biome words anywhere.
Verified: land fraction 51.5% identical at 1024, 2048 and 10240 — the scaling
discipline holds across a 10x resize. Full-size 10240 pass: 36.3 s.
No curve, erosion, rivers, water, crater, biomes, roads, or mesher.
This commit is contained in:
parent
9107b0822b
commit
9af5c73b04
19 changed files with 1159 additions and 4 deletions
|
|
@ -17,11 +17,76 @@ nothing that consumes one at play time.
|
||||||
> **Tools emit data; they never write to a live save.** A generator that could touch live server
|
> **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.
|
> state is a corruption risk for no benefit.
|
||||||
|
|
||||||
## Empty this phase — Phase 1 fills it
|
## What is here
|
||||||
|
|
||||||
The only thing here now is `Scenes/UserDirProbe.tscn` + `Scripts/UserDirProbe.cs`, a print-and-quit
|
### The generator — pass 1 (Phase 1)
|
||||||
diagnostic that confirms this project's `user://` directory is its own and not the old prototype's.
|
|
||||||
It generates nothing.
|
**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/HeightMapRenderer.cs` | Hypsometric PNG + raw `.f32` dump |
|
||||||
|
| `Scripts/TerrainGenTool.cs` | Batch entry point (ladder + seed batch + `INDEX.md`) |
|
||||||
|
| `Scenes/TerrainGenTool.tscn` | Run this |
|
||||||
|
|
||||||
|
```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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
6
Tools/Scenes/NoiseDefaultsProbe.tscn
Normal file
6
Tools/Scenes/NoiseDefaultsProbe.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3 uid="uid://bqp1x2v3isla1"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://Tools/Scripts/NoiseDefaultsProbe.cs" id="1_ndp"]
|
||||||
|
|
||||||
|
[node name="NoiseDefaultsProbe" type="Node"]
|
||||||
|
script = ExtResource("1_ndp")
|
||||||
6
Tools/Scenes/TerrainGenTool.tscn
Normal file
6
Tools/Scenes/TerrainGenTool.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3 uid="uid://bqp1x2v3isla2"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://Tools/Scripts/TerrainGenTool.cs" id="1_tgt"]
|
||||||
|
|
||||||
|
[node name="TerrainGenTool" type="Node"]
|
||||||
|
script = ExtResource("1_tgt")
|
||||||
148
Tools/Scripts/HeightMapRenderer.cs
Normal file
148
Tools/Scripts/HeightMapRenderer.cs
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Renders a raw pass-1 height field as a HYPSOMETRIC height-gradient PNG.
|
||||||
|
///
|
||||||
|
/// ═══ ⚠ WHY THIS WRITES A Godot.Image DIRECTLY, AND NOT VIA THE CAPTURE PATH ═══
|
||||||
|
///
|
||||||
|
/// The reference captured its maps through a SubViewport + a TextureRect whose `_Draw` painted
|
||||||
|
/// overlays — roads, town markers, river polylines — and then read the viewport texture back.
|
||||||
|
/// That machinery exists to COMPOSITE VECTOR OVERLAYS onto a raster. It also carries a real
|
||||||
|
/// cost: it awaits render frames, which is why unattended runs need `xvfb-run` and why
|
||||||
|
/// `--headless` HANGS on it.
|
||||||
|
///
|
||||||
|
/// Phase 1 has no overlays. A heightmap is a raster and nothing else, so it is written
|
||||||
|
/// per-pixel into an Image and saved. No viewport, no frame awaits, no display needed.
|
||||||
|
///
|
||||||
|
/// ⚠ DO NOT REINTRODUCE THE CAPTURE PATH BY HABIT. It becomes correct the moment something
|
||||||
|
/// vector needs compositing on top (roads, towns, river centrelines) — and not before.
|
||||||
|
///
|
||||||
|
/// ═══ THE COLOUR SCHEME ═══
|
||||||
|
///
|
||||||
|
/// Two ramps meeting at the sea threshold, Hispaniola-style:
|
||||||
|
/// below — bathymetric: deep navy → shallow cyan-ish blue
|
||||||
|
/// above — hypsometric: shore green → lowland green → tan → brown → grey → white peak
|
||||||
|
///
|
||||||
|
/// ⚠⚠ THERE ARE NO BIOME WORDS HERE AND THERE MUST NOT BE. Nothing in this file knows about
|
||||||
|
/// jungle, wasteland, snow or desert. It reads a single float per column and picks a colour on
|
||||||
|
/// an elevation ramp. Biomes are a stage-5 CLASSIFICATION of finished shape (D-049 §2, D-056),
|
||||||
|
/// and the whole point of the rewrite's seam is that terrain never learns their names.
|
||||||
|
/// "White at the top" is snow-COLOURED cartography, not a snow biome.
|
||||||
|
///
|
||||||
|
/// ⚠ The sea threshold is a VISUALIZATION boundary only. No water is modelled: no water bodies,
|
||||||
|
/// no ocean/lake distinction, no flooding. Phase 2 owns all of that.
|
||||||
|
/// </summary>
|
||||||
|
public static class HeightMapRenderer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// FIXED ramp anchors in raw height units, deliberately NOT per-run normalized.
|
||||||
|
///
|
||||||
|
/// Per-run normalization would make every PNG use its full colour range, which looks better
|
||||||
|
/// and lies: two seeds, or two rungs of the ablation ladder, would be coloured by different
|
||||||
|
/// scales and could not be compared by eye. Fixed anchors mean the same colour is the same
|
||||||
|
/// height in every image in a batch. Out-of-range values clamp, and the run prints the true
|
||||||
|
/// min/max so saturation is visible as a number rather than guessed from the picture.
|
||||||
|
/// </summary>
|
||||||
|
public const float RampTopHeight = 1.45f;
|
||||||
|
|
||||||
|
/// <summary>The deepest height the bathymetric ramp resolves. Below this, flat abyss colour.</summary>
|
||||||
|
public const float RampBottomHeight = -1.00f;
|
||||||
|
|
||||||
|
// ---- the land ramp: (t in [0,1] above sea) -> colour ----
|
||||||
|
private static readonly (float t, Color c)[] LandRamp =
|
||||||
|
{
|
||||||
|
(0.00f, new Color(0.36f, 0.55f, 0.35f)), // shore green
|
||||||
|
(0.12f, new Color(0.47f, 0.62f, 0.36f)), // lowland
|
||||||
|
(0.30f, new Color(0.72f, 0.71f, 0.44f)), // dry tan
|
||||||
|
(0.50f, new Color(0.72f, 0.57f, 0.36f)), // brown
|
||||||
|
(0.70f, new Color(0.60f, 0.45f, 0.34f)), // dark brown
|
||||||
|
(0.86f, new Color(0.66f, 0.64f, 0.64f)), // grey rock
|
||||||
|
(1.00f, new Color(0.98f, 0.98f, 0.98f)), // white peak
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- the sea ramp: (t in [0,1], 0 = deepest) -> colour ----
|
||||||
|
private static readonly (float t, Color c)[] SeaRamp =
|
||||||
|
{
|
||||||
|
(0.00f, new Color(0.03f, 0.06f, 0.18f)), // abyss
|
||||||
|
(0.45f, new Color(0.06f, 0.16f, 0.36f)), // deep
|
||||||
|
(0.75f, new Color(0.11f, 0.30f, 0.52f)), // mid
|
||||||
|
(0.92f, new Color(0.20f, 0.47f, 0.66f)), // shelf
|
||||||
|
(1.00f, new Color(0.42f, 0.68f, 0.79f)), // shallow, at the waterline
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Render and save. Returns the absolute path written.</summary>
|
||||||
|
public static string SavePng(Pass1Result result, float seaLevel, string absolutePath)
|
||||||
|
{
|
||||||
|
int n = result.MapSize;
|
||||||
|
var img = Image.CreateEmpty(n, n, false, Image.Format.Rgb8);
|
||||||
|
|
||||||
|
for (int x = 0; x < n; x++)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < n; y++)
|
||||||
|
{
|
||||||
|
float h = result.Height[x, y];
|
||||||
|
Color c;
|
||||||
|
|
||||||
|
if (h >= seaLevel)
|
||||||
|
{
|
||||||
|
float t = Mathf.Clamp((h - seaLevel) / (RampTopHeight - seaLevel), 0f, 1f);
|
||||||
|
c = Sample(LandRamp, t);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float t = Mathf.Clamp((h - RampBottomHeight) / (seaLevel - RampBottomHeight), 0f, 1f);
|
||||||
|
c = Sample(SeaRamp, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image is row-major (x = column, y = row); the height field is [x, y] with y
|
||||||
|
// increasing SOUTHWARD, matching the reference's convention. So a larger y is
|
||||||
|
// further down the image, and "the southern sinker" sinks the bottom of the PNG.
|
||||||
|
img.SetPixel(x, y, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Error err = img.SavePng(absolutePath);
|
||||||
|
if (err != Error.Ok)
|
||||||
|
GD.PrintErr($"[HeightMapRenderer] SavePng failed ({err}) for {absolutePath}");
|
||||||
|
return absolutePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color Sample((float t, Color c)[] ramp, float t)
|
||||||
|
{
|
||||||
|
for (int i = 1; i < ramp.Length; i++)
|
||||||
|
{
|
||||||
|
if (t <= ramp[i].t)
|
||||||
|
{
|
||||||
|
float span = ramp[i].t - ramp[i - 1].t;
|
||||||
|
float local = span <= 0f ? 0f : (t - ramp[i - 1].t) / span;
|
||||||
|
return ramp[i - 1].c.Lerp(ramp[i].c, local);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ramp[ramp.Length - 1].c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dump the raw float height field beside the PNG, little-endian float32, x-major.
|
||||||
|
///
|
||||||
|
/// Cheap, and it is what makes a future BYTE-LEVEL port-fidelity check possible — the kind
|
||||||
|
/// of automatic oracle that lets taste-iteration happen without correctness being judged by
|
||||||
|
/// eye. → `Design - Tooling - Iteration and Batching.md`, "build the oracle first".
|
||||||
|
/// </summary>
|
||||||
|
public static string SaveRaw(Pass1Result result, string absolutePath)
|
||||||
|
{
|
||||||
|
using var f = FileAccess.Open(absolutePath, FileAccess.ModeFlags.Write);
|
||||||
|
if (f == null)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"[HeightMapRenderer] could not open {absolutePath}: {FileAccess.GetOpenError()}");
|
||||||
|
return absolutePath;
|
||||||
|
}
|
||||||
|
int n = result.MapSize;
|
||||||
|
for (int x = 0; x < n; x++)
|
||||||
|
for (int y = 0; y < n; y++)
|
||||||
|
f.StoreFloat(result.Height[x, y]);
|
||||||
|
return absolutePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/HeightMapRenderer.cs.uid
Normal file
1
Tools/Scripts/HeightMapRenderer.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://tufk8dg4g14g
|
||||||
51
Tools/Scripts/IslandFalloff.cs
Normal file
51
Tools/Scripts/IslandFalloff.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shape helpers for the island mask, ported from the reference's
|
||||||
|
/// <c>Tools/Scripts/IslandFalloff.cs</c>.
|
||||||
|
///
|
||||||
|
/// ⚠ ONLY THE PASS-1 PARTS ARE HERE. The reference file also carries the submarine COAST SHELF
|
||||||
|
/// (SHELF_STRENGTH / SHELF_SCALE_M / CoastShelf) and the OFFSHORE ISLET layer (OffshoreBlob,
|
||||||
|
/// OffshoreZoneWeight, CalibrateThreshold, and their constants). Both are DEFERRED to Phase 2 —
|
||||||
|
/// they act on below-sea height and are judged once water renders. They will port into THIS
|
||||||
|
/// file, which is why it keeps the reference's name and shape.
|
||||||
|
///
|
||||||
|
/// This type is pure math and engine-free. It sits in Tools/ rather than Core/ so the pass-1
|
||||||
|
/// port stays auditable as one unit against one reference file, and so the deferred Phase-2
|
||||||
|
/// halves land beside their siblings rather than in a second location.
|
||||||
|
/// </summary>
|
||||||
|
public static class IslandFalloff
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The rounding width of the spine crest, in normalized axis-distance units.
|
||||||
|
/// READ from the reference: <c>IslandFalloff.CREST_EPSILON = 0.03f</c> (~:34).
|
||||||
|
/// </summary>
|
||||||
|
public const float CREST_EPSILON = 0.03f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A smooth absolute value: zero AT zero, with zero slope there, converging to |d| away from it.
|
||||||
|
/// READ from the reference (~:42-46), verbatim:
|
||||||
|
/// <code>a = |d|; return a*a / sqrt(a*a + eps*eps);</code>
|
||||||
|
///
|
||||||
|
/// ═══ WHY IT EXISTS — do not "simplify" it back to Abs ═══
|
||||||
|
///
|
||||||
|
/// The spine's ridge axis is the line x = centre, and a plain <c>1 - |x - cx|</c> peaks there
|
||||||
|
/// with a SLOPE DISCONTINUITY. On real terrain that put the four columns at the centre axis
|
||||||
|
/// in the top four slope-step locations out of 5,999 — a visible crease running the whole
|
||||||
|
/// height of the island, measured at ~170x the off-axis controls. It was blamed on the
|
||||||
|
/// falloff blend for a long time; transect measurement ruled that out and found this.
|
||||||
|
///
|
||||||
|
/// SmoothAbs(0) = 0, so the crest keeps its FULL HEIGHT — it rounds without dropping.
|
||||||
|
/// Measured on the reference: the 1-px kink fell 99.3%, the centre column dropped from
|
||||||
|
/// rank 1 of 5,999 to rank 1,364, and nothing anywhere was lowered.
|
||||||
|
/// → `Design - Terrain - Mountain Spine.md`.
|
||||||
|
/// </summary>
|
||||||
|
public static float SmoothAbs(float d, float epsilon)
|
||||||
|
{
|
||||||
|
float a = Math.Abs(d);
|
||||||
|
return a * a / MathF.Sqrt(a * a + epsilon * epsilon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/IslandFalloff.cs.uid
Normal file
1
Tools/Scripts/IslandFalloff.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://k4rjv4ck6l6j
|
||||||
86
Tools/Scripts/NoiseDefaultsProbe.cs
Normal file
86
Tools/Scripts/NoiseDefaultsProbe.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A print-and-quit diagnostic: reports this engine build's <see cref="FastNoiseLite"/>
|
||||||
|
/// CONSTRUCTOR DEFAULTS, and checks them against the values the ported terrain expects.
|
||||||
|
///
|
||||||
|
/// ═══ WHY THIS EXISTS ═══
|
||||||
|
///
|
||||||
|
/// The reference prototype set only three noise properties — NoiseType, Seed, Frequency — and
|
||||||
|
/// left fractal type, octaves, gain, lacunarity and weighted-strength untouched. So the island's
|
||||||
|
/// entire fractal character was Godot 4.7.1's CONSTRUCTOR DEFAULTS, not code. Nothing in that
|
||||||
|
/// repo recorded what they were. (chat1/00 §4.1.)
|
||||||
|
///
|
||||||
|
/// <see cref="TerrainNoise"/> now pins every one of them explicitly, so the terrain no longer
|
||||||
|
/// depends on an engine default. This probe is the other half of that guarantee: it makes a
|
||||||
|
/// silent default change VISIBLE on an engine upgrade instead of quietly reshaping the island.
|
||||||
|
///
|
||||||
|
/// Run it after any Godot version bump:
|
||||||
|
///
|
||||||
|
/// Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
|
||||||
|
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/NoiseDefaultsProbe.tscn
|
||||||
|
///
|
||||||
|
/// Exits non-zero if this engine's defaults have drifted from the recorded baseline — so it
|
||||||
|
/// works as a pre-flight check, not just by eye. A non-zero exit is NOT "the terrain is broken":
|
||||||
|
/// the terrain is pinned and fine. It means the baseline note below needs updating, and that
|
||||||
|
/// anyone reading old code which relied on defaults should be warned.
|
||||||
|
/// </summary>
|
||||||
|
public partial class NoiseDefaultsProbe : Node
|
||||||
|
{
|
||||||
|
// The defaults measured on Godot 4.7.2.stable.mono (this project's pinned engine), which
|
||||||
|
// match the Godot 4 documented defaults the reference implicitly rode on 4.7.1.
|
||||||
|
private const FastNoiseLite.FractalTypeEnum BaselineFractalType = FastNoiseLite.FractalTypeEnum.Fbm;
|
||||||
|
private const int BaselineOctaves = 5;
|
||||||
|
private const float BaselineGain = 0.5f;
|
||||||
|
private const float BaselineLacunarity = 2.0f;
|
||||||
|
private const float BaselineWeightedStrength = 0.0f;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
var n = new FastNoiseLite();
|
||||||
|
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
GD.Print(" FastNoiseLite CONSTRUCTOR DEFAULTS — this engine build");
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
GD.Print($"engine : {Engine.GetVersionInfo()["string"]}");
|
||||||
|
GD.Print("------------------------------------------------------------------");
|
||||||
|
GD.Print($"NoiseType : {n.NoiseType}");
|
||||||
|
GD.Print($"Frequency : {n.Frequency}");
|
||||||
|
GD.Print($"FractalType : {n.FractalType}");
|
||||||
|
GD.Print($"FractalOctaves : {n.FractalOctaves}");
|
||||||
|
GD.Print($"FractalGain : {n.FractalGain}");
|
||||||
|
GD.Print($"FractalLacunarity : {n.FractalLacunarity}");
|
||||||
|
GD.Print($"FractalWeightedStrength : {n.FractalWeightedStrength}");
|
||||||
|
GD.Print($"Seed : {n.Seed}");
|
||||||
|
GD.Print("------------------------------------------------------------------");
|
||||||
|
GD.Print(" DRIFT CHECK vs the recorded 4.7.2 baseline");
|
||||||
|
GD.Print("------------------------------------------------------------------");
|
||||||
|
|
||||||
|
bool drift = false;
|
||||||
|
drift |= Check("FractalType", n.FractalType.ToString(), BaselineFractalType.ToString());
|
||||||
|
drift |= Check("FractalOctaves", n.FractalOctaves.ToString(), BaselineOctaves.ToString());
|
||||||
|
drift |= Check("FractalGain", n.FractalGain.ToString(), BaselineGain.ToString());
|
||||||
|
drift |= Check("FractalLacunarity", n.FractalLacunarity.ToString(), BaselineLacunarity.ToString());
|
||||||
|
drift |= Check("FractalWeightedStrength", n.FractalWeightedStrength.ToString(), BaselineWeightedStrength.ToString());
|
||||||
|
|
||||||
|
GD.Print("------------------------------------------------------------------");
|
||||||
|
GD.Print(drift
|
||||||
|
? " ⚠ DRIFT — this engine's defaults differ from the recorded baseline.\n" +
|
||||||
|
" The PORTED TERRAIN IS UNAFFECTED: TerrainNoise pins every value explicitly.\n" +
|
||||||
|
" Update the baseline constants in this probe, and note the delta."
|
||||||
|
: " ✓ NO DRIFT — defaults match the recorded baseline.");
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
|
||||||
|
GetTree().Quit(drift ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Check(string name, string actual, string expected)
|
||||||
|
{
|
||||||
|
bool differs = actual != expected;
|
||||||
|
GD.Print($" {(differs ? "✗" : "✓")} {name,-24} actual={actual,-12} baseline={expected}");
|
||||||
|
return differs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/NoiseDefaultsProbe.cs.uid
Normal file
1
Tools/Scripts/NoiseDefaultsProbe.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b7vf6kp8041ie
|
||||||
88
Tools/Scripts/Pass1Result.cs
Normal file
88
Tools/Scripts/Pass1Result.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Everything pass 1 produces. → <see cref="Topography"/>.
|
||||||
|
///
|
||||||
|
/// ⭐ THREE OF THESE FIELDS ARE THE PHASE-2 SEAM. The reference computed them in pass 1 and
|
||||||
|
/// consumed them in pass 2; exposing them now is what keeps the deferred ports clean, and it
|
||||||
|
/// costs nothing to carry.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Pass1Result
|
||||||
|
{
|
||||||
|
/// <summary>Map side in columns.</summary>
|
||||||
|
public readonly int MapSize;
|
||||||
|
|
||||||
|
/// <summary>The resolved seed this was generated with. Always positive, always recorded.</summary>
|
||||||
|
public readonly int Seed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The raw pass-1 height field, <c>[x, y]</c>. Pre-curve, pre-erosion, pre-crater.
|
||||||
|
/// The reference's <c>finalH</c> (~:619) written to <c>_heightMap</c> (~:666).
|
||||||
|
/// </summary>
|
||||||
|
public readonly float[,] Height;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐ PHASE-2 SEAM — the falloff value captured BEFORE the <c>Pow(·, 2.5f)</c> and BEFORE the
|
||||||
|
/// Trench wall (reference ~:591, <c>preTrenchFalloff</c>).
|
||||||
|
///
|
||||||
|
/// It is the "how far past the island body are we" number, uncontaminated by the power's
|
||||||
|
/// curvature and by the Trench's additive ramp. The DEFERRED offshore-islet layer reads it
|
||||||
|
/// as its distance mask (<c>OffshoreZoneWeight</c>), which is the only reason it is captured
|
||||||
|
/// mid-computation rather than recomputed.
|
||||||
|
///
|
||||||
|
/// ⚠ A port that "tidies" the falloff chain into one expression destroys this capture point,
|
||||||
|
/// and the loss would not show up until Phase 2's offshore mask moved. It is exposed here so
|
||||||
|
/// that cannot happen quietly.
|
||||||
|
/// </summary>
|
||||||
|
public readonly float[,] PreTrenchFalloff;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐ PHASE-2 SEAM — the wobbled latitude scalar, <c>[x, y]</c>.
|
||||||
|
///
|
||||||
|
/// ⚠ THIS IS NOT A CLIMATE TEMPERATURE MAP. See <see cref="Topography"/> for the full note.
|
||||||
|
/// It is a stage-1-local latitude field that terrain GEOMETRY reads. Phase 2 needs it if the
|
||||||
|
/// latitude sea-level model is ever switched on (<c>SeaLevelModel = "field"</c>); under the
|
||||||
|
/// shipped flat model it has no consumer beyond the spine.
|
||||||
|
/// </summary>
|
||||||
|
public readonly float[,] LatitudeField;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐ PHASE-2 SEAM — the maximum height over the whole map (reference <c>_hMaxSeed</c>, ~:665).
|
||||||
|
///
|
||||||
|
/// The reference's v2+ redistribution curve is SEED-DEPENDENT: its spike maps
|
||||||
|
/// <c>[t4, hMaxSeed]</c> onto the peak band, so pass 2 cannot start until pass 1 has scanned
|
||||||
|
/// every pixel. That is precisely why the pass-1/pass-2 boundary is a hard one and not an
|
||||||
|
/// interleave. Carried here so Phase 2 inherits it rather than rediscovering the dependency.
|
||||||
|
/// </summary>
|
||||||
|
public readonly float HMaxSeed;
|
||||||
|
|
||||||
|
/// <summary>The minimum height. Not a reference field — carried for the renderer's ramp and the report.</summary>
|
||||||
|
public readonly float HMinSeed;
|
||||||
|
|
||||||
|
/// <summary>Wall-clock milliseconds the pass took.</summary>
|
||||||
|
public readonly ulong ElapsedMs;
|
||||||
|
|
||||||
|
public Pass1Result(int mapSize, int seed, float[,] height, float[,] preTrenchFalloff,
|
||||||
|
float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs)
|
||||||
|
{
|
||||||
|
MapSize = mapSize;
|
||||||
|
Seed = seed;
|
||||||
|
Height = height;
|
||||||
|
PreTrenchFalloff = preTrenchFalloff;
|
||||||
|
LatitudeField = latitudeField;
|
||||||
|
HMaxSeed = hMaxSeed;
|
||||||
|
HMinSeed = hMinSeed;
|
||||||
|
ElapsedMs = elapsedMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Fraction of the map at or above the sea threshold. A cheap shape sanity number.</summary>
|
||||||
|
public float LandFraction(float seaLevel)
|
||||||
|
{
|
||||||
|
long land = 0;
|
||||||
|
for (int x = 0; x < MapSize; x++)
|
||||||
|
for (int y = 0; y < MapSize; y++)
|
||||||
|
if (Height[x, y] >= seaLevel) land++;
|
||||||
|
return land / (float)((long)MapSize * MapSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/Pass1Result.cs.uid
Normal file
1
Tools/Scripts/Pass1Result.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dxtoqjudaokbe
|
||||||
104
Tools/Scripts/TerrainGenConfig.cs
Normal file
104
Tools/Scripts/TerrainGenConfig.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
using IslaApocalypse.Core;
|
||||||
|
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The generator's configuration, including the per-element ABLATION TOGGLES.
|
||||||
|
///
|
||||||
|
/// ═══ WHY EVERY PASS-1 ELEMENT IS GATED ═══
|
||||||
|
///
|
||||||
|
/// This is the standing A/B discipline: every shaping change ships behind a gate with the
|
||||||
|
/// previous behaviour surviving on the other side, so changes stay comparable as a pair rather
|
||||||
|
/// than as a memory of last week's render, and a rejected change costs a flipped default rather
|
||||||
|
/// than a reverted commit. → `Design - Tooling - Iteration and Batching.md`.
|
||||||
|
///
|
||||||
|
/// For THIS task the gates do a second job: they are the PORT-FIDELITY CHECK. Generating
|
||||||
|
/// base-noise-only, then adding one element at a time, shows each ported element doing what the
|
||||||
|
/// reference's did — rather than judging six simultaneous changes by their sum.
|
||||||
|
///
|
||||||
|
/// ⚠ Lives in Tools/, not Core/. It configures the generator specifically; Core carries the
|
||||||
|
/// world's data contracts and the scaling rule, not a tool's dials.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TerrainGenConfig
|
||||||
|
{
|
||||||
|
// ---- world ----------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map side in columns. A GENERATION PARAMETER — never baked in.
|
||||||
|
/// Iteration runs small (2048–4096); the shape is scale-invariant by construction, so the
|
||||||
|
/// island reads the same at any size and a full-size pass is confirmation, not iteration.
|
||||||
|
/// </summary>
|
||||||
|
public int MapSize = 2048;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The noise seed. POSITIVE ONLY. Zero or negative means "pick one and print it" — a run
|
||||||
|
/// whose seed is not recorded is a run that cannot be reproduced, so the resolved seed is
|
||||||
|
/// always printed and always in the filename.
|
||||||
|
/// </summary>
|
||||||
|
public int Seed = 0;
|
||||||
|
|
||||||
|
// ---- island shape (reference ConfigManager defaults, READ from source) ----
|
||||||
|
|
||||||
|
/// <summary>Falloff X axis ratio. Reference: <c>ConfigManager.LEGACY_AXIS_X = 1.15f</c>.</summary>
|
||||||
|
public float IslandAxisX = 1.15f;
|
||||||
|
|
||||||
|
/// <summary>Falloff Y axis ratio. Reference: <c>ConfigManager.LEGACY_AXIS_Y = 0.90f</c>.</summary>
|
||||||
|
public float IslandAxisY = 0.90f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multiplier on the falloff term in the combine.
|
||||||
|
/// Reference: <c>[Export] public float FalloffStrength = 1.0f;</c> (MapGenerator ~:11),
|
||||||
|
/// and NOT overridden in MapPreview.tscn — so 1.0f is the value the island was tuned at.
|
||||||
|
/// </summary>
|
||||||
|
public float FalloffStrength = 1.0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The flat sea level, in raw height units. Reference: <c>ConfigManager.SeaLevelValue = 0.15f</c>
|
||||||
|
/// with <c>SeaLevelModel = "flat"</c>, so <c>GetSeaLevel</c> ignores latitude entirely.
|
||||||
|
///
|
||||||
|
/// ⚠ PHASE 1 USES THIS AS A VISUALIZATION THRESHOLD ONLY — the boundary between the
|
||||||
|
/// bathymetric and hypsometric colour ramps. No water is modelled, no water bodies are
|
||||||
|
/// identified, nothing floods. That is Phase 2.
|
||||||
|
/// </summary>
|
||||||
|
public float SeaLevel = 0.15f;
|
||||||
|
|
||||||
|
// ---- ABLATION TOGGLES — the pass-1 ladder ---------------------------
|
||||||
|
|
||||||
|
/// <summary>Rung 1: the base terrain noise, <c>(noise(x,y)+1)/2</c>. Off = flat zero.</summary>
|
||||||
|
public bool BaseNoise = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rung 2: the island falloff/mask — squircle + ellipse blend, and the <c>Pow(·, 2.5f)</c>.
|
||||||
|
/// ⚠ Off also disables rungs 3–5 in effect: edge noise, the sinker and the Trench are all
|
||||||
|
/// modifiers OF the falloff, so with no falloff there is nothing for them to modify. The
|
||||||
|
/// toggles stay independent so the ladder reads honestly; the report says so.
|
||||||
|
/// </summary>
|
||||||
|
public bool IslandFalloff = true;
|
||||||
|
|
||||||
|
/// <summary>Rung 3: coastline edge roughness, modulated by the squircle.</summary>
|
||||||
|
public bool EdgeNoise = true;
|
||||||
|
|
||||||
|
/// <summary>Rung 4: the southern sinker — extra sinking pressure in the bottom 25%.</summary>
|
||||||
|
public bool SouthernSinker = true;
|
||||||
|
|
||||||
|
/// <summary>Rung 5: the Trench — the map-anchored outer-band wall that guarantees an ocean border.</summary>
|
||||||
|
public bool Trench = true;
|
||||||
|
|
||||||
|
/// <summary>Rung 6: the mountain spine up the centre-X axis.</summary>
|
||||||
|
public bool MountainSpine = true;
|
||||||
|
|
||||||
|
/// <summary>A short label for this variant, used in output filenames. E.g. "full", "base_only".</summary>
|
||||||
|
public string VariantLabel = "full";
|
||||||
|
|
||||||
|
/// <summary>The scale object every distance and frequency in the generator derives from.</summary>
|
||||||
|
public GenerationScale Scale => new GenerationScale(MapSize);
|
||||||
|
|
||||||
|
public TerrainGenConfig Clone() => (TerrainGenConfig)MemberwiseClone();
|
||||||
|
|
||||||
|
public override string ToString() =>
|
||||||
|
$"MapSize={MapSize} Seed={Seed} axis={IslandAxisX:F2}x/{IslandAxisY:F2}y " +
|
||||||
|
$"falloffStrength={FalloffStrength:F2} sea={SeaLevel:F2} variant={VariantLabel} " +
|
||||||
|
$"[base={BaseNoise} falloff={IslandFalloff} edge={EdgeNoise} sinker={SouthernSinker} " +
|
||||||
|
$"trench={Trench} spine={MountainSpine}]";
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/TerrainGenConfig.cs.uid
Normal file
1
Tools/Scripts/TerrainGenConfig.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://da5dt7r145j67
|
||||||
208
Tools/Scripts/TerrainGenTool.cs
Normal file
208
Tools/Scripts/TerrainGenTool.cs
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Godot;
|
||||||
|
using IslaApocalypse.Core;
|
||||||
|
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The Phase-1 generation entry point: runs a seed batch plus the ablation ladder, writes
|
||||||
|
/// hypsometric PNGs and raw height dumps into the batch layout, and quits.
|
||||||
|
///
|
||||||
|
/// ═══ RUNNING IT ═══
|
||||||
|
///
|
||||||
|
/// Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
|
||||||
|
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/TerrainGenTool.tscn
|
||||||
|
///
|
||||||
|
/// ⚠ `--headless` is correct HERE and will stay correct as long as the renderer writes a
|
||||||
|
/// Godot.Image directly. It awaits no render frames. The moment anything composites a `_Draw`
|
||||||
|
/// overlay through a SubViewport, this becomes an `xvfb-run` job — see Tools/README.md.
|
||||||
|
///
|
||||||
|
/// ═══ CONFIGURATION (all optional; every path is env-overridable per the tooling rails) ═══
|
||||||
|
///
|
||||||
|
/// ISLA_MAPSIZE map side in columns (default 2048 — iteration size)
|
||||||
|
/// ISLA_SEEDS comma-separated positive ints (default: the pinned batch below)
|
||||||
|
/// ISLA_BATCH batch folder name (default 01_pass1_port)
|
||||||
|
/// ISLA_OUTPUT_DIR where batches/ lives (Core/ToolingPaths)
|
||||||
|
/// ISLA_SKIP_RAW "1" to skip the .f32 dumps
|
||||||
|
/// ISLA_LADDER "0" to skip the ablation ladder (for a full-size confirmation pass)
|
||||||
|
/// </summary>
|
||||||
|
public partial class TerrainGenTool : Node
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PINNED POSITIVE SEEDS. Pinned, not random, so a batch is reproducible and two batches are
|
||||||
|
/// comparable — a standing convention. Positive only.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly int[] DefaultSeeds = { 1063685222, 20260819, 777001, 424242 };
|
||||||
|
|
||||||
|
private const int DefaultMapSize = 2048;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
ToolingPaths.Configure(OS.GetUserDataDir());
|
||||||
|
|
||||||
|
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
|
||||||
|
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
|
||||||
|
string batch = EnvStr("ISLA_BATCH", "01_pass1_port");
|
||||||
|
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
|
||||||
|
bool ladder = EnvStr("ISLA_LADDER", "1") == "1";
|
||||||
|
|
||||||
|
var scale = new GenerationScale(mapSize);
|
||||||
|
string batchRoot = Path.Combine(ToolingPaths.BatchesRoot, batch);
|
||||||
|
string scratch = ToolingPaths.BatchScratch(batchRoot);
|
||||||
|
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
|
||||||
|
DirAccess.MakeDirRecursiveAbsolute(scratch); // persistent; never cleaned
|
||||||
|
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
GD.Print(" PASS-1 TERRAIN GENERATION — the crown-jewel port (Phase 1)");
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
GD.Print($"MapSize : {mapSize} (scaleFactor {scale.ScaleFactor:F3})");
|
||||||
|
GD.Print($"noise : {TerrainNoise.Describe(0, scale).Replace(" · seed 0", "")}");
|
||||||
|
GD.Print($"seeds : {string.Join(", ", seeds)}");
|
||||||
|
GD.Print($"batch : {batchRoot}");
|
||||||
|
GD.Print("------------------------------------------------------------------");
|
||||||
|
GD.Print(ToolingPaths.Describe());
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
|
||||||
|
var rows = new List<string>();
|
||||||
|
|
||||||
|
// ═══ 1. THE ABLATION LADDER — first seed only, one element added per rung ═══
|
||||||
|
//
|
||||||
|
// This is the port-fidelity check: six simultaneous changes cannot be judged by their
|
||||||
|
// sum, so each element is switched on in the reference's own order and looked at alone.
|
||||||
|
if (ladder)
|
||||||
|
{
|
||||||
|
GD.Print("\n--- ABLATION LADDER (seed " + seeds[0] + ") ---");
|
||||||
|
foreach (var (label, mutate) in Ladder())
|
||||||
|
{
|
||||||
|
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seeds[0], VariantLabel = label };
|
||||||
|
mutate(cfg);
|
||||||
|
rows.Add(RunOne(cfg, batchRoot, skipRaw));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else GD.Print("\n--- ablation ladder skipped (ISLA_LADDER=0) ---");
|
||||||
|
|
||||||
|
// ═══ 2. THE SEED BATCH — the full six-element config across pinned seeds ═══
|
||||||
|
GD.Print("\n--- SEED BATCH (full pass-1) ---");
|
||||||
|
foreach (int seed in seeds)
|
||||||
|
{
|
||||||
|
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" };
|
||||||
|
rows.Add(RunOne(cfg, batchRoot, skipRaw));
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteIndex(batchRoot, mapSize, scale, seeds, rows);
|
||||||
|
|
||||||
|
GD.Print("\n==================================================================");
|
||||||
|
GD.Print($" DONE — {rows.Count} maps in {batchRoot}");
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
GetTree().Quit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ablation rungs, in the reference's execution order. Each rung ADDS one element to the
|
||||||
|
/// one before it, so a difference between adjacent images is attributable to exactly one
|
||||||
|
/// ported element.
|
||||||
|
/// </summary>
|
||||||
|
private static List<(string, Action<TerrainGenConfig>)> Ladder() => new()
|
||||||
|
{
|
||||||
|
("ab1_base_only", c => { c.IslandFalloff = false; c.EdgeNoise = false; c.SouthernSinker = false; c.Trench = false; c.MountainSpine = false; }),
|
||||||
|
("ab2_falloff", c => { c.EdgeNoise = false; c.SouthernSinker = false; c.Trench = false; c.MountainSpine = false; }),
|
||||||
|
("ab3_edge", c => { c.SouthernSinker = false; c.Trench = false; c.MountainSpine = false; }),
|
||||||
|
("ab4_sinker", c => { c.Trench = false; c.MountainSpine = false; }),
|
||||||
|
("ab5_trench", c => { c.MountainSpine = false; }),
|
||||||
|
("ab6_spine_full", c => { /* everything on — identical to the "full" variant */ }),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string RunOne(TerrainGenConfig cfg, string batchRoot, bool skipRaw)
|
||||||
|
{
|
||||||
|
Pass1Result r = Topography.Generate(cfg);
|
||||||
|
|
||||||
|
// batches/NN_<name>/<seed>_<variant>/
|
||||||
|
string dir = Path.Combine(batchRoot, $"{cfg.Seed}_{cfg.VariantLabel}");
|
||||||
|
DirAccess.MakeDirRecursiveAbsolute(dir);
|
||||||
|
|
||||||
|
string png = HeightMapRenderer.SavePng(r, cfg.SeaLevel, Path.Combine(dir, "height.png"));
|
||||||
|
if (!skipRaw) HeightMapRenderer.SaveRaw(r, Path.Combine(dir, "height.f32"));
|
||||||
|
|
||||||
|
float land = r.LandFraction(cfg.SeaLevel);
|
||||||
|
GD.Print($" {cfg.VariantLabel,-16} seed {cfg.Seed,-11} " +
|
||||||
|
$"h[{r.HMinSeed,7:F3} .. {r.HMaxSeed,6:F3}] land {land * 100,5:F1}% {r.ElapsedMs,5} ms");
|
||||||
|
|
||||||
|
return $"| `{cfg.Seed}_{cfg.VariantLabel}` | {cfg.Seed} | {cfg.VariantLabel} | " +
|
||||||
|
$"{r.HMinSeed:F3} | {r.HMaxSeed:F3} | {land * 100:F1}% | {r.ElapsedMs} ms |";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteIndex(string batchRoot, int mapSize, GenerationScale scale, int[] seeds, List<string> rows)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("# Batch — pass-1 port (Phase 1)");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Raw pass-1 terrain from the ported crown-jewel noise. **No curve, no erosion, no");
|
||||||
|
sb.AppendLine("rivers, no water, no crater, no biomes.** Each folder holds `height.png` (hypsometric)");
|
||||||
|
sb.AppendLine("and `height.f32` (raw float32, x-major, for byte-level comparison).");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"- **MapSize:** {mapSize} (scaleFactor {scale.ScaleFactor:F3})");
|
||||||
|
sb.AppendLine($"- **Noise:** {TerrainNoise.Describe(0, scale).Replace(" · seed 0", "")}");
|
||||||
|
sb.AppendLine($"- **Sea threshold (visualization only):** 0.15");
|
||||||
|
sb.AppendLine($"- **Colour ramp anchors (FIXED across the batch):** sea {HeightMapRenderer.RampBottomHeight} .. 0.15, land 0.15 .. {HeightMapRenderer.RampTopHeight}");
|
||||||
|
sb.AppendLine($"- **Seeds:** {string.Join(", ", seeds)}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## What to look at");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("**The ablation ladder** (`ab1`..`ab6`, all on seed " + seeds[0] + ") adds one ported element per");
|
||||||
|
sb.AppendLine("rung, in the reference's execution order — so any difference between adjacent images is");
|
||||||
|
sb.AppendLine("attributable to exactly one element:");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Rung | Adds | Expect to see |");
|
||||||
|
sb.AppendLine("|---|---|---|");
|
||||||
|
sb.AppendLine("| `ab1_base_only` | base noise | featureless fractal field, no island, no coast |");
|
||||||
|
sb.AppendLine("| `ab2_falloff` | island mask + `Pow(·,2.5)` | an island appears, smooth-edged |");
|
||||||
|
sb.AppendLine("| `ab3_edge` | edge roughness | the coastline goes jagged — **and only the coastline** |");
|
||||||
|
sb.AppendLine("| `ab4_sinker` | southern sinker | the bottom 25% sinks; southern land bridges break up |");
|
||||||
|
sb.AppendLine("| `ab5_trench` | the Trench | a hard ocean border on all four edges |");
|
||||||
|
sb.AppendLine("| `ab6_spine_full` | mountain spine | a ridge up the centre, fading out toward the south |");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("**The seed batch** (`<seed>_full`) is the complete six-element pass-1 across pinned seeds.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("> ⚠ **The seabed reads raw, and that is expected.** The submarine coast shelf and the");
|
||||||
|
sb.AppendLine("> offshore islets are DEFERRED to Phase 2 — they act only below sea level and are judged");
|
||||||
|
sb.AppendLine("> once water renders. Steep, plain bathymetry here is not a bug.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## Results");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Folder | Seed | Variant | h min | h max | land % | time |");
|
||||||
|
sb.AppendLine("|---|---|---|---|---|---|---|");
|
||||||
|
foreach (string row in rows) sb.AppendLine(row);
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("`scratch/` is persistent and is never cleaned.");
|
||||||
|
|
||||||
|
string index = Path.Combine(batchRoot, "INDEX.md");
|
||||||
|
using var f = Godot.FileAccess.Open(index, Godot.FileAccess.ModeFlags.Write);
|
||||||
|
if (f == null) { GD.PrintErr($"could not write {index}"); return; }
|
||||||
|
f.StoreString(sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- env helpers ----------------------------------------------------
|
||||||
|
|
||||||
|
private static string EnvStr(string k, string fallback)
|
||||||
|
{
|
||||||
|
string v = System.Environment.GetEnvironmentVariable(k);
|
||||||
|
return string.IsNullOrWhiteSpace(v) ? fallback : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int EnvInt(string k, int fallback)
|
||||||
|
=> int.TryParse(EnvStr(k, null) ?? "", out int v) ? v : fallback;
|
||||||
|
|
||||||
|
private static int[] EnvSeeds(string k, int[] fallback)
|
||||||
|
{
|
||||||
|
string v = EnvStr(k, null);
|
||||||
|
if (v == null) return fallback;
|
||||||
|
var outp = new List<int>();
|
||||||
|
foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
if (int.TryParse(part.Trim(), out int s) && s > 0) outp.Add(s);
|
||||||
|
return outp.Count > 0 ? outp.ToArray() : fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/TerrainGenTool.cs.uid
Normal file
1
Tools/Scripts/TerrainGenTool.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://e2oef4cf0vsl
|
||||||
93
Tools/Scripts/TerrainNoise.cs
Normal file
93
Tools/Scripts/TerrainNoise.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
using Godot;
|
||||||
|
using IslaApocalypse.Core;
|
||||||
|
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐⭐ THE CROWN JEWEL — the terrain noise configuration, ported and PINNED.
|
||||||
|
/// → D-050 ("port, don't re-derive"), `Design - Rewrite - Extraction Manifest.md`.
|
||||||
|
///
|
||||||
|
/// ═══ WHAT THE REFERENCE ACTUALLY SET, AND WHAT IT DID NOT ═══
|
||||||
|
///
|
||||||
|
/// The reference (MapGenerator._Ready ~:141-144) set exactly THREE properties:
|
||||||
|
///
|
||||||
|
/// _noise = new FastNoiseLite();
|
||||||
|
/// _noise.Seed = ConfigManager.WorldSeed == 0 ? (int)GD.Randi() : ConfigManager.WorldSeed;
|
||||||
|
/// _noise.NoiseType = FastNoiseLite.NoiseTypeEnum.Simplex;
|
||||||
|
/// _noise.Frequency = 0.004f / scaleFactor;
|
||||||
|
///
|
||||||
|
/// That is all four lines. There is no fifth. Fractal type, octaves, gain, lacunarity and
|
||||||
|
/// weighted-strength were NEVER SET anywhere in that codebase — so the island's entire fractal
|
||||||
|
/// character was Godot 4.7.1's `FastNoiseLite` CONSTRUCTOR DEFAULTS, and nothing recorded what
|
||||||
|
/// they were. (chat1/00 §2.1, §4.1.)
|
||||||
|
///
|
||||||
|
/// ═══ ⚠⚠ WHY EVERY VALUE IS PINNED EXPLICITLY BELOW ═══
|
||||||
|
///
|
||||||
|
/// Because a default is not a decision, and this project no longer rides one. The terrain must
|
||||||
|
/// not change when the engine version changes, and it must not change when this code is ported
|
||||||
|
/// off Godot's FastNoiseLite to a C++ or hand-rolled implementation — which D-049 explicitly
|
||||||
|
/// anticipates ("C++-ready seams... port path-by-path when settled and hot"). An unpinned
|
||||||
|
/// default would move the island silently, and nothing in the source would say what moved.
|
||||||
|
///
|
||||||
|
/// Measured on Godot 4.7.2.stable.mono (Tools/Scenes/NoiseDefaultsProbe.tscn):
|
||||||
|
/// FractalType Fbm · Octaves 5 · Gain 0.5 · Lacunarity 2.0 · WeightedStrength 0.0
|
||||||
|
/// These MATCH the Godot 4 documented defaults the reference rode on 4.7.1 — so pinning them
|
||||||
|
/// reproduces the reference look exactly, with no delta to reconcile.
|
||||||
|
///
|
||||||
|
/// ⚠ Note NoiseType is the one place the reference did NOT ride the default: the constructor
|
||||||
|
/// default is SimplexSmooth, and the reference explicitly chose Simplex. Different noise.
|
||||||
|
///
|
||||||
|
/// Run NoiseDefaultsProbe after any engine upgrade. It cannot change the terrain — this file
|
||||||
|
/// pins it — but it makes a drifting default visible instead of silent.
|
||||||
|
/// </summary>
|
||||||
|
public static class TerrainNoise
|
||||||
|
{
|
||||||
|
// ---- the pinned fractal character (was 4.7.1 constructor defaults) ----
|
||||||
|
public const FastNoiseLite.NoiseTypeEnum PinnedNoiseType = FastNoiseLite.NoiseTypeEnum.Simplex;
|
||||||
|
public const FastNoiseLite.FractalTypeEnum PinnedFractalType = FastNoiseLite.FractalTypeEnum.Fbm;
|
||||||
|
public const int PinnedOctaves = 5;
|
||||||
|
public const float PinnedGain = 0.5f;
|
||||||
|
public const float PinnedLacunarity = 2.0f;
|
||||||
|
public const float PinnedWeightedStrength = 0.0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The base frequency, stated at the 1024 baseline. The reference wrote
|
||||||
|
/// <c>0.004f / scaleFactor</c>; here the division is <see cref="GenerationScale.NoiseFrequency"/>'s
|
||||||
|
/// job, so the literal never reaches a call site. → `Design - Tooling - Scaling Discipline.md`.
|
||||||
|
/// </summary>
|
||||||
|
public const float BaselineFrequency = 0.004f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build the one terrain noise field. ⚠ ONE INSTANCE, deliberately.
|
||||||
|
///
|
||||||
|
/// The reference sampled the latitude wobble, the coastline edge roughness and the base
|
||||||
|
/// height from the SAME `_noise` object — three correlated slices of one field, at three
|
||||||
|
/// different coordinate transforms. That correlation is part of the look, so the port keeps
|
||||||
|
/// one instance rather than "tidying" it into three seeded fields.
|
||||||
|
/// </summary>
|
||||||
|
public static FastNoiseLite Create(int seed, GenerationScale scale)
|
||||||
|
{
|
||||||
|
var noise = new FastNoiseLite();
|
||||||
|
|
||||||
|
// --- ported verbatim from the reference ---
|
||||||
|
noise.Seed = seed;
|
||||||
|
noise.NoiseType = PinnedNoiseType;
|
||||||
|
noise.Frequency = scale.NoiseFrequency(BaselineFrequency); // = 0.004f / scaleFactor
|
||||||
|
|
||||||
|
// --- PINNED: the reference left these to the engine. We do not. ---
|
||||||
|
noise.FractalType = PinnedFractalType;
|
||||||
|
noise.FractalOctaves = PinnedOctaves;
|
||||||
|
noise.FractalGain = PinnedGain;
|
||||||
|
noise.FractalLacunarity = PinnedLacunarity;
|
||||||
|
noise.FractalWeightedStrength = PinnedWeightedStrength;
|
||||||
|
|
||||||
|
return noise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The pinned configuration as one line, for a run header.</summary>
|
||||||
|
public static string Describe(int seed, GenerationScale scale) =>
|
||||||
|
$"Simplex · Fbm · octaves {PinnedOctaves} · gain {PinnedGain} · lacunarity {PinnedLacunarity} · " +
|
||||||
|
$"weighted {PinnedWeightedStrength} · freq {scale.NoiseFrequency(BaselineFrequency):G6} " +
|
||||||
|
$"(= {BaselineFrequency} / {scale.ScaleFactor:F3}) · seed {seed}";
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/TerrainNoise.cs.uid
Normal file
1
Tools/Scripts/TerrainNoise.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bvyldfs65ouuh
|
||||||
292
Tools/Scripts/Topography.cs
Normal file
292
Tools/Scripts/Topography.cs
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
using Godot;
|
||||||
|
using IslaApocalypse.Core;
|
||||||
|
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐⭐ PASS 1 — the raw island height field. THE CROWN-JEWEL PORT.
|
||||||
|
/// → D-050 ("port, don't re-derive"), `Design - Rewrite - Extraction Manifest.md`.
|
||||||
|
///
|
||||||
|
/// Ported from the reference's <c>MapGenerator.GenerateTopography</c> pass-1 loop
|
||||||
|
/// (<c>Tools/Scripts/MapGenerator.cs</c> ~:553-619 at tag <c>pre-rewrite-reference</c>,
|
||||||
|
/// commit <c>ab78883</c>). Constants are READ FROM THAT SOURCE, not re-derived from a design
|
||||||
|
/// summary — that distinction is the whole point of D-050.
|
||||||
|
///
|
||||||
|
/// ═══ THE SIX ELEMENTS, IN THE REFERENCE'S EXECUTION ORDER ═══
|
||||||
|
///
|
||||||
|
/// 1. wobbled latitude scalar (ref ~:559-562)
|
||||||
|
/// 2. island falloff / mask (ref ~:567-574, 593)
|
||||||
|
/// 3. edge / coastline roughness (ref ~:577-578)
|
||||||
|
/// 4. southern sinker (ref ~:582-587)
|
||||||
|
/// 5. the Trench (ref ~:595-598)
|
||||||
|
/// 6. mountain spine (ref ~:605-612)
|
||||||
|
/// → combine and write (ref ~:618-619, 665-666)
|
||||||
|
///
|
||||||
|
/// ⚠⚠ THE ORDER IS LOAD-BEARING AND IS NOT AN IMPLEMENTATION DETAIL.
|
||||||
|
/// The sinker lands BEFORE the power, so its effect is superlinear. The Trench lands AFTER it,
|
||||||
|
/// so it is a raw additive wall the exponent never softens. preTrenchFalloff is captured
|
||||||
|
/// between them. Reordering any of it changes the island.
|
||||||
|
///
|
||||||
|
/// ═══ ⚠ WHAT IS DELIBERATELY NOT PORTED HERE ═══
|
||||||
|
///
|
||||||
|
/// The reference's pass-1 loop continues past the height write with two more task-11 passes:
|
||||||
|
/// the submarine COAST SHELF (~:621-640) and the OFFSHORE ISLET layer (~:641-664). Both are
|
||||||
|
/// DEFERRED to Phase 2 by the developer's ruling — they act only on below-sea height and are
|
||||||
|
/// judged once water renders. <see cref="Pass1Result.PreTrenchFalloff"/> is exposed for them.
|
||||||
|
///
|
||||||
|
/// Nothing from pass 2 is here at all: no redistribution curve, no shelf detail, no erosion,
|
||||||
|
/// no rivers, no water bodies, no crater carve, no biomes.
|
||||||
|
/// </summary>
|
||||||
|
public static class Topography
|
||||||
|
{
|
||||||
|
// ═══ CONSTANTS, ALL READ FROM THE REFERENCE SOURCE ═══
|
||||||
|
// Named rather than inlined so each one is greppable and has a home for its reason.
|
||||||
|
|
||||||
|
/// <summary>The squircle/ellipse blend weight. Reference: <c>Mathf.Lerp(ellipse, squircle, 0.5f)</c> (~:574).</summary>
|
||||||
|
private const float BlendSquircleWeight = 0.5f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ellipse's scale divisor: <c>ellipticalPos.Length() / (MapSize / 1.3f)</c> (~:572).
|
||||||
|
///
|
||||||
|
/// ⚠ AN UNCOMMENTED SHAPE DIAL IN THE REFERENCE, PORTED AS ONE. It sets the ellipse's
|
||||||
|
/// absolute scale against the squircle's, so it moves the coastline — but the reference
|
||||||
|
/// carried no comment and the vault's "50/50 blend of two falloffs" description does not
|
||||||
|
/// expose it. Ported verbatim because the island was tuned with it; named here so the next
|
||||||
|
/// reader knows it is a dial and not arithmetic. (chat1/00 §2.2.)
|
||||||
|
/// </summary>
|
||||||
|
private const float EllipseScaleDivisor = 1.3f;
|
||||||
|
|
||||||
|
/// <summary>Edge-noise coordinate multiplier. Reference: <c>GetNoise2D(x * 2.5f, y * 2.5f)</c> (~:577).</summary>
|
||||||
|
private const float EdgeNoiseCoordScale = 2.5f;
|
||||||
|
|
||||||
|
/// <summary>Edge-noise amplitude. Reference: <c>finalFalloff += (edgeNoise * 0.15f) * squircleFalloff</c> (~:578).</summary>
|
||||||
|
private const float EdgeNoiseAmplitude = 0.15f;
|
||||||
|
|
||||||
|
/// <summary>Southern sinker threshold, as a fraction of the map. Reference: <c>MapSize * 0.75f</c> (~:582).</summary>
|
||||||
|
private const float SouthThresholdFraction = 0.75f;
|
||||||
|
|
||||||
|
/// <summary>Southern sinker depth. Reference: <c>finalFalloff += southDepth * 0.6f</c> (~:586).</summary>
|
||||||
|
private const float SouthSinkAmount = 0.6f;
|
||||||
|
|
||||||
|
/// <summary>The falloff exponent. Reference: <c>Mathf.Pow(finalFalloff, 2.5f)</c> (~:593).</summary>
|
||||||
|
private const float FalloffExponent = 2.5f;
|
||||||
|
|
||||||
|
/// <summary>Trench onset, as a fraction of the half-axis. Reference: <c>if (distX > 0.90f)</c> (~:597).</summary>
|
||||||
|
private const float TrenchOnset = 0.90f;
|
||||||
|
|
||||||
|
/// <summary>Trench slope. Reference: <c>finalFalloff += (distX - 0.90f) * 15.0f</c> (~:597-598).</summary>
|
||||||
|
private const float TrenchSlope = 15.0f;
|
||||||
|
|
||||||
|
/// <summary>Spine gate / fade anchor. Reference: <c>if (temperature < 0.65f)</c> and <c>(0.65f - temperature)</c> (~:606, 610).</summary>
|
||||||
|
private const float SpineLatitudeAnchor = 0.65f;
|
||||||
|
|
||||||
|
/// <summary>Spine southern-fade slope. Reference: <c>Clamp((0.65f - temperature) * 4.0f, 0, 1)</c> (~:610).</summary>
|
||||||
|
private const float SpineFadeSlope = 4.0f;
|
||||||
|
|
||||||
|
/// <summary>Spine cubic concentration. Reference: <c>Mathf.Pow(mountainSpine, 3.0f)</c> (~:611).</summary>
|
||||||
|
private const float SpineConcentration = 3.0f;
|
||||||
|
|
||||||
|
/// <summary>Spine amplitude in raw height units. Reference: <c>* 0.6f</c> (~:611).</summary>
|
||||||
|
private const float SpineAmplitude = 0.6f;
|
||||||
|
|
||||||
|
/// <summary>Latitude wobble amplitude. Reference: <c>temperature += (tempNoise * 0.2f) - 0.1f</c> (~:561) — i.e. ±0.1.</summary>
|
||||||
|
private const float LatitudeWobbleSpan = 0.2f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The latitude noise's decorrelation offset, IN MAP WIDTHS.
|
||||||
|
///
|
||||||
|
/// ⚠⚠ THE ONE PLACE THIS PORT DELIBERATELY DOES NOT COPY THE REFERENCE'S LITERAL.
|
||||||
|
///
|
||||||
|
/// The reference wrote <c>GetNoise2D(x + 1000, y + 1000)</c> — an offset in RAW PIXELS.
|
||||||
|
/// Because frequency already carries a 1/MapSize normalization, a pixel offset does not hold
|
||||||
|
/// still across map sizes: 1000 px is 1.0 normalized units at 4K but 0.5 at 8K, so a resize
|
||||||
|
/// silently samples a DIFFERENT SLICE of the noise field. Same feature scale, different
|
||||||
|
/// realization — the same seed gives a different world at a different size, for no reason
|
||||||
|
/// anyone wrote down. (chat1/00 §4.2; the §6 tightening carried in from chat1/01.)
|
||||||
|
///
|
||||||
|
/// 1000 px at the reference's canonical MapSize of 8192 = 0.1220703125 map widths. Pinning
|
||||||
|
/// THAT reproduces the reference's realization exactly at 8K, and holds it still everywhere
|
||||||
|
/// else — which the reference did not.
|
||||||
|
/// </summary>
|
||||||
|
private const float LatitudeNoiseOffsetMapWidths = 1000f / 8192f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate the raw pass-1 height field.
|
||||||
|
/// </summary>
|
||||||
|
public static Pass1Result Generate(TerrainGenConfig cfg)
|
||||||
|
{
|
||||||
|
ulong t0 = Time.GetTicksMsec();
|
||||||
|
|
||||||
|
GenerationScale scale = cfg.Scale;
|
||||||
|
int mapSize = cfg.MapSize;
|
||||||
|
FastNoiseLite noise = TerrainNoise.Create(cfg.Seed, scale);
|
||||||
|
|
||||||
|
var height = new float[mapSize, mapSize];
|
||||||
|
var preTrenchFalloff = new float[mapSize, mapSize];
|
||||||
|
var latitudeField = new float[mapSize, mapSize];
|
||||||
|
|
||||||
|
// ⚠ MAP-CENTRED, not crater-centred. Reference ~:550:
|
||||||
|
// Vector2 center = new Vector2(MapSize / 2.0f, MapSize / 2.0f);
|
||||||
|
// The crater's centre (_impactCenter) is a separate, randomly placed field used only in
|
||||||
|
// pass 2 and the biome pass. The spine and the mask both key off the MAP centre — the
|
||||||
|
// spine is a map-centred ridge, per `Design - Terrain - Mountain Spine.md`. Confirmed
|
||||||
|
// against source; no crater coupling exists in pass 1.
|
||||||
|
float centerX = mapSize / 2.0f;
|
||||||
|
float centerY = mapSize / 2.0f;
|
||||||
|
|
||||||
|
float axisX = cfg.IslandAxisX;
|
||||||
|
float axisY = cfg.IslandAxisY;
|
||||||
|
|
||||||
|
// Precomputed once — the reference recomputed these per pixel; identical results.
|
||||||
|
float latitudeNoiseOffset = scale.OffsetInMapWidths(LatitudeNoiseOffsetMapWidths);
|
||||||
|
float southThreshold = scale.Fraction(SouthThresholdFraction);
|
||||||
|
float halfSpan = mapSize / 2.0f;
|
||||||
|
|
||||||
|
float hMax = float.MinValue;
|
||||||
|
float hMin = float.MaxValue;
|
||||||
|
|
||||||
|
// ⚠ x IS THE OUTER LOOP, as in the reference. Numerically irrelevant here, but a
|
||||||
|
// PARALLEL port must reduce hMax/hMin rather than share them — noted before someone
|
||||||
|
// reaches for Parallel.For and quietly races on the running max.
|
||||||
|
for (int x = 0; x < mapSize; x++)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < mapSize; y++)
|
||||||
|
{
|
||||||
|
// ═══ 1. WOBBLED LATITUDE SCALAR (ref ~:559-562) ═══
|
||||||
|
//
|
||||||
|
// ⚠⚠ THE REFERENCE CALLED THIS "temperature". IT IS NOT CLIMATE, AND THIS PORT
|
||||||
|
// WILL NOT CALL IT THAT.
|
||||||
|
//
|
||||||
|
// It is a stage-1-local LATITUDE FIELD that terrain GEOMETRY reads — the spine's
|
||||||
|
// southern fade is its only pass-1 consumer. The prototype named it temperature,
|
||||||
|
// then let biomes read the same array, and that shared name is a large part of
|
||||||
|
// how climate and terrain got fused in the first place. In the rewrite,
|
||||||
|
// CLIMATE IS STAGE 3 and classifies finished shape (D-049 §2, D-056); this
|
||||||
|
// scalar is stage 1 and must never be stored as, aliased to, or mistaken for it.
|
||||||
|
//
|
||||||
|
// The ±0.1 WOBBLE IS KEPT, and it is load-bearing: it makes the spine's southern
|
||||||
|
// terminus a low-frequency wave rather than a ruler-straight latitude line.
|
||||||
|
// Replacing this with a bare y/MapSize would straighten it — the exact opposite
|
||||||
|
// of the filed spine-meander want. (chat1/00 §2.3.)
|
||||||
|
float lat = (float)y / mapSize;
|
||||||
|
float latNoise = (noise.GetNoise2D(x + latitudeNoiseOffset, y + latitudeNoiseOffset) + 1.0f) / 2.0f;
|
||||||
|
lat += (latNoise * LatitudeWobbleSpan) - (LatitudeWobbleSpan / 2.0f);
|
||||||
|
latitudeField[x, y] = lat;
|
||||||
|
|
||||||
|
// ═══ 2. THE ISLAND FALLOFF / MASK (ref ~:567-574) ═══
|
||||||
|
float finalFalloff = 0f;
|
||||||
|
float squircleFalloff = 0f;
|
||||||
|
|
||||||
|
if (cfg.IslandFalloff)
|
||||||
|
{
|
||||||
|
// Squircle — Max(nx, ny), giving squared-off corners. ISLAND-anchored:
|
||||||
|
// the axis ratios are applied here.
|
||||||
|
float nx = Mathf.Abs(x - centerX) / (halfSpan * axisX);
|
||||||
|
float ny = Mathf.Abs(y - centerY) / (halfSpan * axisY);
|
||||||
|
squircleFalloff = Mathf.Max(nx, ny);
|
||||||
|
|
||||||
|
// Ellipse — vector length, giving a rounded shape.
|
||||||
|
var ellipticalPos = new Vector2((x - centerX) / axisX, (y - centerY) / axisY);
|
||||||
|
float ellipticalFalloff = ellipticalPos.Length() / (mapSize / EllipseScaleDivisor);
|
||||||
|
|
||||||
|
// 50/50 blend.
|
||||||
|
finalFalloff = Mathf.Lerp(ellipticalFalloff, squircleFalloff, BlendSquircleWeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ 3. EDGE / COASTLINE ROUGHNESS (ref ~:577-578) ═══
|
||||||
|
//
|
||||||
|
// ⚠ THE `* squircleFalloff` MODULATION IS PART OF IT AND IS EASY TO DROP.
|
||||||
|
// It makes the roughness ZERO AT THE ISLAND CENTRE and strongest at the rim —
|
||||||
|
// which is why it reads as coastline jitter instead of interior grain. The
|
||||||
|
// vault's description of this element omits it. (chat1/00 §2.2, §4.5.)
|
||||||
|
//
|
||||||
|
// The x2.5 coordinate multiplier is NOT an unscaled quantity: it multiplies
|
||||||
|
// coordinates that the already-normalized frequency then scales, so it is a
|
||||||
|
// fixed 2.5x RATIO to the base noise at every map size. (chat1/00 §4.2 —
|
||||||
|
// which corrected the vault on exactly this point.)
|
||||||
|
if (cfg.IslandFalloff && cfg.EdgeNoise)
|
||||||
|
{
|
||||||
|
float edgeNoise = (noise.GetNoise2D(x * EdgeNoiseCoordScale, y * EdgeNoiseCoordScale) + 1.0f) / 2.0f;
|
||||||
|
finalFalloff += (edgeNoise * EdgeNoiseAmplitude) * squircleFalloff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ 4. THE SOUTHERN SINKER (ref ~:582-587) ═══
|
||||||
|
//
|
||||||
|
// Sinks the stretched land bridges in the bottom 25%. ⚠ BEFORE the power, so its
|
||||||
|
// effect is superlinear — +0.6 on a falloff already near 1 costs far more height
|
||||||
|
// than +0.6 near 0. Moving it after the power would change the southern coast.
|
||||||
|
if (cfg.IslandFalloff && cfg.SouthernSinker && y > southThreshold)
|
||||||
|
{
|
||||||
|
float southDepth = (y - southThreshold) / (mapSize - southThreshold);
|
||||||
|
finalFalloff += southDepth * SouthSinkAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ ⭐ THE PHASE-2 SEAM — captured BEFORE the power and BEFORE the Trench ═══
|
||||||
|
// (ref ~:591). See Pass1Result.PreTrenchFalloff for why this exact point.
|
||||||
|
preTrenchFalloff[x, y] = finalFalloff;
|
||||||
|
|
||||||
|
if (cfg.IslandFalloff)
|
||||||
|
finalFalloff = Mathf.Pow(finalFalloff, FalloffExponent);
|
||||||
|
|
||||||
|
// ═══ 5. THE TRENCH (ref ~:595-598) ═══
|
||||||
|
//
|
||||||
|
// ⚠⚠ MAP-ANCHORED, NOT ISLAND-ANCHORED — distX/distY carry NO axis ratio, unlike
|
||||||
|
// nx/ny above. That is deliberate and load-bearing: the Trench is a guarantee
|
||||||
|
// about the MAP BORDER, not about the island's ellipse. It is what makes the
|
||||||
|
// flood fill's unchecked (0,0) ocean seed safe — the corner is guaranteed
|
||||||
|
// underwater by a x15 wall. DO NOT "unify" it with the mask's normalization.
|
||||||
|
// → `Design - Terrain - Island Mask.md`, chat1/00 §4.5.
|
||||||
|
//
|
||||||
|
// ⚠ The two clauses are ADDITIVE — a corner pixel takes both.
|
||||||
|
// ⚠ AFTER the power, so it is a raw additive wall the exponent never softens.
|
||||||
|
if (cfg.IslandFalloff && cfg.Trench)
|
||||||
|
{
|
||||||
|
float distX = Mathf.Abs(x - centerX) / halfSpan;
|
||||||
|
float distY = Mathf.Abs(y - centerY) / halfSpan;
|
||||||
|
if (distX > TrenchOnset) finalFalloff += (distX - TrenchOnset) * TrenchSlope;
|
||||||
|
if (distY > TrenchOnset) finalFalloff += (distY - TrenchOnset) * TrenchSlope;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ 6. THE MOUNTAIN SPINE (ref ~:605-612) ═══
|
||||||
|
//
|
||||||
|
// ⚠ THE REFERENCE'S `if (temperature < 0.65f)` GATE IS DROPPED, AND THE RESULT IS
|
||||||
|
// BIT-IDENTICAL. southernFade = Clamp((0.65 - lat) * 4, 0, 1) already reaches
|
||||||
|
// EXACTLY ZERO at lat = 0.65 — the same constant the gate tested — so the gate
|
||||||
|
// guarded a term that had already faded to nothing. It introduced no
|
||||||
|
// discontinuity and skipped no visible work. southernFade, not the gate, is what
|
||||||
|
// actually shapes the spine's southern end. (chat1/00 §2.3.)
|
||||||
|
//
|
||||||
|
// The gate is expressed below as `fade > 0`, which is the same predicate stated
|
||||||
|
// where it is true rather than where it is incidental.
|
||||||
|
float mountainSpine = 0f;
|
||||||
|
if (cfg.MountainSpine)
|
||||||
|
{
|
||||||
|
float southernFade = Mathf.Clamp((SpineLatitudeAnchor - lat) * SpineFadeSlope, 0.0f, 1.0f);
|
||||||
|
if (southernFade > 0f)
|
||||||
|
{
|
||||||
|
// ISLAND-anchored: axisX applies here (ref ~:608).
|
||||||
|
float distanceToCenterX = Mathf.Abs(x - centerX) / (halfSpan * axisX);
|
||||||
|
float crest = 1.0f - IslandFalloff.SmoothAbs(distanceToCenterX, IslandFalloff.CREST_EPSILON);
|
||||||
|
mountainSpine = Mathf.Pow(crest, SpineConcentration) * southernFade * SpineAmplitude;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ COMBINE AND WRITE (ref ~:618-619, 665-666) ═══
|
||||||
|
float rawBase = cfg.BaseNoise ? (noise.GetNoise2D(x, y) + 1.0f) / 2.0f : 0f;
|
||||||
|
float finalH = rawBase + mountainSpine - (finalFalloff * cfg.FalloffStrength);
|
||||||
|
|
||||||
|
if (finalH > hMax) hMax = finalH;
|
||||||
|
if (finalH < hMin) hMin = finalH;
|
||||||
|
height[x, y] = finalH;
|
||||||
|
|
||||||
|
// ⚠ THE REFERENCE'S PASS 1 CONTINUES HERE with the coast shelf (~:621-640) and
|
||||||
|
// the offshore islets (~:641-664). Both DEFERRED to Phase 2 — below-sea only,
|
||||||
|
// judged once water renders. preTrenchFalloff above is their inlet.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Pass1Result(mapSize, cfg.Seed, height, preTrenchFalloff, latitudeField,
|
||||||
|
hMax, hMin, Time.GetTicksMsec() - t0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/Topography.cs.uid
Normal file
1
Tools/Scripts/Topography.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cwaxsloo2ncr8
|
||||||
Loading…
Reference in a new issue