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>
This commit is contained in:
Stewart Howe 2026-08-08 21:23:28 -04:00
parent af1a6d912b
commit 59f49ac8a2
2 changed files with 34 additions and 5 deletions

View file

@ -519,6 +519,7 @@ public partial class MapGenerator : TextureRect
float raw = _heightMap[x, y];
float classifyH = raw;
float curvedH;
float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter);
if (_curveOn)
{
// v4: per-column shelf modulation — anchors and strength from the
@ -528,14 +529,20 @@ public partial class MapGenerator : TextureRect
float plateauLo = HeightCurve.PLATEAU_BASE + _plateauNoise.GetNoise2D(x, y) * HeightCurve.PLATEAU_AMP;
float shelfSpan = HeightCurve.ShelfSpan((_strengthNoise.GetNoise2D(x, y) + 1f) * 0.5f);
float edgeShift = _detailOn ? _edgeNoise.GetNoise2D(x, y) * _edgeAmpRaw : 0f;
// Detail yields to the crater: zero inside the carve, feathered just
// outside it, so the carve stays the final authority on its terrain.
float wCrater = _detailOn
? TerrainDetailPass.CraterDetailWeight(distToCrater, _impactRadius)
: 0f;
float edgeShift = _detailOn ? _edgeNoise.GetNoise2D(x, y) * _edgeAmpRaw * wCrater : 0f;
curvedH = HeightCurve.Apply(raw, _hMaxSeed, benchLo, shelfSpan, plateauLo, shelfSpan, _curveKnots, edgeShift);
if (_detailOn)
if (_detailOn && wCrater > 0f)
{
float wShelf = TerrainDetailPass.ShelfWeight(raw, _curveKnots, edgeShift);
if (wShelf > 0f)
curvedH += _reliefNoise.GetNoise2D(x, y) * reliefAmpRaw * wShelf;
curvedH += _reliefNoise.GetNoise2D(x, y) * reliefAmpRaw * wShelf * wCrater;
}
}
else
@ -546,8 +553,6 @@ public partial class MapGenerator : TextureRect
// --- 5. CARVE THE CRATER (The Flooded Bay & Landbridge Fix!) ---
// Both maps are carved from LOCALS and written once, so the curve-off
// aliasing (classify and height are the same array) cannot double-carve.
float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter);
// We only carve the physical hole at 80% of the radius to guarantee a landbridge!
if (distToCrater < physicalCraterRadius)
{

View file

@ -52,6 +52,30 @@ public static class TerrainDetailPass
// 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