fix: shrink erosion's crater exclusion to the strike core; add FULL/FEATHER modes (terrain-water task 19)
Task 17's hard 1.2 x CraterRadius cutoff left a visible un-eroded disc with a hard edge. Measured (radius dump, seed 1280587109): the carve writes only inside 0.80 x (640 px) and its displacement is EXACTLY 0 beyond that, so the 640-960 px annulus was 620,811 LAND cells of ordinary terrain held smooth for no geometric reason. That annulus is now eroded — 98% of those cells are touched. The protected core is the carve's own extent (CraterErosionCore, 0.80 x). CraterErosionMode selects the transition: "full" applies full strength at the core boundary (older-crater look), "feather" ramps 0->full out to CraterErosionFeather (1.05 x, mirroring the detail pass) for a younger-crater look with no seam. Implemented as a WEIGHT that scales carve and deposit amounts, not a skip, which is what makes feather a one-liner. Default "feather" pending the gate. Core = the carve radius is load-bearing, not tidy: the carve runs AFTER erosion and scales height toward the sea target, so it amplifies any erosion delta inside its radius. Measured at a 0.50 core: 79 cells newly below the rendered sea and 113,310 below-sea cells disturbed — the in-pass flood guard cannot see this because it measures before the carve. At 0.80 the two passes touch disjoint cells and the guarantee is exact again (0/0/0). A narrower core also reclaims nothing extra, since the over-protected annulus lies entirely outside the carve. MapGenerator now owns CRATER_CARVE_FACTOR as the single source of that 0.80 and warns loudly if the configured core is narrower. Verified both modes, seed 1280587109: 1_biomes/0_water md5-identical to erosion-OFF, BIOM/WBID bitwise equal, 0 newly below/above sea, 0 below-sea cells modified, 0 cells modified inside the core, island top 457.65 m unchanged, isotropy 1.005/1.007. Bay-to-ocean connection demonstrated explicitly by flood fill through rendered water to the map's north border (2,122,935 cells), not merely argued from the sea clamp. EROS body version -> 3: craterExclFactor replaced by craterCoreFactor + craterFeatherFactor + craterMode. Round-trip harness PASS with a real v3 payload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
85e7bceb72
commit
f810ff44dc
7 changed files with 141 additions and 24 deletions
|
|
@ -38,9 +38,12 @@ using System;
|
|||
/// read-only — never eroded, never deposited on. Land stays land, sea stays sea;
|
||||
/// the rendered coastline cannot move. Deposition only raises land cells.
|
||||
///
|
||||
/// The crater exclusion: no cell within CraterExclRadius of the impact centre is
|
||||
/// modified (droplets may traverse). The carve remains the final authority on its
|
||||
/// own terrain.
|
||||
/// The crater treatment (task 19): no cell within the protected strike CORE is
|
||||
/// modified (droplets may traverse), and outside it either FULL strength applies
|
||||
/// immediately or FEATHER ramps in across a band. The carve remains the final
|
||||
/// authority on the deep bowl; the bay's sea connection is guaranteed by the sea
|
||||
/// clamp rather than by the exclusion, since below-sea cells are read-only in
|
||||
/// both directions.
|
||||
///
|
||||
/// Heights in the array are raw blueprint units (1 unit = 251 m). All sediment
|
||||
/// accounting below is done in METRES and converted only when a delta is applied,
|
||||
|
|
@ -62,9 +65,25 @@ public static class HydraulicErosion
|
|||
|
||||
public const float M_PER_UNIT = 251f;
|
||||
|
||||
// Crater exclusion factor: erosion stays outside 1.2 × CraterRadius — fully
|
||||
// clear of both the physical carve (0.80×) and the detail feather (1.05×).
|
||||
public const float CRATER_EXCL_FACTOR = 1.2f;
|
||||
// --- Crater treatment (task 19) ---
|
||||
//
|
||||
// Task 17 used a hard 1.2 × CraterRadius cutoff. Measured on seed 1280587109
|
||||
// (task-19 radius dump): the carve writes only inside 0.80 × (640 px) and its
|
||||
// displacement is EXACTLY 0 beyond that, so the 640–960 px annulus was 620,811
|
||||
// land cells of ordinary terrain held smooth for no geometric reason — a
|
||||
// visible un-eroded disc against dissected ground, with a hard edge.
|
||||
//
|
||||
// The protected core is now the deep strike zone only. The bay itself needs no
|
||||
// exclusion: below-sea cells are read-only in both directions (the sea clamp),
|
||||
// so erosion can neither carve the bay's sea connection open nor silt it shut.
|
||||
// The core exists to stop the BOWL being dissected on seeds where it holds land
|
||||
// (on 1280587109 there is no land at all inside 0.50 ×, so the core is
|
||||
// functionally redundant there — the guard is for the general seed).
|
||||
public const float CRATER_CORE_FACTOR_DEFAULT = 0.50f; // ×CraterRadius
|
||||
public const float CRATER_FEATHER_FACTOR_DEFAULT = 1.05f; // ×CraterRadius, FEATHER only
|
||||
|
||||
public const byte CRATER_MODE_FULL = 0;
|
||||
public const byte CRATER_MODE_FEATHER = 1;
|
||||
|
||||
// Spawn: droplets source in the mountains, never the ocean. A land point is
|
||||
// accepted with probability SPAWN_FLOOR + (1-SPAWN_FLOOR) · relative elevation,
|
||||
|
|
@ -91,6 +110,7 @@ public static class HydraulicErosion
|
|||
public float DepositRate; // fraction of surplus sediment dropped per step
|
||||
public float Evaporation; // water lost per step (fraction)
|
||||
public float Gravity; // speed gain per metre of drop
|
||||
public byte CraterMode; // CRATER_MODE_FULL | CRATER_MODE_FEATHER (task 19)
|
||||
public int Seed; // resolvedSeed + SEED_OFFSET
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +150,7 @@ public static class HydraulicErosion
|
|||
/// bound is violated on exit — the caller treats that as a build failure.
|
||||
/// </summary>
|
||||
public static Stats Apply(float[,] height, int mapSize, float[,] seaMap, float seaFlat,
|
||||
float craterCx, float craterCy, float craterExclRadius, Params p)
|
||||
float craterCx, float craterCy, float craterCoreRadius, float craterFeatherRadius, Params p)
|
||||
{
|
||||
var stats = new Stats();
|
||||
var rng = new Pcg32(p.Seed);
|
||||
|
|
@ -173,12 +193,25 @@ public static class HydraulicErosion
|
|||
for (int j = 0; j < brushN; j++) brushW[j] /= wSum;
|
||||
}
|
||||
|
||||
float exclSq = craterExclRadius * craterExclRadius;
|
||||
float SeaAt(int cx, int cy) => seaMap != null ? seaMap[cx, cy] : seaFlat;
|
||||
bool Excluded(int cx, int cy)
|
||||
|
||||
// Crater weight (task 19): 0 inside the protected strike core, 1 where erosion
|
||||
// runs at full strength. FULL steps straight to 1 at the core boundary; FEATHER
|
||||
// ramps linearly out to craterFeatherRadius, mirroring the detail pass's shape,
|
||||
// so the crater reads as younger/less-weathered with no seam. Amounts are SCALED
|
||||
// by this rather than skipped, which is what makes FEATHER a one-liner.
|
||||
float coreSq = craterCoreRadius * craterCoreRadius;
|
||||
bool feather = p.CraterMode == CRATER_MODE_FEATHER
|
||||
&& craterFeatherRadius > craterCoreRadius;
|
||||
float CraterWeight(int cx, int cy)
|
||||
{
|
||||
float ddx = cx - craterCx, ddy = cy - craterCy;
|
||||
return ddx * ddx + ddy * ddy < exclSq;
|
||||
float d2 = ddx * ddx + ddy * ddy;
|
||||
if (d2 < coreSq) return 0f;
|
||||
if (!feather) return 1f;
|
||||
float d = MathF.Sqrt(d2);
|
||||
if (d >= craterFeatherRadius) return 1f;
|
||||
return (d - craterCoreRadius) / (craterFeatherRadius - craterCoreRadius);
|
||||
}
|
||||
|
||||
for (int drop = 0; drop < p.DropletCount; drop++)
|
||||
|
|
@ -261,12 +294,13 @@ public static class HydraulicErosion
|
|||
{
|
||||
int cx = xi + brushDx[b], cy = yi + brushDy[b];
|
||||
if (cx < 0 || cx >= mapSize || cy < 0 || cy >= mapSize) continue;
|
||||
if (Excluded(cx, cy)) continue;
|
||||
float wCrater = CraterWeight(cx, cy);
|
||||
if (wCrater <= 0f) continue;
|
||||
float hCell = height[cx, cy];
|
||||
// Below-sea cells are read-only in BOTH directions: no
|
||||
// submarine deltas, so the rendered coastline cannot move.
|
||||
if (hCell < SeaAt(cx, cy)) continue;
|
||||
float give = amountM * brushW[b];
|
||||
float give = amountM * brushW[b] * wCrater;
|
||||
// Governor 4: the ledger read the other way. net is negative
|
||||
// where the cell has already been built up, so the headroom
|
||||
// is cap + net.
|
||||
|
|
@ -293,11 +327,12 @@ public static class HydraulicErosion
|
|||
{
|
||||
int cx = xi + brushDx[b], cy = yi + brushDy[b];
|
||||
if (cx < 0 || cx >= mapSize || cy < 0 || cy >= mapSize) continue;
|
||||
if (Excluded(cx, cy)) continue;
|
||||
float wCrater = CraterWeight(cx, cy);
|
||||
if (wCrater <= 0f) continue;
|
||||
float sea = SeaAt(cx, cy);
|
||||
float hCell = height[cx, cy];
|
||||
if (hCell < sea) continue; // below-sea cells are read-only
|
||||
float want = amountM * brushW[b];
|
||||
float want = amountM * brushW[b] * wCrater;
|
||||
float bySea = MathF.Max(0f, (hCell - (sea + p.SeaMarginM / M_PER_UNIT)) * M_PER_UNIT);
|
||||
float byCap = MathF.Max(0f, p.CarveCapM - net[cx, cy]);
|
||||
float take = MathF.Min(want, MathF.Min(bySea, byCap));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue