PASS B, replacing the reverted incision. The developer's sketch asked for
the shelf/riser BOUNDARY to be organic, not for water: notches, coves and
small peninsulas where a flat shelf meets its riser, instead of the clean
oval contour the curve produces.
Mechanism (the task's preferred "boundary warp", in its most
monotonic-safe form): every shelf/riser boundary is the contour where the
raw height crosses K3, K4 or K5, so the boundary is warped by SLIDING
THOSE THREE KNOTS per column -- edgeShift = simplex(resolvedSeed + 7507,
12 per island width) x ShelfEdgeVariation. The contours then wander in and
out of the terrain instead of tracing an iso-height line. Noise-warped by
construction, so there is no grid direction for an artifact to line up on
-- the failure mode of the thing this replaces.
Why slide knots rather than perturb a weight or the input height:
monotonicity becomes structural instead of conditional. The curve is
strictly monotonic for ANY ordered knot set, so no derivative bound, no
amplitude-vs-feather-width tuning, no way for a dial to invert a column.
MaxEdgeShift keeps the set ordered (half the smallest margin to a fixed
knot = 12.3 m of input height for the v5 knots); the config dial is
clamped to it, loudly. AssertMonotonic now sweeps 24 corners -- the 8
modulation extremes x {-max, 0, +max} shift -- and checks the bound first.
K1, K2 and K6 never move, which buys the guarantees exactly rather than
statistically: below K2 and above K6 a warped column is bit-identical to
an unwarped one, so the red ceiling still floors every shelf edge (storm
ladder safe), the 420 m cap still caps, and the toe and summit spikes are
untouched. The block slides rigidly, so the bench and mid-riser keep their
exact widths -- shelf interiors stay flat, riser interiors keep their
profile, and only the foothill riser and plateau stretch to absorb it.
The micro-relief mask takes the same shift, so pass A's skin follows the
shelf wherever pass B moved its edge.
Dial ShelfEdgeVariation (default 5 m of INPUT height -- a boundary
displacement, not an elevation change) under the existing TerrainDetail
gate; both passes stay one judged unit. TDTL v2 body records relief and
edge amp/frequency/seed-offset plus the applied clamp bound; writer,
parser and harness follow. No blueprint was written between the revert
and this commit, so v2 only ever means relief + edge warp on disk.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
219 lines
9.1 KiB
C#
219 lines
9.1 KiB
C#
using Godot;
|
||
|
||
/// <summary>
|
||
/// One preset's input knots for the v5 curve. Two presets exist for the task-09
|
||
/// taste batch — COMPACT (maximum lowland, cordillera-from-plains) and BALANCED
|
||
/// (the task-07/08 lineage, gradual highland approach). The loser retires after
|
||
/// the developer's gate; the winner becomes plain "v5".
|
||
/// </summary>
|
||
public sealed class CurveKnots
|
||
{
|
||
public readonly byte PresetId;
|
||
public readonly string Name;
|
||
public readonly float K1, K2, K3, K4, K5, K6;
|
||
|
||
public CurveKnots(byte id, string name, float k1, float k2, float k3, float k4, float k5, float k6)
|
||
{
|
||
PresetId = id; Name = name;
|
||
K1 = k1; K2 = k2; K3 = k3; K4 = k4; K5 = k5; K6 = k6;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// The height-redistribution curve, v5 (terrain-water task 09) — v4's spatially
|
||
/// modulated shelves plus the THREE CORNER FIXES, with PRESET-PARAMETERIZED knots.
|
||
/// Pure, static, monotonic; every per-column input and the knot set are explicit
|
||
/// PARAMETERS (D-035).
|
||
///
|
||
/// The corner fixes (both presets — the naturalness work):
|
||
/// 1. Riser endpoint slope floor 0.2 → 0.1 (blend 0.1u + 0.9·smoothstep): climbs
|
||
/// decelerate into shelves and accelerate out of them — no machined edges.
|
||
/// 2. Summit-spike base floor 0.1 → 0.05 (blend 0.05u + 0.95·u⁴): the spike
|
||
/// leaves the plateau gently — no hard skirt under the peaks.
|
||
/// 3. SHELF_SPAN_MIN 2 m → 6 m: pronounced shelves keep a gentle tilt — flat to
|
||
/// build on, never snooker-table flat.
|
||
///
|
||
/// Presets (input land-fraction targets; knots calibrated 2026-08-08 from the same
|
||
/// pooled batch-04 flat-sea land CDF as tasks 05/07, 340,618,126 samples; achieved
|
||
/// fractions exact by construction):
|
||
/// COMPACT — 65/12/7/4/6/3/3 (orange/red/foothill-riser/bench/mid-riser/plateau/spike)
|
||
/// BALANCED — 60/13/10/5/8/3/1
|
||
///
|
||
/// Unchanged from v4: storm-ladder anchors, bench 100±12 m, plateau 220±20 m,
|
||
/// strength modulation (span max 25 m), 420 m cap, per-seed spike normalization,
|
||
/// modulation fields/seed offsets, the classify-map invariant.
|
||
///
|
||
/// The curve SHAPE is frozen at v5. Task 10 adds no band, anchor or slope — only
|
||
/// a per-column `edgeShift` parameter on Apply, which slides the shelf/riser knot
|
||
/// block K3/K4/K5 so those three boundaries stop being clean iso-height contours.
|
||
/// It is a new input to the same curve, not a new curve.
|
||
/// </summary>
|
||
public static class HeightCurve
|
||
{
|
||
public const ushort VERSION = 5;
|
||
|
||
// The task-09 taste gate's WINNER: BALANCED (id 2). COMPACT retired with the
|
||
// verdict; its knots survive only in the task-09 report/batch for the record.
|
||
public static readonly CurveKnots V5 = new CurveKnots(2, "balanced",
|
||
0.515899f, 0.612157f, 0.710472f, 0.784045f, 0.962922f, 1.119118f); // P60/73/83/88/96/99
|
||
|
||
// Fixed output anchors — storm ladder + ceiling (frozen).
|
||
public const float SEA = 0.15f;
|
||
public const float ORANGE_CEIL = 0.206f;
|
||
public const float RED_CEIL = 0.27f;
|
||
public const float PEAK_CAP = SEA + 420f / 251f;
|
||
public const float TAIL_SLOPE = 0.25f;
|
||
public const float SPIKE_MIN_SPAN = 0.01f;
|
||
|
||
// Modulated shelf anchors (unchanged from v4).
|
||
public const float BENCH_BASE = SEA + 100f / 251f;
|
||
public const float BENCH_AMP = 12f / 251f;
|
||
public const float PLATEAU_BASE = SEA + 220f / 251f;
|
||
public const float PLATEAU_AMP = 20f / 251f;
|
||
|
||
// Corner fix 3: pronounced shelves keep ~6 m of tilt across the shelf band.
|
||
public const float SHELF_SPAN_MIN = 6f / 251f; // ≈ 0.0239 (v4: 0.008 ≈ 2 m)
|
||
public const float SHELF_SPAN_MAX = 0.10f;
|
||
|
||
// Modulation-field derivation (unchanged from v4).
|
||
public const int BENCH_SEED_OFFSET = 7101;
|
||
public const int PLATEAU_SEED_OFFSET = 7207;
|
||
public const int STRENGTH_SEED_OFFSET = 7303;
|
||
public const float ELEV_FREQ_ISLANDS = 3.0f;
|
||
public const float STRENGTH_FREQ_ISLANDS = 5.0f;
|
||
|
||
public static float EffectiveSpikeMax(float hMaxSeed, CurveKnots k)
|
||
{
|
||
return Mathf.Max(hMaxSeed, k.K6 + SPIKE_MIN_SPAN);
|
||
}
|
||
|
||
public static float ShelfSpan(float strength01)
|
||
{
|
||
return Mathf.Lerp(SHELF_SPAN_MAX, SHELF_SPAN_MIN, Mathf.Clamp(strength01, 0f, 1f));
|
||
}
|
||
|
||
/// <summary>
|
||
/// The curve for ONE column. <paramref name="edgeShift"/> (task 10 pass B) slides
|
||
/// the shelf/riser knot BLOCK — K3/K4/K5 — up or down by a per-column amount,
|
||
/// leaving K1/K2/K6 fixed. Every shelf↔riser boundary is the contour where the
|
||
/// raw height crosses one of those three knots, so shifting them makes those
|
||
/// contours wander instead of tracing a clean iso-height line: the shelf edge
|
||
/// scallops. Because the block moves rigidly, the bench and mid-riser bands keep
|
||
/// their exact widths (their interior shapes are translated, not distorted); only
|
||
/// the foothill riser and the plateau stretch or compress to absorb the shift.
|
||
/// Monotonicity is structural, not conditional — the curve is monotonic for ANY
|
||
/// strictly ordered knot set, and TerrainDetailPass.MaxEdgeShift keeps the set
|
||
/// ordered by construction. Below K2 and above K6 the output is bit-identical to
|
||
/// an unwarped column, which is what makes the red-ceiling floor and the 420 m
|
||
/// peak cap exact under the warp.
|
||
/// </summary>
|
||
public static float Apply(float h, float hMaxSeed,
|
||
float benchLo, float benchSpan, float plateauLo, float plateauSpan, CurveKnots k,
|
||
float edgeShift)
|
||
{
|
||
if (h <= SEA) return h;
|
||
|
||
float k3 = k.K3 + edgeShift, k4 = k.K4 + edgeShift, k5 = k.K5 + edgeShift;
|
||
|
||
float u, s;
|
||
if (h < k.K1)
|
||
{
|
||
u = (h - SEA) / (k.K1 - SEA);
|
||
s = 0.3f * u + 0.7f * (u * (2f - u)); // frozen ease-out toe
|
||
return SEA + s * (ORANGE_CEIL - SEA);
|
||
}
|
||
if (h < k.K2)
|
||
{
|
||
u = (h - k.K1) / (k.K2 - k.K1);
|
||
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // frozen linear rise
|
||
}
|
||
if (h < k3)
|
||
{
|
||
u = (h - k.K2) / (k3 - k.K2);
|
||
s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // foothill riser — corner fix 1
|
||
return RED_CEIL + s * (benchLo - RED_CEIL);
|
||
}
|
||
if (h < k4)
|
||
{
|
||
u = (h - k3) / (k4 - k3);
|
||
return benchLo + u * benchSpan; // bench (min span 6 m — fix 3)
|
||
}
|
||
float benchTop = benchLo + benchSpan;
|
||
if (h < k5)
|
||
{
|
||
u = (h - k4) / (k5 - k4);
|
||
s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // mid riser — corner fix 1
|
||
return benchTop + s * (plateauLo - benchTop);
|
||
}
|
||
if (h < k.K6)
|
||
{
|
||
u = (h - k5) / (k.K6 - k5);
|
||
return plateauLo + u * plateauSpan; // plateau
|
||
}
|
||
float plateauTop = plateauLo + plateauSpan;
|
||
float spikeMax = EffectiveSpikeMax(hMaxSeed, k);
|
||
if (h < spikeMax)
|
||
{
|
||
u = (h - k.K6) / (spikeMax - k.K6);
|
||
s = 0.05f * u + 0.95f * (u * u * u * u); // summit spike — corner fix 2
|
||
return plateauTop + s * (PEAK_CAP - plateauTop);
|
||
}
|
||
return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Per-generation numeric strict-monotonicity check of the EFFECTIVE curve for
|
||
/// the selected preset: all 8 modulation-extreme corners × the shelf-edge warp
|
||
/// extremes (±maxEdgeShift and 0) × per-seed spikeMax — 24 corners. The corner
|
||
/// fixes lower the slope floors (risers 0.1, spike base 0.05) and the warp
|
||
/// squeezes the foothill riser and the plateau; the sweep proves every slope
|
||
/// stays strictly positive at the extremes of both. Also checks the knot set
|
||
/// itself stays strictly ordered under the warp. Loud throw on failure.
|
||
/// </summary>
|
||
public static void AssertMonotonic(float hMaxSeed, CurveKnots k, float maxEdgeShift)
|
||
{
|
||
if (maxEdgeShift < 0f || k.K2 + maxEdgeShift >= k.K3 || k.K5 + maxEdgeShift >= k.K6)
|
||
throw new System.InvalidOperationException(
|
||
$"[HeightCurve] EDGE-SHIFT BOUND VIOLATION: maxEdgeShift={maxEdgeShift} does not keep K2<K3±d and K5±d<K6 (preset {k.Name}). Refusing to generate.");
|
||
|
||
float[] benchLos = { BENCH_BASE - BENCH_AMP, BENCH_BASE + BENCH_AMP };
|
||
float[] plateauLos = { PLATEAU_BASE - PLATEAU_AMP, PLATEAU_BASE + PLATEAU_AMP };
|
||
float[] spans = { SHELF_SPAN_MIN, SHELF_SPAN_MAX };
|
||
float[] edgeShifts = maxEdgeShift > 0f
|
||
? new float[] { -maxEdgeShift, 0f, maxEdgeShift }
|
||
: new float[] { 0f };
|
||
|
||
foreach (float bl in benchLos)
|
||
{
|
||
foreach (float pl in plateauLos)
|
||
{
|
||
foreach (float sp in spans)
|
||
{
|
||
foreach (float es in edgeShifts)
|
||
{
|
||
float prevH = -7f;
|
||
float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp, k, es);
|
||
|
||
void Check(double hd)
|
||
{
|
||
float h = (float)hd;
|
||
if (h <= prevH) return; // dedupe float32 samples (task-05 fix)
|
||
float v = Apply(h, hMaxSeed, bl, sp, pl, sp, k, es);
|
||
if (v <= prev)
|
||
throw new System.InvalidOperationException(
|
||
$"[HeightCurve] MONOTONICITY VIOLATION at h={h} (preset {k.Name}, hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}, edgeShift={es}): {v} <= {prev}. Refusing to generate.");
|
||
prev = v;
|
||
prevH = h;
|
||
}
|
||
|
||
double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed, k) + 0.5);
|
||
for (double hh = -7.0 + 0.01; hh < 0.10; hh += 0.01) Check(hh);
|
||
for (double hh = 0.10; hh <= top; hh += 0.0001) Check(hh);
|
||
for (double hh = top + 0.05; hh <= top + 6.0; hh += 0.05) Check(hh);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION} preset '{k.Name}', 8 modulation corners × edge shifts ±{maxEdgeShift:F6}, effective spikeMax {EffectiveSpikeMax(hMaxSeed, k):F6}).");
|
||
}
|
||
}
|