Ports the reference's v5 height curve and shelf-detail passes onto Phase 1's shape and re-calibrates them against this repo's actual pass-1 distribution. This is the BASELINE the reshape gets judged against, not the reshape. Core (engine-free, D-060): - WorldScale — THE vertical yardstick. One metres/raw number (251), replacing the prototype's three duplicate M_PER_UNIT constants and ~20 bare literals. The chunk-height coupling it had there is recorded as a DEFERRED vault decision, not inherited. RawFromMetres divides, matching the reference bit-for-bit. - HeightCurve — the 7 bands, the frozen corner-fix blends, the per-seed spike normalization, the 24-corner monotonicity sweep that throws and refuses. Identity at and below sea, which everything downstream rests on. - CurveKnots / CurveAnchors — input knots (measured percentiles) and output anchors (storm ladder) split apart and both made parameters, so the anchors are A/B-able without editing source. The reference's shipped knots are kept beside the measured ones as the fidelity yardstick. - TerrainDetailPass — micro-relief skin plus the shelf-edge KNOT warp (which slides K3/K4/K5, not height — that is what keeps monotonicity structural). The crater exclusion is ported and inert until the carve lands. Tools: - Shaping — pass 2a, producing the two height fields. classify is bit-for-bit the raw pass-1 field; render is curved and detailed. Aliased when the curve is off, as the reference did. Pass1Result is left immutable so the oracle can compare. - LandHistogram — the calibration engine AND the diagnostic. The reference shipped six knot literals and threw the measuring instrument away; this rebuilds it. - ShapingOracle + CurveBaselineTool — four automatic checks before anything is looked at, and the batch that runs them. Measured, not assumed: - Knots re-measured over a 6-seed / 12.8M-sample pool. They differ from the reference's by at most 5.6 m of world height, against a 44.7 m per-seed spread — the pass-1 port is faithful. - Oracle all pass, including pass 1 bit-identical to Phase 1's own .f32 dump. - Band shares land on 60/13/10/5/8/3/1 to 0.00 pp. - Knots hold across map size: the 8K delta (5.8 m) sits inside seed noise. The finding the histograms deliver: 83% of land ends below 100 m and 96% below 220 m, with the median column at 13 m. That is the share targets doing exactly what they say, not a bug — and it is the developer's call, which is why nothing here reshapes it and the palette was deliberately left mis-fitted rather than recalibrated to disguise it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
182 lines
9.4 KiB
C#
182 lines
9.4 KiB
C#
using System;
|
||
|
||
namespace IslaApocalypse.Core
|
||
{
|
||
/// <summary>
|
||
/// The terrain DETAIL passes — the two things that make a redistributed shelf read as ground
|
||
/// rather than as a terrace. Ported from <c>REFERENCE:Tools/Scripts/TerrainDetailPass.cs</c>
|
||
/// (v1) at tag <c>pre-rewrite-reference</c> (<c>ab78883</c>).
|
||
///
|
||
/// ═══ TWO PASSES, AND THEY DO DIFFERENT KINDS OF THING ═══
|
||
///
|
||
/// PASS A — SHELF MICRO-RELIEF. A medium-frequency noise skin (±3 m by default) added to the
|
||
/// OUTPUT height, weighted by shelf-ness. The curve compresses the shelves flat; this gives
|
||
/// them their rolling texture back. Risers and peaks are untouched by construction.
|
||
///
|
||
/// PASS B — SHELF-EDGE VARIATION. A per-column shift of the shelf/riser KNOT BLOCK (K3/K4/K5)
|
||
/// by a low-frequency field.
|
||
///
|
||
/// > ### ⚠⚠ PASS B IS NOT HEIGHT PERTURBATION, AND THE DIFFERENCE IS THE WHOLE POINT.
|
||
/// >
|
||
/// > Pass A moves a column's OUTPUT HEIGHT. Pass B moves WHERE THE BANDS ARE for that column.
|
||
/// >
|
||
/// > Every shelf↔riser boundary is the contour where the raw height crosses one of K3/K4/K5. Slide
|
||
/// > those knots per column and the contour stops tracing a clean iso-height line: the shelf edge
|
||
/// > scallops into coves, notches and peninsulas. Do it by perturbing height instead and you get
|
||
/// > a fuzzy terrace edge, not an organic one — and you lose monotonicity as a STRUCTURAL
|
||
/// > property, because the curve is monotonic for any ordered knot set but nothing is monotonic
|
||
/// > after arbitrary additive noise.
|
||
/// >
|
||
/// > The amplitude is therefore stated in METRES OF INPUT HEIGHT (raw × the yardstick): how far a
|
||
/// > boundary contour is displaced in raw-height terms, NOT an output elevation change. What the
|
||
/// > eye sees is the LATERAL wander — that displacement divided by the local raw gradient.
|
||
/// > Measured on the reference (seed 1375359975): |∇raw| at the K3/K4/K5 contours is p50 0.00088
|
||
/// > raw/px, so 12 m of input height buys a median peak displacement of ~54 px and a mean of ~8 px
|
||
/// > along the boundary. 5 m — the first attempt — moved it a mean 3.7 px and was invisible at
|
||
/// > map scale.
|
||
///
|
||
/// ═══ ⚠ OUTPUT-HEIGHT ONLY. THE CLASSIFY FIELD NEVER SEES EITHER PASS. ═══
|
||
///
|
||
/// Both are consumers of the raw height, never producers of it. That is what keeps the classify
|
||
/// map — and every biome and water body that will later be derived from it — invariant under
|
||
/// every detail change.
|
||
///
|
||
/// ═══ WHAT THIS FILE DELIBERATELY DOES NOT CONTAIN ═══
|
||
///
|
||
/// The reference's task-10 draft also carried D8 flow routing, accumulation and drainage
|
||
/// INCISION. It shipped, produced the canonical grid artifact — thousands of straight,
|
||
/// disconnected, pooling scratches along the D8 neighbour directions — and was reverted whole.
|
||
/// → `Design - Terrain - D8 Incision Revert.md`. D8 returns later as ANALYSIS only.
|
||
///
|
||
/// Engine-free (System.MathF), so it sits in <c>Core/</c> beside <see cref="HeightCurve"/>: it
|
||
/// reads <see cref="CurveKnots"/> and is meaningless apart from the curve it warps.
|
||
/// </summary>
|
||
public static class TerrainDetailPass
|
||
{
|
||
/// <summary>Detail body version — the identity of this pass's layout.</summary>
|
||
public const ushort Version = 1;
|
||
|
||
// ---- pass A: micro-relief -------------------------------------------
|
||
|
||
/// <summary>Micro-relief amplitude, metres of OUTPUT height. Reference default: 3 m.</summary>
|
||
public const float ReliefAmpDefaultM = 3f;
|
||
|
||
/// <summary>
|
||
/// Micro-relief frequency, periods per MAP WIDTH. Reference: 40 — about 40 undulations
|
||
/// across the island (~200 m features at 8K). ⚠ Scale-safe by construction: stated per map
|
||
/// width, so the feature SIZE in metres holds at every map profile.
|
||
/// </summary>
|
||
public const float ReliefFreqPerMapWidth = 40f;
|
||
|
||
/// <summary>Micro-relief field seed offset. Reference: 7409. ⚠ A SEED offset, not a coordinate offset.</summary>
|
||
public const int ReliefSeedOffset = 7409;
|
||
|
||
// ---- pass B: shelf-edge variation -----------------------------------
|
||
|
||
/// <summary>Edge-warp amplitude, metres of INPUT height. Reference default: 12 m. See the type header.</summary>
|
||
public const float EdgeAmpDefaultM = 12f;
|
||
|
||
/// <summary>
|
||
/// Edge-warp frequency, periods per MAP WIDTH. Reference: 20 — ~410 px wavelength at 8K,
|
||
/// coves and notches at the scale of the developer's sketch, not a fringe of teeth.
|
||
/// </summary>
|
||
public const float EdgeFreqPerMapWidth = 20f;
|
||
|
||
/// <summary>Edge-warp field seed offset. Reference: 7507. ⚠ A SEED offset, not a coordinate offset.</summary>
|
||
public const int EdgeSeedOffset = 7507;
|
||
|
||
/// <summary>
|
||
/// The warp bound, as a fraction of the smaller adjacent band. Reference: 2/3.
|
||
///
|
||
/// ⚠ THE REAL CONSTRAINT IS BAND SQUEEZE; KNOT ORDERING FOLLOWS FROM IT. The shift compresses
|
||
/// whichever of the foothill riser / plateau it moves into. Bounding it at 2/3 of the smaller
|
||
/// band means that band never compresses below a THIRD of its nominal width — its slope never
|
||
/// more than triples, even where peak noise lands exactly on a boundary. Ordering
|
||
/// (K2 < K3±d, K5±d < K6) is then automatic, with a third of each band to spare.
|
||
/// </summary>
|
||
public const float EdgeSafetyFraction = 2f / 3f;
|
||
|
||
// ---- the crater's precedence ----------------------------------------
|
||
//
|
||
// ⚠ INERT UNTIL THE CRATER CARVE LANDS. No crater exists in this phase, so
|
||
// CraterDetailWeight is called with a non-positive radius and returns 1 everywhere — detail
|
||
// applies unmasked. The logic is ported now rather than later because it is part of THIS
|
||
// pass's contract, and bolting it on after the carve arrives is how the reference's 532-px
|
||
// bug happened in the first place. It is exercised when the carve lands.
|
||
|
||
/// <summary>
|
||
/// Detail exclusion radius, as a factor of CraterRadius. Reference: 0.80 — EXACTLY the
|
||
/// carve's own extent (<c>CRATER_CARVE_FACTOR</c>), so the two agree by construction rather
|
||
/// than by two constants that happen to match.
|
||
/// </summary>
|
||
public const float CraterDetailExclFactor = 0.80f;
|
||
|
||
/// <summary>Detail feather-to-full radius, factor of CraterRadius. Reference: 1.05.</summary>
|
||
public const float CraterDetailFeatherFactor = 1.05f;
|
||
|
||
/// <summary>
|
||
/// Detail weight from distance to the impact centre: 0 inside the carve, 1 well outside it,
|
||
/// linear between.
|
||
///
|
||
/// ═══ WHY IT EXISTS — measured, not precautionary ═══
|
||
///
|
||
/// Without it, detail moves a column's PRE-carve height, the carve's Lerp passes a fraction
|
||
/// of that through, and columns sitting a metre or two above sea inside the bowl get pushed
|
||
/// UNDER it: 532 px on reference seed 1158286446 in the first batch — terrain below the sea
|
||
/// scalar that the (classify-driven, and correctly unchanged) water grid calls dry. The carve
|
||
/// is the final authority on its own terrain; detail yields to it.
|
||
/// </summary>
|
||
/// <param name="craterRadius">
|
||
/// The configured crater radius. ⚠ <b>Non-positive means NO CRATER EXISTS</b> — returns 1
|
||
/// (detail unmasked). That is this phase's state, and it is a defined case, not a fallthrough.
|
||
/// </param>
|
||
public static float CraterDetailWeight(float distToCrater, float craterRadius)
|
||
{
|
||
if (craterRadius <= 0f) return 1f; // no crater in this phase — see the note above
|
||
|
||
float excl = craterRadius * CraterDetailExclFactor;
|
||
if (distToCrater <= excl) return 0f;
|
||
|
||
float feather = craterRadius * CraterDetailFeatherFactor;
|
||
if (distToCrater >= feather) return 1f;
|
||
|
||
return (distToCrater - excl) / (feather - excl);
|
||
}
|
||
|
||
/// <summary>
|
||
/// The largest per-column knot shift this knot set allows — see <see cref="EdgeSafetyFraction"/>.
|
||
/// K1/K2/K6 never move, so the toe, the orange/red bands and the summit spike are
|
||
/// bit-identical whatever the warp does; that is what makes the red-ceiling floor and the
|
||
/// peak cap exact rather than statistical.
|
||
/// </summary>
|
||
public static float MaxEdgeShift(CurveKnots k)
|
||
=> EdgeSafetyFraction * MathF.Min(k.K3 - k.K2, k.K6 - k.K5);
|
||
|
||
/// <summary>
|
||
/// Shelf-ness weight from the RAW input height: 1 mid-shelf, feathering to 0 through the
|
||
/// risers. Covers both shelves (bench and plateau).
|
||
///
|
||
/// ⚠ <paramref name="edgeShift"/> must be the SAME per-column warp the curve was evaluated
|
||
/// with, so the micro-relief skin follows the shelf wherever pass B moved its boundary.
|
||
/// Passing 0 here while the curve got a shift puts the skin on the wrong ground.
|
||
///
|
||
/// ⚠ Note the asymmetry in the second call: K6 is NOT shifted, because K6 never moves.
|
||
/// </summary>
|
||
public static float ShelfWeight(float raw, CurveKnots k, float edgeShift)
|
||
=> MathF.Max(BandBump(raw, k.K3 + edgeShift, k.K4 + edgeShift),
|
||
BandBump(raw, k.K5 + edgeShift, k.K6));
|
||
|
||
/// <summary>
|
||
/// A trapezoid over one band: full inside the central 60 %, linear feather to zero at 130 %
|
||
/// of the band half-width — so the skin dies out inside the risers rather than at the exact
|
||
/// band edge, which would put a visible seam on the boundary the warp is busy hiding.
|
||
/// </summary>
|
||
private static float BandBump(float h, float lo, float hi)
|
||
{
|
||
float half = (hi - lo) * 0.5f;
|
||
if (half <= 0f) return 0f; // degenerate band — no skin, no divide by zero
|
||
float t = MathF.Abs(h - (lo + half)) / half; // 0 at centre, 1 at the band edge
|
||
return Math.Clamp(1f - (t - 0.6f) / 0.7f, 0f, 1f);
|
||
}
|
||
}
|
||
}
|