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
|
|
@ -49,7 +49,9 @@ namespace IslaApocalypse.Core
|
||||||
// current) inserts the DEPOSIT CAP governor after the carve cap — deposition is
|
// current) inserts the DEPOSIT CAP governor after the carve cap — deposition is
|
||||||
// now brush-spread and per-cell bounded. Same reader rule as TDTL: an unknown
|
// 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.
|
// body version is skipped whole rather than misread into plausible nonsense.
|
||||||
public const ushort EROS_VERSION = 2;
|
// 3 (task 19, current) replaces the single crater exclusion factor with the
|
||||||
|
// protected-core factor, a feather-band factor, and the crater mode byte.
|
||||||
|
public const ushort EROS_VERSION = 3;
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,9 @@ namespace IslaApocalypse.Core
|
||||||
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
|
||||||
writer.Write(e.Evaporation); writer.Write(e.Gravity); // 2 × f32
|
writer.Write(e.Evaporation); writer.Write(e.Gravity); // 2 × f32
|
||||||
writer.Write(e.CraterExclFactor); // f32
|
writer.Write(e.CraterCoreFactor); // f32
|
||||||
|
writer.Write(e.CraterFeatherFactor); // f32
|
||||||
|
writer.Write(e.CraterMode); // u8
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)
|
private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,32 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
||||||
public static float ErosionEvaporation = 0.004f;
|
public static float ErosionEvaporation = 0.004f;
|
||||||
public static float ErosionGravity = 4.0f;
|
public static float ErosionGravity = 4.0f;
|
||||||
|
|
||||||
|
// How erosion treats the crater surrounds (task 19). The task-17 hard
|
||||||
|
// 1.2 × CraterRadius cutoff left a visible un-eroded disc: measured, the carve
|
||||||
|
// writes only inside 0.80 × and its displacement is exactly 0 beyond that, so
|
||||||
|
// 620 811 LAND cells of ordinary terrain were being held smooth for no
|
||||||
|
// geometric reason. Now only the deep strike core is protected.
|
||||||
|
// "full" — full-strength erosion right up to the core boundary. The
|
||||||
|
// crater formed after the terrain and has weathered since.
|
||||||
|
// "feather" — erosion ramps 0→full across CraterErosionCore →
|
||||||
|
// CraterErosionFeather (the detail pass's shape), so the crater
|
||||||
|
// reads as younger, less-weathered, and there is no seam at all.
|
||||||
|
// Radii are FACTORS of CraterRadius. The flooded bay and its sea connection do
|
||||||
|
// NOT depend on these: below-sea cells are read-only in both directions.
|
||||||
|
public static string CraterErosionMode = "feather";
|
||||||
|
public static float CraterErosionCore = CRATER_EROSION_CORE_DEFAULT;
|
||||||
|
public static float CraterErosionFeather = CRATER_EROSION_FEATHER_DEFAULT;
|
||||||
|
// 0.80 = the carve's OWN extent (MapGenerator's physicalCraterRadius). Keeping the
|
||||||
|
// core at least this wide is what makes erosion and the carve touch DISJOINT cells,
|
||||||
|
// which is what keeps the post-carve flood guard exactly zero: the carve runs after
|
||||||
|
// erosion and scales height toward the sea target, so it AMPLIFIES any erosion delta
|
||||||
|
// inside its radius and can push a hair-above-sea cell across the waterline. Measured
|
||||||
|
// at a 0.50 core: 79 cells newly below the rendered sea, 113 310 below-sea cells
|
||||||
|
// disturbed. A smaller core reclaims nothing extra either — the over-protected annulus
|
||||||
|
// is 0.80x-1.2x, entirely outside the carve.
|
||||||
|
public const float CRATER_EROSION_CORE_DEFAULT = 0.80f;
|
||||||
|
public const float CRATER_EROSION_FEATHER_DEFAULT = 1.05f;
|
||||||
|
|
||||||
// Island falloff shaping (task 11).
|
// Island falloff shaping (task 11).
|
||||||
//
|
//
|
||||||
// CoastProfile: "wide" adds the submarine shelf — the height curve is identity
|
// CoastProfile: "wide" adds the submarine shelf — the height curve is identity
|
||||||
|
|
@ -246,6 +272,29 @@ 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.");
|
||||||
|
// Crater erosion treatment (task 19)
|
||||||
|
if (data.ContainsKey("CraterErosionMode"))
|
||||||
|
{
|
||||||
|
string cm = (string)data["CraterErosionMode"];
|
||||||
|
if (cm == "full" || cm == "feather")
|
||||||
|
CraterErosionMode = cm;
|
||||||
|
else
|
||||||
|
GD.PrintErr($"[ConfigManager] Unknown CraterErosionMode '{cm}'. Keeping '{CraterErosionMode}'.");
|
||||||
|
}
|
||||||
|
if (data.ContainsKey("CraterErosionCore")) CraterErosionCore = (float)data["CraterErosionCore"];
|
||||||
|
if (data.ContainsKey("CraterErosionFeather")) CraterErosionFeather = (float)data["CraterErosionFeather"];
|
||||||
|
CraterErosionCore = Mathf.Clamp(CraterErosionCore, 0f, 3f);
|
||||||
|
CraterErosionFeather = Mathf.Clamp(CraterErosionFeather, 0f, 4f);
|
||||||
|
// A feather band that does not extend past the core is not a band; say so
|
||||||
|
// rather than silently behaving like "full".
|
||||||
|
if (CraterErosionMode == "feather" && CraterErosionFeather <= CraterErosionCore)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"[ConfigManager] CraterErosionFeather {CraterErosionFeather:F2} must exceed " +
|
||||||
|
$"CraterErosionCore {CraterErosionCore:F2} — the ramp would have zero width. " +
|
||||||
|
$"Restoring {CRATER_EROSION_FEATHER_DEFAULT:F2}.");
|
||||||
|
CraterErosionFeather = CRATER_EROSION_FEATHER_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
// Negative is meaningless; 0 is the documented "unbounded" escape hatch.
|
// Negative is meaningless; 0 is the documented "unbounded" escape hatch.
|
||||||
ErosionDepositCap = Mathf.Clamp(ErosionDepositCap, 0f, 60f);
|
ErosionDepositCap = Mathf.Clamp(ErosionDepositCap, 0f, 60f);
|
||||||
ErosionSeaMargin = Mathf.Clamp(ErosionSeaMargin, 0f, 5f);
|
ErosionSeaMargin = Mathf.Clamp(ErosionSeaMargin, 0f, 5f);
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,8 @@ namespace IslaApocalypse.Core
|
||||||
public float CarveCapM, DepositCapM, 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 CraterCoreFactor, CraterFeatherFactor;
|
||||||
|
public byte CraterMode; // 0 = full, 1 = feather
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WorldBlueprint
|
public class WorldBlueprint
|
||||||
|
|
@ -515,7 +516,8 @@ namespace IslaApocalypse.Core
|
||||||
e.MinSlopeM = reader.ReadSingle();
|
e.MinSlopeM = reader.ReadSingle();
|
||||||
e.ErodeRate = reader.ReadSingle(); e.DepositRate = reader.ReadSingle();
|
e.ErodeRate = reader.ReadSingle(); e.DepositRate = reader.ReadSingle();
|
||||||
e.Evaporation = reader.ReadSingle(); e.Gravity = reader.ReadSingle();
|
e.Evaporation = reader.ReadSingle(); e.Gravity = reader.ReadSingle();
|
||||||
e.CraterExclFactor = reader.ReadSingle();
|
e.CraterCoreFactor = reader.ReadSingle(); e.CraterFeatherFactor = reader.ReadSingle();
|
||||||
|
e.CraterMode = reader.ReadByte();
|
||||||
blueprint.Erosion = e;
|
blueprint.Erosion = e;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,12 @@ using System;
|
||||||
/// read-only — never eroded, never deposited on. Land stays land, sea stays sea;
|
/// read-only — never eroded, never deposited on. Land stays land, sea stays sea;
|
||||||
/// the rendered coastline cannot move. Deposition only raises land cells.
|
/// the rendered coastline cannot move. Deposition only raises land cells.
|
||||||
///
|
///
|
||||||
/// The crater exclusion: no cell within CraterExclRadius of the impact centre is
|
/// The crater treatment (task 19): no cell within the protected strike CORE is
|
||||||
/// modified (droplets may traverse). The carve remains the final authority on its
|
/// modified (droplets may traverse), and outside it either FULL strength applies
|
||||||
/// own terrain.
|
/// 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
|
/// 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,
|
/// 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;
|
public const float M_PER_UNIT = 251f;
|
||||||
|
|
||||||
// Crater exclusion factor: erosion stays outside 1.2 × CraterRadius — fully
|
// --- Crater treatment (task 19) ---
|
||||||
// clear of both the physical carve (0.80×) and the detail feather (1.05×).
|
//
|
||||||
public const float CRATER_EXCL_FACTOR = 1.2f;
|
// 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
|
// Spawn: droplets source in the mountains, never the ocean. A land point is
|
||||||
// accepted with probability SPAWN_FLOOR + (1-SPAWN_FLOOR) · relative elevation,
|
// 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 DepositRate; // fraction of surplus sediment dropped per step
|
||||||
public float Evaporation; // water lost per step (fraction)
|
public float Evaporation; // water lost per step (fraction)
|
||||||
public float Gravity; // speed gain per metre of drop
|
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
|
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.
|
/// bound is violated on exit — the caller treats that as a build failure.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Stats Apply(float[,] height, int mapSize, float[,] seaMap, float seaFlat,
|
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 stats = new Stats();
|
||||||
var rng = new Pcg32(p.Seed);
|
var rng = new Pcg32(p.Seed);
|
||||||
|
|
@ -173,12 +193,25 @@ public static class HydraulicErosion
|
||||||
for (int j = 0; j < brushN; j++) brushW[j] /= wSum;
|
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;
|
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;
|
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++)
|
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];
|
int cx = xi + brushDx[b], cy = yi + brushDy[b];
|
||||||
if (cx < 0 || cx >= mapSize || cy < 0 || cy >= mapSize) continue;
|
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];
|
float hCell = height[cx, cy];
|
||||||
// Below-sea cells are read-only in BOTH directions: no
|
// Below-sea cells are read-only in BOTH directions: no
|
||||||
// submarine deltas, so the rendered coastline cannot move.
|
// submarine deltas, so the rendered coastline cannot move.
|
||||||
if (hCell < SeaAt(cx, cy)) continue;
|
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
|
// Governor 4: the ledger read the other way. net is negative
|
||||||
// where the cell has already been built up, so the headroom
|
// where the cell has already been built up, so the headroom
|
||||||
// is cap + net.
|
// is cap + net.
|
||||||
|
|
@ -293,11 +327,12 @@ public static class HydraulicErosion
|
||||||
{
|
{
|
||||||
int cx = xi + brushDx[b], cy = yi + brushDy[b];
|
int cx = xi + brushDx[b], cy = yi + brushDy[b];
|
||||||
if (cx < 0 || cx >= mapSize || cy < 0 || cy >= mapSize) continue;
|
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 sea = SeaAt(cx, cy);
|
||||||
float hCell = height[cx, cy];
|
float hCell = height[cx, cy];
|
||||||
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] * wCrater;
|
||||||
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 - net[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));
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,12 @@ public partial class MapGenerator : TextureRect
|
||||||
// Hydraulic erosion (task 17): output-only droplet pass on the RENDER map,
|
// Hydraulic erosion (task 17): output-only droplet pass on the RENDER map,
|
||||||
// after detail, before the crater carve. The classify map never sees it.
|
// after detail, before the crater carve. The classify map never sees it.
|
||||||
private bool _erosionOn;
|
private bool _erosionOn;
|
||||||
|
private byte _craterErosionMode;
|
||||||
|
|
||||||
|
// The carve's radius as a fraction of CraterRadius — the ONLY place this number
|
||||||
|
// lives on the generator side. Erosion's protected core must be at least this
|
||||||
|
// wide or the two passes overlap; see the guard before the erosion call.
|
||||||
|
private const float CRATER_CARVE_FACTOR = 0.80f;
|
||||||
|
|
||||||
// Task-10 detail passes (shelf micro-relief + shelf-edge variation): gated by
|
// Task-10 detail passes (shelf micro-relief + shelf-edge variation): gated by
|
||||||
// TerrainDetail, active only with the curve on (both are defined in terms of
|
// TerrainDetail, active only with the curve on (both are defined in terms of
|
||||||
|
|
@ -114,6 +120,8 @@ public partial class MapGenerator : TextureRect
|
||||||
_curveKnots = ConfigManager.TerrainCurve == "v5" ? HeightCurve.V5 : null;
|
_curveKnots = ConfigManager.TerrainCurve == "v5" ? HeightCurve.V5 : null;
|
||||||
_curveOn = _curveKnots != null;
|
_curveOn = _curveKnots != null;
|
||||||
_erosionOn = ConfigManager.Erosion == "v1";
|
_erosionOn = ConfigManager.Erosion == "v1";
|
||||||
|
_craterErosionMode = ConfigManager.CraterErosionMode == "feather"
|
||||||
|
? HydraulicErosion.CRATER_MODE_FEATHER : HydraulicErosion.CRATER_MODE_FULL;
|
||||||
// (The monotonicity assertion now runs inside GenerateTopography, against the
|
// (The monotonicity assertion now runs inside GenerateTopography, against the
|
||||||
// effective per-seed curve, once hMaxSeed is known.)
|
// effective per-seed curve, once hMaxSeed is known.)
|
||||||
// Classify gets its own array whenever ANY render-only pass diverges the two
|
// Classify gets its own array whenever ANY render-only pass diverges the two
|
||||||
|
|
@ -392,7 +400,9 @@ public partial class MapGenerator : TextureRect
|
||||||
DepositRate = ConfigManager.ErosionDepositRate,
|
DepositRate = ConfigManager.ErosionDepositRate,
|
||||||
Evaporation = ConfigManager.ErosionEvaporation,
|
Evaporation = ConfigManager.ErosionEvaporation,
|
||||||
Gravity = ConfigManager.ErosionGravity,
|
Gravity = ConfigManager.ErosionGravity,
|
||||||
CraterExclFactor = HydraulicErosion.CRATER_EXCL_FACTOR
|
CraterCoreFactor = ConfigManager.CraterErosionCore,
|
||||||
|
CraterFeatherFactor = ConfigManager.CraterErosionFeather,
|
||||||
|
CraterMode = _craterErosionMode
|
||||||
} : null,
|
} : null,
|
||||||
TerrainDetail = _detailOn ? new TerrainDetailInfo
|
TerrainDetail = _detailOn ? new TerrainDetailInfo
|
||||||
{
|
{
|
||||||
|
|
@ -638,7 +648,7 @@ public partial class MapGenerator : TextureRect
|
||||||
// `raw`; neither can move a column below the red ceiling or above the cap,
|
// `raw`; neither can move a column below the red ceiling or above the cap,
|
||||||
// because K2 and K6 never move and the curve is monotonic between them.
|
// because K2 and K6 never move and the curve is monotonic between them.
|
||||||
float reliefAmpRaw = ConfigManager.ShelfReliefAmp / 251f;
|
float reliefAmpRaw = ConfigManager.ShelfReliefAmp / 251f;
|
||||||
float physicalCraterRadius = _impactRadius * 0.80f;
|
float physicalCraterRadius = _impactRadius * CRATER_CARVE_FACTOR;
|
||||||
for (int x = 0; x < MapSize; x++)
|
for (int x = 0; x < MapSize; x++)
|
||||||
{
|
{
|
||||||
for (int y = 0; y < MapSize; y++)
|
for (int y = 0; y < MapSize; y++)
|
||||||
|
|
@ -700,6 +710,17 @@ public partial class MapGenerator : TextureRect
|
||||||
for (int y = 0; y < MapSize; y++)
|
for (int y = 0; y < MapSize; y++)
|
||||||
seaMap[x, y] = GetSeaLevel(_tempMap[x, y]);
|
seaMap[x, y] = GetSeaLevel(_tempMap[x, y]);
|
||||||
}
|
}
|
||||||
|
// Erosion's protected core must cover the carve, or the two passes touch the
|
||||||
|
// same cells and the carve — which runs AFTER erosion and scales height
|
||||||
|
// toward the sea target — amplifies erosion's delta across the waterline.
|
||||||
|
// Refusing to fail silently: the in-pass flood guard below cannot see this,
|
||||||
|
// because it measures before the carve.
|
||||||
|
if (ConfigManager.CraterErosionCore < CRATER_CARVE_FACTOR)
|
||||||
|
GD.PrintErr($"[MapGenerator] ⚠ CraterErosionCore {ConfigManager.CraterErosionCore:F2} is " +
|
||||||
|
$"inside the carve radius ({CRATER_CARVE_FACTOR:F2} × CraterRadius). Erosion will modify " +
|
||||||
|
"carve-authored terrain, and the crater carve will then amplify those deltas — expect a " +
|
||||||
|
"handful of rendered-waterline crossings inside the bay that the erosion flood guard cannot see.");
|
||||||
|
|
||||||
long wetBefore = CountRenderWaterPixels(seaMap, seaFlat);
|
long wetBefore = CountRenderWaterPixels(seaMap, seaFlat);
|
||||||
|
|
||||||
var p = new HydraulicErosion.Params
|
var p = new HydraulicErosion.Params
|
||||||
|
|
@ -717,17 +738,22 @@ public partial class MapGenerator : TextureRect
|
||||||
DepositRate = ConfigManager.ErosionDepositRate,
|
DepositRate = ConfigManager.ErosionDepositRate,
|
||||||
Evaporation = ConfigManager.ErosionEvaporation,
|
Evaporation = ConfigManager.ErosionEvaporation,
|
||||||
Gravity = ConfigManager.ErosionGravity,
|
Gravity = ConfigManager.ErosionGravity,
|
||||||
|
CraterMode = _craterErosionMode,
|
||||||
Seed = _noise.Seed + HydraulicErosion.SEED_OFFSET
|
Seed = _noise.Seed + HydraulicErosion.SEED_OFFSET
|
||||||
};
|
};
|
||||||
var st = HydraulicErosion.Apply(_heightMap, MapSize, seaMap, seaFlat,
|
var st = HydraulicErosion.Apply(_heightMap, MapSize, seaMap, seaFlat,
|
||||||
_impactCenter.X, _impactCenter.Y,
|
_impactCenter.X, _impactCenter.Y,
|
||||||
_impactRadius * HydraulicErosion.CRATER_EXCL_FACTOR, p);
|
_impactRadius * ConfigManager.CraterErosionCore,
|
||||||
|
_impactRadius * ConfigManager.CraterErosionFeather, p);
|
||||||
|
|
||||||
long wetAfter = CountRenderWaterPixels(seaMap, seaFlat);
|
long wetAfter = CountRenderWaterPixels(seaMap, seaFlat);
|
||||||
if (wetAfter != wetBefore)
|
if (wetAfter != wetBefore)
|
||||||
throw new System.InvalidOperationException(
|
throw new System.InvalidOperationException(
|
||||||
$"[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] crater '{ConfigManager.CraterErosionMode}': core {_impactRadius * ConfigManager.CraterErosionCore:F0} px" +
|
||||||
|
(ConfigManager.CraterErosionMode == "feather"
|
||||||
|
? $" -> feather to {_impactRadius * ConfigManager.CraterErosionFeather:F0} px" : " (hard edge, full strength beyond)") + ".");
|
||||||
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.ModifiedCells} touched cells " +
|
$"{(Time.GetTicksMsec() - tEro0) / 1000.0:F1}s wall. Eroded {st.ErodedVolumeM3:F0} m³ over {st.ModifiedCells} touched cells " +
|
||||||
$"(max cell carve {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³ " +
|
||||||
|
|
|
||||||
|
|
@ -257,11 +257,12 @@ public partial class RoundTripHarness : Node
|
||||||
var ea = a.Erosion; var eb = b.Erosion;
|
var ea = a.Erosion; var eb = b.Erosion;
|
||||||
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
|
||||||
|
&& ea.CraterMode == eb.CraterMode;
|
||||||
float[] fa = { ea.CarveCapM, ea.DepositCapM, 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.CraterCoreFactor, ea.CraterFeatherFactor };
|
||||||
float[] fb = { eb.CarveCapM, eb.DepositCapM, 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.CraterCoreFactor, eb.CraterFeatherFactor };
|
||||||
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;
|
||||||
if (!same) { GD.PrintErr("[Harness] EROS fields differ."); return false; }
|
if (!same) { GD.PrintErr("[Harness] EROS fields differ."); return false; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue