feat: shelf-edge variation -- the red-line scalloping (terrain-water task 10)

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>
This commit is contained in:
Stewart Howe 2026-08-08 20:31:41 -04:00
parent 3b6d166458
commit 492b56a87c
7 changed files with 182 additions and 69 deletions

View file

@ -8,16 +8,19 @@ using Godot;
/// default 3 m) weighted by shelf-ness, so the compressed shelves get their
/// rolling texture back while risers and peaks stay untouched.
///
/// Output-height only; the classify map never sees it.
/// PASS B — shelf-edge variation: a per-column shift of the shelf/riser KNOT
/// BLOCK (K3/K4/K5) by a low-frequency noise field, so the boundary where a
/// shelf meets its riser wanders in and out instead of tracing a clean height
/// contour — organic notches, coves and peninsulas at the shelf edge.
///
/// NOTE — what this file deliberately no longer contains: the task-10 draft's D8
/// steepest-descent flow routing, accumulation and drainage incision. It shipped,
/// and it produced the canonical grid artifact — thousands of straight,
/// disconnected, pooling scratches running along the eight D8 neighbour
/// directions, because per-cell steepest descent on a regular grid can only ever
/// route along those eight headings. It is reverted whole. Rivers and erosion are
/// Phase C work: a hydraulic-erosion pass over FINAL terrain, not a routing carve
/// on a grid mid-pipeline.
/// Both are output-height only; the classify map never sees either of them.
///
/// NOTE — what this file deliberately does NOT contain: the task-10 draft's D8
/// flow routing / accumulation / drainage incision. It shipped, produced the
/// canonical grid artifact (thousands of straight, disconnected, pooling
/// scratches along the D8 neighbour directions) and was reverted whole. Rivers
/// and erosion are Phase C work — a hydraulic-erosion pass over FINAL terrain,
/// not a per-cell steepest-descent carve on a grid.
/// </summary>
public static class TerrainDetailPass
{
@ -29,14 +32,38 @@ public static class TerrainDetailPass
public const float RELIEF_FREQ_ISLANDS = 40f; // ~40 undulations per island width (~200 m features)
public const int RELIEF_SEED_OFFSET = 7409;
// Pass B — shelf-edge variation. The amplitude is stated in metres of INPUT
// height (raw × 251): it is how far, in raw-height terms, a shelf boundary
// contour is displaced — not an output elevation change. Lateral wander on
// the map is that displacement divided by the local raw gradient.
public const float EDGE_AMP_DEFAULT_M = 5f; // config dial: ShelfEdgeVariation (metres of input height)
public const float EDGE_FREQ_ISLANDS = 12f; // ~12 undulations per island width — a handful of notches
public const int EDGE_SEED_OFFSET = 7507; // per shelf perimeter, not hundreds of teeth
public const float EDGE_SAFETY_FRACTION = 0.5f; // shift ≤ half the smallest margin to a fixed knot
/// <summary>
/// The largest per-column knot shift this preset can take while keeping the
/// knot set strictly ordered (K2 &lt; K3+d, K5+d &lt; K6) with margin to spare.
/// K1/K2/K6 never move, so the toe, the orange/red bands and the summit spike
/// are bit-identical whatever the warp does — which is what makes the
/// red-ceiling and peak-height guarantees exact rather than statistical.
/// </summary>
public static float MaxEdgeShift(CurveKnots k)
{
return EDGE_SAFETY_FRACTION * 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 (feather extends 30 % of the band half-width past each
/// shelf edge). Covers both shelves.
/// shelf edge). Covers both shelves. <paramref name="edgeShift"/> is the same
/// per-column warp the curve is evaluated with, so the micro-relief skin
/// follows the shelf wherever pass B has moved its boundary.
/// </summary>
public static float ShelfWeight(float raw, CurveKnots k)
public static float ShelfWeight(float raw, CurveKnots k, float edgeShift)
{
return Mathf.Max(BandBump(raw, k.K3, k.K4), BandBump(raw, k.K5, k.K6));
return Mathf.Max(BandBump(raw, k.K3 + edgeShift, k.K4 + edgeShift),
BandBump(raw, k.K5 + edgeShift, k.K6));
}
private static float BandBump(float h, float lo, float hi)