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.
292 lines
14 KiB
C#
292 lines
14 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|