fix: brush-spread deposition + deposit-cap governor; retune erosion for a drainage hierarchy (terrain-water task 18)
Deposition now spreads over the same cone brush as carving, and the per-cell erosion ledger becomes a signed NET displacement ledger, so both caps are measured from the height the pass found. Brush-spreading alone cut the spike 15.49 -> 6.12 m at task-17 dials, but NOT at hierarchy dials (long paths carry far more sediment; a loaded droplet meeting a rise dumps min(rise, load) at once -> 25.5 m). So deposition also gets governor 4, ErosionDepositCap (default 6 m, <= 0 = unbounded), asserted on exit like the carve cap. Dial defaults retuned for a drainage HIERARCHY: 250k droplets x 384 steps at inertia 0.35 / evaporation 0.004, erode 0.12, carve cap 15 m. Diagnosis was that lifetime 48 let a droplet travel at most 48 px on a ~3000 px island radius, so paths could not overlap into trunks: the deepest task-17 features were 43x36 px patches. Now 177 channel systems of 200+ cells at the 3 m threshold, top one 204x279 px with 14 tributary tips; cells past 5 m up 8.6x (4,948 -> 42,385) while the fine rills are retained. Verified descending, not contour-locked: 0/155 components have drop/extent < 0.12 (median 1.13). EROS body version -> 2 (deposit cap inserted after the carve cap); v1 payloads are skipped whole by the existing rule. HydraulicErosion.VERSION now reads BlueprintFormat.EROS_VERSION instead of restating it — the local copy had already drifted and stamped a v2 body as v1, which readers decode with every float shifted by one field. Oracle green on both seeds (1280587109, 1512575962): 1_biomes/0_water md5-identical erosion-ON vs OFF, BIOM/WBID bitwise equal. Flood guard 0 new water px, 0 below-sea cells touched, crater zone untouched, island top unchanged, carve field isotropic to 1.4% across the folded 45deg period. Erosion pass 34 s at these dials. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
56ac17e670
commit
cc6a24d2b9
8 changed files with 105 additions and 59 deletions
|
|
@ -43,11 +43,13 @@ namespace IslaApocalypse.Core
|
||||||
// differently shaped payload into plausible-looking nonsense.
|
// differently shaped payload into plausible-looking nonsense.
|
||||||
public const ushort TDTL_VERSION = 2;
|
public const ushort TDTL_VERSION = 2;
|
||||||
|
|
||||||
// EROS body version (terrain-water task 17). 1 = the droplet hydraulic-
|
// EROS body version. 1 (task 17) = governors count/lifetime/carve cap, sea
|
||||||
// erosion params block: governors (count/lifetime/carve cap), sea clamp,
|
// clamp, brush, strength constants, RNG seed offset, crater exclusion factor;
|
||||||
// brush, strength constants, RNG seed offset, crater exclusion factor.
|
// the only v1 payloads in existence are in that task's batch tree. 2 (task 18,
|
||||||
// Same reader rule as TDTL: an unknown body version is skipped whole.
|
// current) inserts the DEPOSIT CAP governor after the carve cap — deposition is
|
||||||
public const ushort EROS_VERSION = 1;
|
// now brush-spread and per-cell bounded. Same reader rule as TDTL: an unknown
|
||||||
|
// body version is skipped whole rather than misread into plausible nonsense.
|
||||||
|
public const ushort EROS_VERSION = 2;
|
||||||
|
|
||||||
// WSRF quantization: u16, 0 reserved as the no-water sentinel. A real level L
|
// WSRF quantization: u16, 0 reserved as the no-water sentinel. A real level L
|
||||||
// (raw blueprint height units) encodes as 1 + round(L × 32768), so a genuine
|
// (raw blueprint height units) encodes as 1 + round(L × 32768), so a genuine
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,8 @@ namespace IslaApocalypse.Core
|
||||||
writer.Write(e.Version); // u16
|
writer.Write(e.Version); // u16
|
||||||
writer.Write(e.DropletCount); writer.Write(e.Lifetime); // 2 × i32
|
writer.Write(e.DropletCount); writer.Write(e.Lifetime); // 2 × i32
|
||||||
writer.Write(e.BrushRadius); writer.Write(e.SeedOffset); // 2 × i32
|
writer.Write(e.BrushRadius); writer.Write(e.SeedOffset); // 2 × i32
|
||||||
writer.Write(e.CarveCapM); writer.Write(e.SeaMarginM); // 2 × f32
|
writer.Write(e.CarveCapM); writer.Write(e.DepositCapM); // 2 × f32
|
||||||
|
writer.Write(e.SeaMarginM); // f32
|
||||||
writer.Write(e.Inertia); writer.Write(e.CapacityFactor); // 2 × f32
|
writer.Write(e.Inertia); writer.Write(e.CapacityFactor); // 2 × f32
|
||||||
writer.Write(e.MinSlopeM); // f32
|
writer.Write(e.MinSlopeM); // f32
|
||||||
writer.Write(e.ErodeRate); writer.Write(e.DepositRate); // 2 × f32
|
writer.Write(e.ErodeRate); writer.Write(e.DepositRate); // 2 × f32
|
||||||
|
|
|
||||||
|
|
@ -55,18 +55,28 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
||||||
// never touched at all, so the rendered coastline cannot move. The remaining
|
// never touched at all, so the rendered coastline cannot move. The remaining
|
||||||
// dials are the standard droplet-model strength constants; slopes/amounts
|
// dials are the standard droplet-model strength constants; slopes/amounts
|
||||||
// are in METRES (1 raw height unit = 251 m).
|
// are in METRES (1 raw height unit = 251 m).
|
||||||
|
// Task-18 defaults tune for a DRAINAGE HIERARCHY: long-lived, committed
|
||||||
|
// droplets (lifetime 384 at inertia 0.35, evaporation 0.004) travel far
|
||||||
|
// enough down a flank that their paths overlap and deepen shared low lines
|
||||||
|
// into trunk channels, instead of dying as independent 48-px scratches;
|
||||||
|
// the carve cap is raised to 15 m so trunks can separate from the fine
|
||||||
|
// rills instead of both piling up against the same ceiling. A modest
|
||||||
|
// erode rate keeps the total material moved in detailing range.
|
||||||
|
// ErosionDepositCap is governor 4 (task 18): brush-spread deposition alone
|
||||||
|
// does not bound a spike once droplets carry long-path loads.
|
||||||
public static string Erosion = "off";
|
public static string Erosion = "off";
|
||||||
public static int ErosionDropletCount = 400000;
|
public static int ErosionDropletCount = 250000;
|
||||||
public static int ErosionDropletLifetime = 48;
|
public static int ErosionDropletLifetime = 384;
|
||||||
public static float ErosionCarveCap = 8.0f; // m per cell
|
public static float ErosionCarveCap = 15.0f; // m per cell
|
||||||
|
public static float ErosionDepositCap = 6.0f; // m per cell; <= 0 = unbounded
|
||||||
public static float ErosionSeaMargin = 0.5f; // m above sea, carve floor
|
public static float ErosionSeaMargin = 0.5f; // m above sea, carve floor
|
||||||
public static int ErosionBrushRadius = 2; // px
|
public static int ErosionBrushRadius = 2; // px
|
||||||
public static float ErosionInertia = 0.05f;
|
public static float ErosionInertia = 0.35f;
|
||||||
public static float ErosionCapacity = 4.0f;
|
public static float ErosionCapacity = 4.0f;
|
||||||
public static float ErosionMinSlope = 0.01f; // m per px, capacity floor
|
public static float ErosionMinSlope = 0.02f; // m per px, capacity floor
|
||||||
public static float ErosionErodeRate = 0.3f;
|
public static float ErosionErodeRate = 0.12f;
|
||||||
public static float ErosionDepositRate = 0.3f;
|
public static float ErosionDepositRate = 0.15f;
|
||||||
public static float ErosionEvaporation = 0.02f;
|
public static float ErosionEvaporation = 0.004f;
|
||||||
public static float ErosionGravity = 4.0f;
|
public static float ErosionGravity = 4.0f;
|
||||||
|
|
||||||
// Island falloff shaping (task 11).
|
// Island falloff shaping (task 11).
|
||||||
|
|
@ -215,6 +225,7 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
||||||
if (data.ContainsKey("ErosionDropletCount")) ErosionDropletCount = (int)data["ErosionDropletCount"];
|
if (data.ContainsKey("ErosionDropletCount")) ErosionDropletCount = (int)data["ErosionDropletCount"];
|
||||||
if (data.ContainsKey("ErosionDropletLifetime")) ErosionDropletLifetime = (int)data["ErosionDropletLifetime"];
|
if (data.ContainsKey("ErosionDropletLifetime")) ErosionDropletLifetime = (int)data["ErosionDropletLifetime"];
|
||||||
if (data.ContainsKey("ErosionCarveCap")) ErosionCarveCap = (float)data["ErosionCarveCap"];
|
if (data.ContainsKey("ErosionCarveCap")) ErosionCarveCap = (float)data["ErosionCarveCap"];
|
||||||
|
if (data.ContainsKey("ErosionDepositCap")) ErosionDepositCap = (float)data["ErosionDepositCap"];
|
||||||
if (data.ContainsKey("ErosionSeaMargin")) ErosionSeaMargin = (float)data["ErosionSeaMargin"];
|
if (data.ContainsKey("ErosionSeaMargin")) ErosionSeaMargin = (float)data["ErosionSeaMargin"];
|
||||||
if (data.ContainsKey("ErosionBrushRadius")) ErosionBrushRadius = (int)data["ErosionBrushRadius"];
|
if (data.ContainsKey("ErosionBrushRadius")) ErosionBrushRadius = (int)data["ErosionBrushRadius"];
|
||||||
if (data.ContainsKey("ErosionInertia")) ErosionInertia = (float)data["ErosionInertia"];
|
if (data.ContainsKey("ErosionInertia")) ErosionInertia = (float)data["ErosionInertia"];
|
||||||
|
|
@ -235,6 +246,8 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
||||||
ErosionCarveCap = Mathf.Clamp(ErosionCarveCap, 0f, 60f);
|
ErosionCarveCap = Mathf.Clamp(ErosionCarveCap, 0f, 60f);
|
||||||
if (rawCount != ErosionDropletCount || rawLife != ErosionDropletLifetime || rawCap != ErosionCarveCap)
|
if (rawCount != ErosionDropletCount || rawLife != ErosionDropletLifetime || rawCap != ErosionCarveCap)
|
||||||
GD.PrintErr($"[ConfigManager] Erosion governor out of bounds — clamped: count {rawCount}->{ErosionDropletCount}, lifetime {rawLife}->{ErosionDropletLifetime}, cap {rawCap}->{ErosionCarveCap} m.");
|
GD.PrintErr($"[ConfigManager] Erosion governor out of bounds — clamped: count {rawCount}->{ErosionDropletCount}, lifetime {rawLife}->{ErosionDropletLifetime}, cap {rawCap}->{ErosionCarveCap} m.");
|
||||||
|
// Negative is meaningless; 0 is the documented "unbounded" escape hatch.
|
||||||
|
ErosionDepositCap = Mathf.Clamp(ErosionDepositCap, 0f, 60f);
|
||||||
ErosionSeaMargin = Mathf.Clamp(ErosionSeaMargin, 0f, 5f);
|
ErosionSeaMargin = Mathf.Clamp(ErosionSeaMargin, 0f, 5f);
|
||||||
ErosionBrushRadius = Mathf.Clamp(ErosionBrushRadius, 0, 8);
|
ErosionBrushRadius = Mathf.Clamp(ErosionBrushRadius, 0, 8);
|
||||||
ErosionInertia = Mathf.Clamp(ErosionInertia, 0f, 0.99f);
|
ErosionInertia = Mathf.Clamp(ErosionInertia, 0f, 0.99f);
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ namespace IslaApocalypse.Core
|
||||||
{
|
{
|
||||||
public ushort Version;
|
public ushort Version;
|
||||||
public int DropletCount, Lifetime, BrushRadius, SeedOffset;
|
public int DropletCount, Lifetime, BrushRadius, SeedOffset;
|
||||||
public float CarveCapM, SeaMarginM;
|
public float CarveCapM, DepositCapM, SeaMarginM;
|
||||||
public float Inertia, CapacityFactor, MinSlopeM;
|
public float Inertia, CapacityFactor, MinSlopeM;
|
||||||
public float ErodeRate, DepositRate, Evaporation, Gravity;
|
public float ErodeRate, DepositRate, Evaporation, Gravity;
|
||||||
public float CraterExclFactor;
|
public float CraterExclFactor;
|
||||||
|
|
@ -509,7 +509,8 @@ namespace IslaApocalypse.Core
|
||||||
}
|
}
|
||||||
e.DropletCount = reader.ReadInt32(); e.Lifetime = reader.ReadInt32();
|
e.DropletCount = reader.ReadInt32(); e.Lifetime = reader.ReadInt32();
|
||||||
e.BrushRadius = reader.ReadInt32(); e.SeedOffset = reader.ReadInt32();
|
e.BrushRadius = reader.ReadInt32(); e.SeedOffset = reader.ReadInt32();
|
||||||
e.CarveCapM = reader.ReadSingle(); e.SeaMarginM = reader.ReadSingle();
|
e.CarveCapM = reader.ReadSingle(); e.DepositCapM = reader.ReadSingle();
|
||||||
|
e.SeaMarginM = reader.ReadSingle();
|
||||||
e.Inertia = reader.ReadSingle(); e.CapacityFactor = reader.ReadSingle();
|
e.Inertia = reader.ReadSingle(); e.CapacityFactor = reader.ReadSingle();
|
||||||
e.MinSlopeM = reader.ReadSingle();
|
e.MinSlopeM = reader.ReadSingle();
|
||||||
e.ErodeRate = reader.ReadSingle(); e.DepositRate = reader.ReadSingle();
|
e.ErodeRate = reader.ReadSingle(); e.DepositRate = reader.ReadSingle();
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ using System;
|
||||||
/// then walks downhill with inertia, carrying water and sediment. Where the ground
|
/// then walks downhill with inertia, carrying water and sediment. Where the ground
|
||||||
/// is steep and it moves fast it ERODES (up to capacity, spread over a small brush
|
/// is steep and it moves fast it ERODES (up to capacity, spread over a small brush
|
||||||
/// so no single-cell spikes — the anti-artifact that killed the D8 predecessor);
|
/// so no single-cell spikes — the anti-artifact that killed the D8 predecessor);
|
||||||
/// where it flattens out it DEPOSITS, building valley floors and fans. Water
|
/// where it flattens out it DEPOSITS, building valley floors and fans, over the
|
||||||
|
/// SAME brush (task 18 — bilinear 4-cell deposition built isolated cones at gully
|
||||||
|
/// mouths; carving and dumping are now symmetric). Water
|
||||||
/// evaporates each step; the droplet dies at its lifetime, at the map edge, or on
|
/// evaporates each step; the droplet dies at its lifetime, at the map edge, or on
|
||||||
/// reaching the sea (its remaining sediment is lost to the ocean).
|
/// reaching the sea (its remaining sediment is lost to the ocean).
|
||||||
///
|
///
|
||||||
|
|
@ -20,9 +22,16 @@ using System;
|
||||||
/// The three hard governors (the pass provably cannot run away):
|
/// The three hard governors (the pass provably cannot run away):
|
||||||
/// 1. DropletCount — total droplets (the main detail/cost dial).
|
/// 1. DropletCount — total droplets (the main detail/cost dial).
|
||||||
/// 2. Lifetime — max steps per droplet; no infinite wandering.
|
/// 2. Lifetime — max steps per droplet; no infinite wandering.
|
||||||
/// 3. CarveCapM — max cumulative erosion depth per cell, in metres,
|
/// 3. CarveCapM — max erosion depth per cell, in metres, measured from the
|
||||||
/// enforced against a per-cell ledger. The runaway-trench
|
/// height the pass found and enforced against a per-cell NET
|
||||||
/// guard, and what keeps this a DETAILING pass.
|
/// displacement ledger. The runaway-trench guard, and the
|
||||||
|
/// dial that decides how deep trunk channels may cut.
|
||||||
|
/// 4. DepositCapM — max build-up per cell, the same ledger read the other way
|
||||||
|
/// (task 18). Brush-spreading alone does not bound a spike:
|
||||||
|
/// droplets on long paths carry far more sediment, and a
|
||||||
|
/// loaded droplet meeting a rise dumps min(rise, load) at
|
||||||
|
/// once. This makes "no deposit cones" a governor rather
|
||||||
|
/// than a hope. <= 0 disables it (the reference model).
|
||||||
///
|
///
|
||||||
/// The sea clamp (the "don't over-flood" guard): erosion never lowers any cell
|
/// The sea clamp (the "don't over-flood" guard): erosion never lowers any cell
|
||||||
/// below its local sea level + SeaMarginM, and cells already below sea are
|
/// below its local sea level + SeaMarginM, and cells already below sea are
|
||||||
|
|
@ -40,7 +49,11 @@ using System;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HydraulicErosion
|
public static class HydraulicErosion
|
||||||
{
|
{
|
||||||
public const ushort VERSION = 1;
|
// The EROS body version is owned by the format (Core) and read from there, not
|
||||||
|
// restated here: the version byte IS the payload layout's identity, so a local
|
||||||
|
// copy that drifts writes a v2 body stamped v1 and every reader shifts a field.
|
||||||
|
// (Caught doing exactly that in task 18 — mirrors TerrainDetailPass.VERSION.)
|
||||||
|
public const ushort VERSION = IslaApocalypse.Core.BlueprintFormat.EROS_VERSION;
|
||||||
|
|
||||||
// Deterministic RNG stream: seeded from resolvedSeed + this offset, so a seed
|
// Deterministic RNG stream: seeded from resolvedSeed + this offset, so a seed
|
||||||
// reproduces exactly and the stream is decorrelated from every noise field
|
// reproduces exactly and the stream is decorrelated from every noise field
|
||||||
|
|
@ -68,6 +81,7 @@ public static class HydraulicErosion
|
||||||
public int DropletCount; // governor 1
|
public int DropletCount; // governor 1
|
||||||
public int Lifetime; // governor 2
|
public int Lifetime; // governor 2
|
||||||
public float CarveCapM; // governor 3 (metres)
|
public float CarveCapM; // governor 3 (metres)
|
||||||
|
public float DepositCapM; // governor 4 (metres); <= 0 = unbounded
|
||||||
public float SeaMarginM; // sea clamp margin (metres)
|
public float SeaMarginM; // sea clamp margin (metres)
|
||||||
public int BrushRadius; // erosion brush radius, px
|
public int BrushRadius; // erosion brush radius, px
|
||||||
public float Inertia; // 0 = pure gradient descent, 1 = never turns
|
public float Inertia; // 0 = pure gradient descent, 1 = never turns
|
||||||
|
|
@ -89,7 +103,8 @@ public static class HydraulicErosion
|
||||||
public double ErodedVolumeM3; // 1 px = 1 m², so metres of depth sum to m³
|
public double ErodedVolumeM3; // 1 px = 1 m², so metres of depth sum to m³
|
||||||
public double DepositedVolumeM3;
|
public double DepositedVolumeM3;
|
||||||
public float MaxCellErosionM; // must end ≤ CarveCapM
|
public float MaxCellErosionM; // must end ≤ CarveCapM
|
||||||
public long ErodedCells; // cells with any net ledger erosion
|
public float MaxCellDepositM; // the deposit-spike metric (task 18)
|
||||||
|
public long ModifiedCells; // cells the pass touched at all
|
||||||
}
|
}
|
||||||
|
|
||||||
// PCG32 (O'Neill) — tiny, deterministic, trivially portable to C++.
|
// PCG32 (O'Neill) — tiny, deterministic, trivially portable to C++.
|
||||||
|
|
@ -122,8 +137,12 @@ public static class HydraulicErosion
|
||||||
float capUnits = p.CarveCapM / M_PER_UNIT;
|
float capUnits = p.CarveCapM / M_PER_UNIT;
|
||||||
if (p.DropletCount <= 0 || capUnits <= 0f) return stats;
|
if (p.DropletCount <= 0 || capUnits <= 0f) return stats;
|
||||||
|
|
||||||
// Per-cell cumulative-erosion ledger — governor 3's enforcement record.
|
// Per-cell NET displacement ledger, metres, positive = carved below where the
|
||||||
float[,] eroded = new float[mapSize, mapSize];
|
// pass found this cell, negative = built up above it. Governor 3's enforcement
|
||||||
|
// record: the cap bounds `net`, so it bounds erosion depth measured from the
|
||||||
|
// ORIGINAL height — deposit-then-carve at one cell cannot smuggle in extra
|
||||||
|
// depth, and carve-then-deposit correctly frees the headroom back up.
|
||||||
|
float[,] net = new float[mapSize, mapSize];
|
||||||
|
|
||||||
// Spawn weighting needs the seed's top height.
|
// Spawn weighting needs the seed's top height.
|
||||||
float hTop = float.MinValue;
|
float hTop = float.MinValue;
|
||||||
|
|
@ -227,18 +246,40 @@ public static class HydraulicErosion
|
||||||
if (dhM > 0f || sedimentM > capacityM)
|
if (dhM > 0f || sedimentM > capacityM)
|
||||||
{
|
{
|
||||||
// Moving uphill (fill the pit behind us, at most the rise) or
|
// Moving uphill (fill the pit behind us, at most the rise) or
|
||||||
// over capacity (drop a fraction of the surplus): DEPOSIT at
|
// over capacity (drop a fraction of the surplus): DEPOSIT over
|
||||||
// the OLD position, bilinear over its 4 cells.
|
// the SAME cone brush erosion uses (task 18). Bilinear 4-cell
|
||||||
|
// deposition — the reference model's — concentrated a whole
|
||||||
|
// droplet's load into one cell at gully mouths and built
|
||||||
|
// isolated cones (measured 15.5 m on seed 1280587109, task 17
|
||||||
|
// §6.1). Spreading it makes deposition the symmetric mirror of
|
||||||
|
// carving; total mass is unchanged, only its footprint.
|
||||||
float amountM = dhM > 0f ? MathF.Min(dhM, sedimentM)
|
float amountM = dhM > 0f ? MathF.Min(dhM, sedimentM)
|
||||||
: (sedimentM - capacityM) * p.DepositRate;
|
: (sedimentM - capacityM) * p.DepositRate;
|
||||||
if (amountM > 0f)
|
if (amountM > 0f)
|
||||||
{
|
{
|
||||||
float w00 = (1f - fx) * (1f - fy), w10 = fx * (1f - fy);
|
for (int b = 0; b < brushN; b++)
|
||||||
float w01 = (1f - fx) * fy, w11 = fx * fy;
|
{
|
||||||
sedimentM -= DepositCell(height, eroded, xi, yi, amountM * w00, stats, SeaAt, Excluded)
|
int cx = xi + brushDx[b], cy = yi + brushDy[b];
|
||||||
+ DepositCell(height, eroded, xi + 1, yi, amountM * w10, stats, SeaAt, Excluded)
|
if (cx < 0 || cx >= mapSize || cy < 0 || cy >= mapSize) continue;
|
||||||
+ DepositCell(height, eroded, xi, yi + 1, amountM * w01, stats, SeaAt, Excluded)
|
if (Excluded(cx, cy)) continue;
|
||||||
+ DepositCell(height, eroded, xi + 1, yi + 1, amountM * w11, stats, SeaAt, Excluded);
|
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];
|
||||||
|
// 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.
|
||||||
|
if (p.DepositCapM > 0f)
|
||||||
|
give = MathF.Min(give, MathF.Max(0f, p.DepositCapM + net[cx, cy]));
|
||||||
|
if (give <= 0f) continue;
|
||||||
|
height[cx, cy] = hCell + give / M_PER_UNIT;
|
||||||
|
if (net[cx, cy] == 0f) stats.ModifiedCells++;
|
||||||
|
net[cx, cy] -= give;
|
||||||
|
if (-net[cx, cy] > stats.MaxCellDepositM) stats.MaxCellDepositM = -net[cx, cy];
|
||||||
|
sedimentM -= give;
|
||||||
|
stats.DepositedVolumeM3 += give;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -258,13 +299,13 @@ public static class HydraulicErosion
|
||||||
if (hCell < sea) continue; // below-sea cells are read-only
|
if (hCell < sea) continue; // below-sea cells are read-only
|
||||||
float want = amountM * brushW[b];
|
float want = amountM * brushW[b];
|
||||||
float bySea = MathF.Max(0f, (hCell - (sea + p.SeaMarginM / M_PER_UNIT)) * M_PER_UNIT);
|
float bySea = MathF.Max(0f, (hCell - (sea + p.SeaMarginM / M_PER_UNIT)) * M_PER_UNIT);
|
||||||
float byCap = MathF.Max(0f, p.CarveCapM - eroded[cx, cy]);
|
float byCap = MathF.Max(0f, p.CarveCapM - net[cx, cy]);
|
||||||
float take = MathF.Min(want, MathF.Min(bySea, byCap));
|
float take = MathF.Min(want, MathF.Min(bySea, byCap));
|
||||||
if (take <= 0f) continue;
|
if (take <= 0f) continue;
|
||||||
height[cx, cy] = hCell - take / M_PER_UNIT;
|
height[cx, cy] = hCell - take / M_PER_UNIT;
|
||||||
if (eroded[cx, cy] == 0f) stats.ErodedCells++;
|
if (net[cx, cy] == 0f) stats.ModifiedCells++;
|
||||||
eroded[cx, cy] += take;
|
net[cx, cy] += take;
|
||||||
if (eroded[cx, cy] > stats.MaxCellErosionM) stats.MaxCellErosionM = eroded[cx, cy];
|
if (net[cx, cy] > stats.MaxCellErosionM) stats.MaxCellErosionM = net[cx, cy];
|
||||||
sedimentM += take;
|
sedimentM += take;
|
||||||
stats.ErodedVolumeM3 += take;
|
stats.ErodedVolumeM3 += take;
|
||||||
}
|
}
|
||||||
|
|
@ -284,27 +325,11 @@ public static class HydraulicErosion
|
||||||
if (stats.MaxCellErosionM > p.CarveCapM * (1f + 1e-5f))
|
if (stats.MaxCellErosionM > p.CarveCapM * (1f + 1e-5f))
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"[HydraulicErosion] CARVE-CAP VIOLATION: a cell accumulated {stats.MaxCellErosionM} m against cap {p.CarveCapM} m. Refusing to generate.");
|
$"[HydraulicErosion] CARVE-CAP VIOLATION: a cell accumulated {stats.MaxCellErosionM} m against cap {p.CarveCapM} m. Refusing to generate.");
|
||||||
|
if (p.DepositCapM > 0f && stats.MaxCellDepositM > p.DepositCapM * (1f + 1e-5f))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"[HydraulicErosion] DEPOSIT-CAP VIOLATION: a cell built up {stats.MaxCellDepositM} m against cap {p.DepositCapM} m. Refusing to generate.");
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deposits up to <paramref name="amountM"/> metres on one cell; returns what was
|
|
||||||
/// actually placed. Below-sea cells and crater-excluded cells take nothing —
|
|
||||||
/// deposition only ever raises LAND, so the coastline cannot move and the sea
|
|
||||||
/// cannot shallow. A cell's ledgered erosion is paid back first, so erode-then-
|
|
||||||
/// deposit at one cell frees cap headroom instead of double-counting.
|
|
||||||
/// </summary>
|
|
||||||
private static float DepositCell(float[,] height, float[,] eroded, int cx, int cy,
|
|
||||||
float amountM, Stats stats, Func<int, int, float> seaAt, Func<int, int, bool> excluded)
|
|
||||||
{
|
|
||||||
if (amountM <= 0f) return 0f;
|
|
||||||
float hCell = height[cx, cy];
|
|
||||||
if (hCell < seaAt(cx, cy)) return 0f;
|
|
||||||
if (excluded(cx, cy)) return 0f;
|
|
||||||
height[cx, cy] = hCell + amountM / M_PER_UNIT;
|
|
||||||
eroded[cx, cy] = MathF.Max(0f, eroded[cx, cy] - amountM);
|
|
||||||
stats.DepositedVolumeM3 += amountM;
|
|
||||||
return amountM;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
Tools/Scripts/HydraulicErosion.cs.uid
Normal file
1
Tools/Scripts/HydraulicErosion.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://hls3pvvnqci5
|
||||||
|
|
@ -383,6 +383,7 @@ public partial class MapGenerator : TextureRect
|
||||||
BrushRadius = ConfigManager.ErosionBrushRadius,
|
BrushRadius = ConfigManager.ErosionBrushRadius,
|
||||||
SeedOffset = HydraulicErosion.SEED_OFFSET,
|
SeedOffset = HydraulicErosion.SEED_OFFSET,
|
||||||
CarveCapM = ConfigManager.ErosionCarveCap,
|
CarveCapM = ConfigManager.ErosionCarveCap,
|
||||||
|
DepositCapM = ConfigManager.ErosionDepositCap,
|
||||||
SeaMarginM = ConfigManager.ErosionSeaMargin,
|
SeaMarginM = ConfigManager.ErosionSeaMargin,
|
||||||
Inertia = ConfigManager.ErosionInertia,
|
Inertia = ConfigManager.ErosionInertia,
|
||||||
CapacityFactor = ConfigManager.ErosionCapacity,
|
CapacityFactor = ConfigManager.ErosionCapacity,
|
||||||
|
|
@ -706,6 +707,7 @@ public partial class MapGenerator : TextureRect
|
||||||
DropletCount = ConfigManager.ErosionDropletCount,
|
DropletCount = ConfigManager.ErosionDropletCount,
|
||||||
Lifetime = ConfigManager.ErosionDropletLifetime,
|
Lifetime = ConfigManager.ErosionDropletLifetime,
|
||||||
CarveCapM = ConfigManager.ErosionCarveCap,
|
CarveCapM = ConfigManager.ErosionCarveCap,
|
||||||
|
DepositCapM = ConfigManager.ErosionDepositCap,
|
||||||
SeaMarginM = ConfigManager.ErosionSeaMargin,
|
SeaMarginM = ConfigManager.ErosionSeaMargin,
|
||||||
BrushRadius = ConfigManager.ErosionBrushRadius,
|
BrushRadius = ConfigManager.ErosionBrushRadius,
|
||||||
Inertia = ConfigManager.ErosionInertia,
|
Inertia = ConfigManager.ErosionInertia,
|
||||||
|
|
@ -727,8 +729,9 @@ public partial class MapGenerator : TextureRect
|
||||||
$"[MapGenerator] EROSION FLOOD-GUARD VIOLATION: render-map water pixels {wetBefore} -> {wetAfter}. Refusing to generate.");
|
$"[MapGenerator] EROSION FLOOD-GUARD VIOLATION: render-map water pixels {wetBefore} -> {wetAfter}. Refusing to generate.");
|
||||||
|
|
||||||
GD.Print($"{T()} [Erosion] v1: {st.Spawned} droplets ({st.SkippedNoLand} skipped), {st.Steps} steps, " +
|
GD.Print($"{T()} [Erosion] v1: {st.Spawned} droplets ({st.SkippedNoLand} skipped), {st.Steps} steps, " +
|
||||||
$"{(Time.GetTicksMsec() - tEro0) / 1000.0:F1}s wall. Eroded {st.ErodedVolumeM3:F0} m³ over {st.ErodedCells} cells " +
|
$"{(Time.GetTicksMsec() - tEro0) / 1000.0:F1}s wall. Eroded {st.ErodedVolumeM3:F0} m³ over {st.ModifiedCells} touched cells " +
|
||||||
$"(max cell {st.MaxCellErosionM:F2} m vs cap {p.CarveCapM:F2} m), deposited {st.DepositedVolumeM3:F0} m³. " +
|
$"(max cell carve {st.MaxCellErosionM:F2} m vs cap {p.CarveCapM:F2} m), deposited {st.DepositedVolumeM3:F0} m³ " +
|
||||||
|
$"(max cell deposit {st.MaxCellDepositM:F2} m vs cap {p.DepositCapM:F2} m). " +
|
||||||
$"Deaths: {st.DiedSea} sea / {st.DiedEdge} edge / {st.DiedDry} dry / {st.DiedLifetime} lifetime. " +
|
$"Deaths: {st.DiedSea} sea / {st.DiedEdge} edge / {st.DiedDry} dry / {st.DiedLifetime} lifetime. " +
|
||||||
$"Water pixels {wetBefore} -> {wetAfter} (flood guard holds).");
|
$"Water pixels {wetBefore} -> {wetAfter} (flood guard holds).");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -258,9 +258,9 @@ public partial class RoundTripHarness : Node
|
||||||
bool same = ea.Version == eb.Version
|
bool same = ea.Version == eb.Version
|
||||||
&& ea.DropletCount == eb.DropletCount && ea.Lifetime == eb.Lifetime
|
&& ea.DropletCount == eb.DropletCount && ea.Lifetime == eb.Lifetime
|
||||||
&& ea.BrushRadius == eb.BrushRadius && ea.SeedOffset == eb.SeedOffset;
|
&& ea.BrushRadius == eb.BrushRadius && ea.SeedOffset == eb.SeedOffset;
|
||||||
float[] fa = { ea.CarveCapM, ea.SeaMarginM, ea.Inertia, ea.CapacityFactor, ea.MinSlopeM,
|
float[] fa = { ea.CarveCapM, ea.DepositCapM, ea.SeaMarginM, ea.Inertia, ea.CapacityFactor, ea.MinSlopeM,
|
||||||
ea.ErodeRate, ea.DepositRate, ea.Evaporation, ea.Gravity, ea.CraterExclFactor };
|
ea.ErodeRate, ea.DepositRate, ea.Evaporation, ea.Gravity, ea.CraterExclFactor };
|
||||||
float[] fb = { eb.CarveCapM, eb.SeaMarginM, eb.Inertia, eb.CapacityFactor, eb.MinSlopeM,
|
float[] fb = { eb.CarveCapM, eb.DepositCapM, eb.SeaMarginM, eb.Inertia, eb.CapacityFactor, eb.MinSlopeM,
|
||||||
eb.ErodeRate, eb.DepositRate, eb.Evaporation, eb.Gravity, eb.CraterExclFactor };
|
eb.ErodeRate, eb.DepositRate, eb.Evaporation, eb.Gravity, eb.CraterExclFactor };
|
||||||
for (int i = 0; i < fa.Length; i++)
|
for (int i = 0; i < fa.Length; i++)
|
||||||
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false;
|
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue