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>
112 lines
5.8 KiB
C#
112 lines
5.8 KiB
C#
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 < K3+d, K5+d < 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);
|
||
}
|
||
}
|