islaApocalypse/Tools/Scripts/TerrainDetailPass.cs
beezm 59f49ac8a2 fix: detail must yield to the crater carve (terrain-water task 10)
Batch 10_detail_edge measured 532 new below-sea pixels on seed
1158286446 and 44 on 72563200 -- exported terrain under the sea scalar
that the water grid (classify-driven, and correctly identical) calls dry.
Located: every one of them strictly inside the crater's physical carve
radius, 201-323 px out of 640.

Mechanism, confirmed against the data: detail moves a column's PRE-carve
height, and the carve is Lerp(curvedH, target, t). At t ~ 0.45-0.62 a
pre-carve drop of up to 8.4 m (relief plus the band-shift the warp
implies) still passes 0.02-3.53 m through, which is enough to push a
column sitting 0-3.45 m above the sea inside the bowl under it.

The crater carve is supposed to be the FINAL authority on its own
terrain, so detail now yields to it: CraterDetailWeight is 0 inside
0.80 x CraterRadius (exactly the carve's own radius) and feathers to
full by 1.05 x, scaling both the edge shift and the relief skin. Inside
the carve, B is bit-identical to A by construction, so the count is zero
rather than small. The reverted incision pass carried the same exclusion
for the same reason -- the principle outlived the pass that motivated it.

Re-running the 6 B_detail generations; A_off and the continuity run are
untouched by a detail-only change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 21:23:28 -04:00

112 lines
5.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Godot;
/// <summary>
/// The terrain DETAIL passes (terrain-water task 10) — pure numeric functions
/// (D-035; a named future C++ candidate, kept standalone):
///
/// PASS A — shelf micro-relief: a medium-frequency noise skin (±ShelfReliefAmp,
/// default 3 m) weighted by shelf-ness, so the compressed shelves get their
/// rolling texture back while risers and peaks stay untouched.
///
/// 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.
///
/// 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
{
// The TDTL body version is owned by the format (Core); the pass just stamps it.
public const ushort VERSION = IslaApocalypse.Core.BlueprintFormat.TDTL_VERSION;
// Pass A — micro-relief.
public const float RELIEF_AMP_DEFAULT_M = 3f; // config dial: ShelfReliefAmp (metres)
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. What the eye sees is
// the LATERAL wander, which is that displacement divided by the local raw
// gradient. Measured on 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 — coves and notches, which
// is where the developer's sketch sits. 5 m (the first try) moved the boundary
// a mean 3.7 px and was invisible at map scale.
public const float EDGE_AMP_DEFAULT_M = 12f; // config dial: ShelfEdgeVariation (metres of input height)
public const float EDGE_FREQ_ISLANDS = 20f; // ~410 px wavelength at 8K — coves and notches at the
public const int EDGE_SEED_OFFSET = 7507; // scale of the sketch, not a fringe of teeth
// The shift squeezes whichever of the foothill riser / plateau bands it moves
// into. Bounding it at 2/3 of the smaller band means that band never compresses
// below a THIRD of its nominal width — i.e. its slope never more than triples,
// even where peak noise lands on a boundary. That is the real constraint; knot
// ordering follows from it.
public const float EDGE_SAFETY_FRACTION = 2f / 3f;
// The crater carve is the final authority on its own terrain. Detail is masked
// out inside the physical carve radius (0.80 × CraterRadius — exactly where the
// carve applies) and feathers to full by 1.05 ×. Without this, 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 the sea inside the bowl get pushed
// under it — 532 px on seed 1158286446 in the first batch, terrain below the sea
// scalar that the (classify-driven, and correctly unchanged) water grid calls dry.
public const float CRATER_DETAIL_EXCL_FACTOR = 0.80f;
public const float CRATER_DETAIL_FEATHER_FACTOR = 1.05f;
/// <summary>
/// Detail weight from distance to the impact centre: 0 inside the carve, 1 well
/// outside it, linear between. <paramref name="craterRadius"/> is the configured
/// CraterRadius (the carve itself uses 0.80 × of it).
/// </summary>
public static float CraterDetailWeight(float distToCrater, float craterRadius)
{
float excl = craterRadius * CRATER_DETAIL_EXCL_FACTOR;
if (distToCrater <= excl) return 0f;
float feather = craterRadius * CRATER_DETAIL_FEATHER_FACTOR;
if (distToCrater >= feather) return 1f;
return (distToCrater - excl) / (feather - excl);
}
/// <summary>
/// The largest per-column knot shift this preset allows: bounded by the band
/// squeeze above, which also keeps the knot set strictly ordered
/// (K2 &lt; K3+d, K5+d &lt; K6) with a third of each band 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
/// floor and the 420 m cap 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. <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, float edgeShift)
{
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)
{
float half = (hi - lo) * 0.5f;
float t = Mathf.Abs(h - (lo + half)) / half; // 0 centre, 1 at band edge
// full inside 60 % of the band, linear feather to zero at 130 %
return Mathf.Clamp(1f - (t - 0.6f) / 0.7f, 0f, 1f);
}
}