feat: stepped river water + widen + lake-ender join + the task-22 nits (terrain-water task 23, C0b finale)
THE PAYOFF: rivers now carry water. Each carved main river becomes a chain of stepped flat water-body reaches — a new reach every RiverStepDropM (2 m) of bed descent, sitting RiverWaterDepthM (1.2 m) above its bed, strictly descending to the outlet. Reaches are ordinary water bodies (WBID cells + WBTB type 2 'river', fresh; WSRF derives from body levels as ever), so river water renders through the C1 path with the task-15 presence rule at its banks, untouched. Existing bodies are never overwritten — a river MEETS its lake or the sea. Measured on 1280587109: 298 reaches across 6 rivers, 184,620 wet px, levels stepping 278 m down to 39.5 m; in-engine, one valley frame renders surfaces 46.2-114.5 m. Beds widened (RiverWidthScale default 1.75). Lake-enders: the stem's pooling terminal IS a local minimum, so the first extension attempt (blind steepest descent) dead-ended in 0 steps — replaced with the lowground Dijkstra to the nearest classify-water cell; the E-lagoon river now joins its lake (87 px). Smoothing is applied ONLY to lowland routes: smoothing upland stems moved them off their carved valley floors into the walls (max cut 14.6 -> 27.3 m measured; split restores valley-floor fidelity). TYPE_RIVER added to the WBTB registry (parser validation extended; doc updated in the docs commit). Task-22 nits fixed: max-cut is now CUMULATIVE vs the pre-pass surface (the honest number: p95 carve 7.0 m; 339 cells island-wide exceed 20 m — localized outlet-gorge notches where stems cross deposit ridges, deepest 26 m); and 0_height/0_water are re-drawn and re-captured AFTER the river pass so the exported snapshots show beds and water. Guards: flood guard holds (water pixels unchanged around the carve; the water stage touches no heights), BIOME oracle md5-identical to the task-22 baseline, 0_water changed (that IS the river water), island top 457.65 m exact, crater core excluded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
148602b4c5
commit
8f5767308a
4 changed files with 210 additions and 13 deletions
|
|
@ -119,9 +119,16 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
||||||
// absolute floor above sea — the erosion flood-guard discipline: no river
|
// absolute floor above sea — the erosion flood-guard discipline: no river
|
||||||
// bed may create inland below-sea cells, so the rendered coastline cannot
|
// bed may create inland below-sea cells, so the rendered coastline cannot
|
||||||
// move even with rivers carved.
|
// move even with rivers carved.
|
||||||
|
// RiverStepDropM/RiverWaterDepthM (task 23): the stepped-water dials — each
|
||||||
|
// river is a chain of flat water-body reaches; a new reach starts every
|
||||||
|
// StepDrop metres of bed descent and sits WaterDepth metres above its bed.
|
||||||
|
// Smaller drop = more, finer steps = smoother water (the smoothing dial;
|
||||||
|
// tilted continuous-slope water is the deferred model B).
|
||||||
public static string Rivers = "off";
|
public static string Rivers = "off";
|
||||||
public static string RiverRoutingStyle = "lowground";
|
public static string RiverRoutingStyle = "lowground";
|
||||||
public static float RiverWidthScale = 1.0f;
|
public static float RiverWidthScale = 1.75f; // widened at the task-23 gate's ask
|
||||||
|
public static float RiverStepDropM = 2.0f;
|
||||||
|
public static float RiverWaterDepthM = 1.2f;
|
||||||
public static float RiverDepthScale = 1.0f;
|
public static float RiverDepthScale = 1.0f;
|
||||||
public static float RiverSeaMargin = 0.2f; // m above sea, bed floor
|
public static float RiverSeaMargin = 0.2f; // m above sea, bed floor
|
||||||
|
|
||||||
|
|
@ -330,6 +337,10 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
||||||
if (data.ContainsKey("RiverWidthScale")) RiverWidthScale = (float)data["RiverWidthScale"];
|
if (data.ContainsKey("RiverWidthScale")) RiverWidthScale = (float)data["RiverWidthScale"];
|
||||||
if (data.ContainsKey("RiverDepthScale")) RiverDepthScale = (float)data["RiverDepthScale"];
|
if (data.ContainsKey("RiverDepthScale")) RiverDepthScale = (float)data["RiverDepthScale"];
|
||||||
if (data.ContainsKey("RiverSeaMargin")) RiverSeaMargin = (float)data["RiverSeaMargin"];
|
if (data.ContainsKey("RiverSeaMargin")) RiverSeaMargin = (float)data["RiverSeaMargin"];
|
||||||
|
if (data.ContainsKey("RiverStepDropM")) RiverStepDropM = (float)data["RiverStepDropM"];
|
||||||
|
if (data.ContainsKey("RiverWaterDepthM")) RiverWaterDepthM = (float)data["RiverWaterDepthM"];
|
||||||
|
RiverStepDropM = Mathf.Clamp(RiverStepDropM, 0.25f, 10f);
|
||||||
|
RiverWaterDepthM = Mathf.Clamp(RiverWaterDepthM, 0.2f, 5f);
|
||||||
RiverWidthScale = Mathf.Clamp(RiverWidthScale, 0.1f, 5f);
|
RiverWidthScale = Mathf.Clamp(RiverWidthScale, 0.1f, 5f);
|
||||||
RiverDepthScale = Mathf.Clamp(RiverDepthScale, 0.1f, 5f);
|
RiverDepthScale = Mathf.Clamp(RiverDepthScale, 0.1f, 5f);
|
||||||
RiverSeaMargin = Mathf.Clamp(RiverSeaMargin, 0f, 5f);
|
RiverSeaMargin = Mathf.Clamp(RiverSeaMargin, 0f, 5f);
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ namespace IslaApocalypse.Core
|
||||||
{
|
{
|
||||||
public const byte TYPE_OCEAN = 0;
|
public const byte TYPE_OCEAN = 0;
|
||||||
public const byte TYPE_LAKE = 1;
|
public const byte TYPE_LAKE = 1;
|
||||||
|
public const byte TYPE_RIVER = 2; // task 23: a stepped river REACH (one flat level)
|
||||||
public const byte SALINITY_FRESH = 0;
|
public const byte SALINITY_FRESH = 0;
|
||||||
public const byte SALINITY_SALT = 1;
|
public const byte SALINITY_SALT = 1;
|
||||||
|
|
||||||
|
|
@ -432,7 +433,7 @@ namespace IslaApocalypse.Core
|
||||||
body.SurfaceLevel = reader.ReadSingle();
|
body.SurfaceLevel = reader.ReadSingle();
|
||||||
body.PixelCount = reader.ReadInt32();
|
body.PixelCount = reader.ReadInt32();
|
||||||
body.Centroid = new Vector2(reader.ReadSingle(), reader.ReadSingle());
|
body.Centroid = new Vector2(reader.ReadSingle(), reader.ReadSingle());
|
||||||
if (body.Type > WaterBodyInfo.TYPE_LAKE)
|
if (body.Type > WaterBodyInfo.TYPE_RIVER)
|
||||||
{
|
{
|
||||||
GD.PrintErr($"[MapDataParser] ERROR: water body {i} has unknown type {body.Type}.");
|
GD.PrintErr($"[MapDataParser] ERROR: water body {i} has unknown type {body.Type}.");
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,16 @@ public partial class MapGenerator : TextureRect
|
||||||
// only: biomes and WBID are already computed from classify — the oracle is
|
// only: biomes and WBID are already computed from classify — the oracle is
|
||||||
// untouched by construction. NO WATER — part 2b.
|
// untouched by construction. NO WATER — part 2b.
|
||||||
if (_riversOn)
|
if (_riversOn)
|
||||||
|
{
|
||||||
CarveRivers();
|
CarveRivers();
|
||||||
|
// The 0_height/0_water snapshots were captured before rivers existed —
|
||||||
|
// re-draw and re-capture so the exported PNGs show the carved beds and
|
||||||
|
// the new river water (task-22 nit 2).
|
||||||
|
DrawHeightStageTexture();
|
||||||
|
await CaptureStage("0_height");
|
||||||
|
DrawWaterStageTexture();
|
||||||
|
await CaptureStage("0_water");
|
||||||
|
}
|
||||||
|
|
||||||
if (ConfigManager.SkipRoads)
|
if (ConfigManager.SkipRoads)
|
||||||
{
|
{
|
||||||
|
|
@ -863,6 +872,36 @@ public partial class MapGenerator : TextureRect
|
||||||
for (int y = 0; y < MapSize; y++)
|
for (int y = 0; y < MapSize; y++)
|
||||||
if (_heightMap[x, y] > topAfter) topAfter = _heightMap[x, y];
|
if (_heightMap[x, y] > topAfter) topAfter = _heightMap[x, y];
|
||||||
|
|
||||||
|
// --- Stepped river water (task 23, part 2b): reaches as flat water bodies.
|
||||||
|
// Levels-not-cells, the existing model: WBID cells + WBTB entries per reach;
|
||||||
|
// the writer derives WSRF from body levels. Touches no heights, so the
|
||||||
|
// flood-guard count above stays valid; touches no classify data, so the
|
||||||
|
// BIOME oracle holds (0_water changes — that IS the river water).
|
||||||
|
ushort nextBodyId = 1;
|
||||||
|
foreach (var b in _waterBodies)
|
||||||
|
if (b.Id >= nextBodyId) nextBodyId = (ushort)(b.Id + 1);
|
||||||
|
var reaches = RiverCarvePass.AddSteppedWater(_heightMap, MapSize, _waterBodyIds,
|
||||||
|
nextBodyId, st.Carved, seaMap, seaFlat,
|
||||||
|
_impactCenter.X, _impactCenter.Y,
|
||||||
|
_impactRadius * ConfigManager.CraterErosionCore,
|
||||||
|
ConfigManager.RiverStepDropM, ConfigManager.RiverWaterDepthM);
|
||||||
|
long riverWetPx = 0;
|
||||||
|
foreach (var reach in reaches)
|
||||||
|
{
|
||||||
|
_waterBodies.Add(new WaterBodyInfo
|
||||||
|
{
|
||||||
|
Id = reach.Id,
|
||||||
|
Type = WaterBodyInfo.TYPE_RIVER,
|
||||||
|
Salinity = WaterBodyInfo.SALINITY_FRESH,
|
||||||
|
SurfaceLevel = reach.Level,
|
||||||
|
PixelCount = reach.PixelCount,
|
||||||
|
Centroid = new Vector2((float)reach.Cx, (float)reach.Cy)
|
||||||
|
});
|
||||||
|
riverWetPx += reach.PixelCount;
|
||||||
|
}
|
||||||
|
GD.Print($"{T()} [Rivers] water: {reaches.Count} stepped reaches across {st.Carved.Count} rivers, " +
|
||||||
|
$"{riverWetPx} wet px, step drop {ConfigManager.RiverStepDropM:F1} m, depth {ConfigManager.RiverWaterDepthM:F1} m.");
|
||||||
|
|
||||||
GD.Print($"{T()} [Rivers] v1 '{ConfigManager.RiverRoutingStyle}': plan {st.AnalysisSeconds:F1}s, " +
|
GD.Print($"{T()} [Rivers] v1 '{ConfigManager.RiverRoutingStyle}': plan {st.AnalysisSeconds:F1}s, " +
|
||||||
$"routing {st.RoutingSeconds:F1}s, carve {st.CarveSeconds:F1}s " +
|
$"routing {st.RoutingSeconds:F1}s, carve {st.CarveSeconds:F1}s " +
|
||||||
$"({(Time.GetTicksMsec() - tRiv0) / 1000.0:F1}s total). " +
|
$"({(Time.GetTicksMsec() - tRiv0) / 1000.0:F1}s total). " +
|
||||||
|
|
|
||||||
|
|
@ -84,12 +84,26 @@ public static class RiverCarvePass
|
||||||
public double VolumeM3;
|
public double VolumeM3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>The carved geometry the water stage consumes (main rivers only).</summary>
|
||||||
|
public class CarvedRiver
|
||||||
|
{
|
||||||
|
public string Name, Kind;
|
||||||
|
public bool Southern;
|
||||||
|
public long DrainagePx;
|
||||||
|
public List<(float x, float y)> Dense; // head → mouth, ~1-px samples
|
||||||
|
public float[] Bed; // raw units, monotone non-increasing
|
||||||
|
public float[] HalfW; // px
|
||||||
|
public bool ReachedWaterTerminal; // lake-enders: extension reached classify water
|
||||||
|
}
|
||||||
|
|
||||||
public class Stats
|
public class Stats
|
||||||
{
|
{
|
||||||
public List<RiverStat> Rivers = new();
|
public List<RiverStat> Rivers = new();
|
||||||
|
public List<CarvedRiver> Carved = new(); // for the stepped-water stage (task 23)
|
||||||
|
internal float[] PrePass; // cumulative-cut baseline
|
||||||
public long CarvedCells;
|
public long CarvedCells;
|
||||||
public double CarvedVolumeM3;
|
public double CarvedVolumeM3;
|
||||||
public float MaxCutM;
|
public float MaxCutM; // CUMULATIVE vs pre-pass heights (task-22 nit 1 fixed)
|
||||||
public double AnalysisSeconds, RoutingSeconds, CarveSeconds;
|
public double AnalysisSeconds, RoutingSeconds, CarveSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,6 +136,14 @@ public static class RiverCarvePass
|
||||||
|
|
||||||
// --- carve ---
|
// --- carve ---
|
||||||
t0 = secondsNow();
|
t0 = secondsNow();
|
||||||
|
// Pre-pass snapshot: max-cut is measured CUMULATIVELY against the heights
|
||||||
|
// this pass found, not per-write — overlapping stamps re-cut a cell and the
|
||||||
|
// per-write number understated the true deepest cut ~4× (task-22 nit 1).
|
||||||
|
float[] pre = new float[n * n];
|
||||||
|
for (int x = 0; x < n; x++)
|
||||||
|
for (int y = 0; y < n; y++)
|
||||||
|
pre[x * n + y] = height[x, y];
|
||||||
|
stats.PrePass = pre;
|
||||||
int riverIdx = 0;
|
int riverIdx = 0;
|
||||||
foreach (var t in plan.Trunks)
|
foreach (var t in plan.Trunks)
|
||||||
{
|
{
|
||||||
|
|
@ -140,10 +162,25 @@ public static class RiverCarvePass
|
||||||
var route = giantRoutes[gi]; gi++;
|
var route = giantRoutes[gi]; gi++;
|
||||||
var course = new List<(float x, float y)>(g.Course);
|
var course = new List<(float x, float y)>(g.Course);
|
||||||
course.Reverse(); // head → terminal
|
course.Reverse(); // head → terminal
|
||||||
|
|
||||||
|
// Lake-enders (task 23): the stem pools on dry ground short of its lake
|
||||||
|
// BECAUSE its pooling point is a local minimum — a blind descent walk
|
||||||
|
// dead-ends there immediately (measured: 0 steps). Route to the nearest
|
||||||
|
// classify-water cell with the same lowground Dijkstra the routed giants
|
||||||
|
// use, so the bed (and then the water) actually joins the lake.
|
||||||
|
bool reachedLake = false;
|
||||||
|
if (g.Kind == "lake-ender")
|
||||||
|
{
|
||||||
|
var ext = RouteToOcean(height, n, isClassifyWater,
|
||||||
|
(int)g.Terminal.x, (int)g.Terminal.y, STYLE_LOWGROUND, SeaAt);
|
||||||
|
if (ext.Count > 0) { route = ext; reachedLake = true; }
|
||||||
|
}
|
||||||
|
|
||||||
var rs = CarveRiver($"giant{gi}", g.Kind, g.DrainageAreaPx,
|
var rs = CarveRiver($"giant{gi}", g.Kind, g.DrainageAreaPx,
|
||||||
course, route, height, n, SeaAt, coreSq, craterCx, craterCy, p, stats);
|
course, route, height, n, SeaAt, coreSq, craterCx, craterCy, p, stats);
|
||||||
rs.SouthernCandidate = g.SouthernCandidate;
|
rs.SouthernCandidate = g.SouthernCandidate;
|
||||||
rs.ReachedOcean = g.Kind != "routed" || (route != null && route.Count > 0);
|
rs.ReachedOcean = g.Kind == "routed" ? (route != null && route.Count > 0) : reachedLake;
|
||||||
|
if (stats.Carved.Count > 0) stats.Carved[^1].ReachedWaterTerminal = reachedLake;
|
||||||
foreach (var trib in g.Tributaries)
|
foreach (var trib in g.Tributaries)
|
||||||
CarveTributary(trib, height, n, SeaAt, coreSq, craterCx, craterCy, p, stats);
|
CarveTributary(trib, height, n, SeaAt, coreSq, craterCx, craterCy, p, stats);
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +242,7 @@ public static class RiverCarvePass
|
||||||
/// or an empty list if no path exists (reported upstream, never asserted away).
|
/// or an empty list if no path exists (reported upstream, never asserted away).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static List<(float x, float y)> RouteToOcean(float[,] height, int n,
|
private static List<(float x, float y)> RouteToOcean(float[,] height, int n,
|
||||||
bool[] isOcean, int sx, int sy, byte style, Func<int, int, float> seaAt)
|
bool[] targets, int sx, int sy, byte style, Func<int, int, float> seaAt)
|
||||||
{
|
{
|
||||||
int total = n * n;
|
int total = n * n;
|
||||||
var gcost = new float[total];
|
var gcost = new float[total];
|
||||||
|
|
@ -227,7 +264,7 @@ public static class RiverCarvePass
|
||||||
int c = pq.Dequeue();
|
int c = pq.Dequeue();
|
||||||
if (closed[c]) continue;
|
if (closed[c]) continue;
|
||||||
closed[c] = true;
|
closed[c] = true;
|
||||||
if (isOcean[c]) { goal = c; break; }
|
if (targets[c]) { goal = c; break; }
|
||||||
int cx = c / n, cy = c % n;
|
int cx = c / n, cy = c % n;
|
||||||
float hc = height[cx, cy];
|
float hc = height[cx, cy];
|
||||||
for (int k = 0; k < 8; k++)
|
for (int k = 0; k < 8; k++)
|
||||||
|
|
@ -281,12 +318,17 @@ public static class RiverCarvePass
|
||||||
float[,] height, int n, Func<int, int, float> seaAt,
|
float[,] height, int n, Func<int, int, float> seaAt,
|
||||||
float coreSq, float craterCx, float craterCy, Params p, Stats stats)
|
float coreSq, float craterCx, float craterCy, Params p, Stats stats)
|
||||||
{
|
{
|
||||||
// Full head→mouth polyline: upland stem, then the lowland reach if any —
|
// Full head→mouth polyline: upland stem, then the lowland reach if any.
|
||||||
// then SMOOTHED (task 23) so the carved centreline carries no routing kinks.
|
// ONLY the lowland reach is smoothed: the Dijkstra 45° kinks live there, on
|
||||||
|
// near-flat ground where a rounded corner costs nothing. The upland stems
|
||||||
|
// already thread the carved valley FLOORS — smoothing them off-line cut
|
||||||
|
// valley walls (measured: max cut 14.6 → 27.3 m before this was split).
|
||||||
var pts = new List<(float x, float y)>(upland);
|
var pts = new List<(float x, float y)>(upland);
|
||||||
if (lowlandRoute != null && lowlandRoute.Count > 1)
|
if (lowlandRoute != null && lowlandRoute.Count > 1)
|
||||||
pts.AddRange(lowlandRoute.GetRange(1, lowlandRoute.Count - 1));
|
{
|
||||||
pts = SmoothCourse(pts);
|
var smoothedRoute = SmoothCourse(lowlandRoute);
|
||||||
|
pts.AddRange(smoothedRoute.GetRange(1, smoothedRoute.Count - 1));
|
||||||
|
}
|
||||||
|
|
||||||
// Densify to ~1-px samples (plan courses are decimated ×4).
|
// Densify to ~1-px samples (plan courses are decimated ×4).
|
||||||
var dense = new List<(float x, float y)>();
|
var dense = new List<(float x, float y)>();
|
||||||
|
|
@ -394,18 +436,122 @@ public static class RiverCarvePass
|
||||||
if (target < old)
|
if (target < old)
|
||||||
{
|
{
|
||||||
float cutM = (old - target) * M_PER_UNIT;
|
float cutM = (old - target) * M_PER_UNIT;
|
||||||
|
// Cumulative depth vs the PRE-PASS surface (nit 1): the
|
||||||
|
// honest "how deep did we cut here in total" number.
|
||||||
|
float cumM = (stats.PrePass[x * n + y] - target) * M_PER_UNIT;
|
||||||
height[x, y] = target;
|
height[x, y] = target;
|
||||||
stats.CarvedCells++;
|
stats.CarvedCells++;
|
||||||
stats.CarvedVolumeM3 += cutM;
|
stats.CarvedVolumeM3 += cutM;
|
||||||
if (cutM > stats.MaxCutM) stats.MaxCutM = cutM;
|
if (cumM > stats.MaxCutM) stats.MaxCutM = cumM;
|
||||||
if (cutM > rs.MaxCutM) rs.MaxCutM = cutM;
|
if (cumM > rs.MaxCutM) rs.MaxCutM = cumM;
|
||||||
rs.VolumeM3 += cutM;
|
rs.VolumeM3 += cutM;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name != null) stats.Rivers.Add(rs);
|
if (name != null)
|
||||||
|
{
|
||||||
|
stats.Rivers.Add(rs);
|
||||||
|
stats.Carved.Add(new CarvedRiver
|
||||||
|
{
|
||||||
|
Name = name, Kind = kind, DrainagePx = drainagePx,
|
||||||
|
Dense = dense, Bed = bed, HalfW = halfW
|
||||||
|
});
|
||||||
|
}
|
||||||
return rs;
|
return rs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The stepped-water builder (task 23, part 2b): segments each carved main
|
||||||
|
/// river into REACHES — flat water bodies stepping down the bed toward the
|
||||||
|
/// outlet — and stamps their ids into the WBID grid. Reuses the existing
|
||||||
|
/// levels-not-cells water model exactly: one body per reach, one flat level
|
||||||
|
/// each; the writer derives WSRF from body levels as it always has. The step
|
||||||
|
/// drops are the smoothing dial (smaller drop = more, finer steps); tilted
|
||||||
|
/// water is the deferred model B and is NOT built here.
|
||||||
|
///
|
||||||
|
/// Emission is plain data (no engine types): the caller turns reaches into
|
||||||
|
/// WBTB entries. Wet cells: inside the channel half-width, currently dry in
|
||||||
|
/// WBID, at/above sea (below-sea cells belong to the ocean/crater-seam rule),
|
||||||
|
/// bed below the reach level. Existing water bodies are never overwritten —
|
||||||
|
/// a river MEETS a lake or the sea, it does not repaint them.
|
||||||
|
/// </summary>
|
||||||
|
public class Reach
|
||||||
|
{
|
||||||
|
public ushort Id;
|
||||||
|
public string River;
|
||||||
|
public float Level; // raw units
|
||||||
|
public int PixelCount;
|
||||||
|
public double Cx, Cy; // centroid accumulators → mean
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Reach> AddSteppedWater(float[,] height, int mapSize,
|
||||||
|
ushort[,] wbid, ushort firstId, List<CarvedRiver> rivers,
|
||||||
|
float[,] seaMap, float seaFlat, float craterCx, float craterCy,
|
||||||
|
float craterCoreRadius, float stepDropM, float waterDepthM)
|
||||||
|
{
|
||||||
|
int n = mapSize;
|
||||||
|
float SeaAt(int x, int y) => seaMap != null ? seaMap[x, y] : seaFlat;
|
||||||
|
float coreSq = craterCoreRadius * craterCoreRadius;
|
||||||
|
var reaches = new List<Reach>();
|
||||||
|
ushort nextId = firstId;
|
||||||
|
|
||||||
|
foreach (var r in rivers)
|
||||||
|
{
|
||||||
|
int m = r.Dense.Count;
|
||||||
|
if (m < 2) continue;
|
||||||
|
int i = 0;
|
||||||
|
float lastLevel = float.MaxValue;
|
||||||
|
while (i < m)
|
||||||
|
{
|
||||||
|
// Reach spans from i while the bed stays within stepDropM of the
|
||||||
|
// reach's starting bed; its flat level sits waterDepthM above that
|
||||||
|
// start (deepening toward the next step — the pool behind a riffle).
|
||||||
|
float startBed = r.Bed[i];
|
||||||
|
float level = startBed + waterDepthM / M_PER_UNIT;
|
||||||
|
if (level >= lastLevel) // enforce strict descent
|
||||||
|
level = lastLevel - 0.01f / M_PER_UNIT;
|
||||||
|
int j = i;
|
||||||
|
while (j < m && r.Bed[j] > startBed - stepDropM / M_PER_UNIT) j++;
|
||||||
|
|
||||||
|
var reach = new Reach { Id = nextId, River = r.Name, Level = level };
|
||||||
|
for (int k2 = i; k2 < j; k2++)
|
||||||
|
{
|
||||||
|
float hw = r.HalfW[k2];
|
||||||
|
int x0 = (int)MathF.Floor(r.Dense[k2].x - hw), x1 = (int)MathF.Ceiling(r.Dense[k2].x + hw);
|
||||||
|
int y0 = (int)MathF.Floor(r.Dense[k2].y - hw), y1 = (int)MathF.Ceiling(r.Dense[k2].y + hw);
|
||||||
|
for (int x = x0; x <= x1; x++)
|
||||||
|
{
|
||||||
|
if (x < 0 || x >= n) continue;
|
||||||
|
for (int y = y0; y <= y1; y++)
|
||||||
|
{
|
||||||
|
if (y < 0 || y >= n) continue;
|
||||||
|
if (wbid[x, y] != 0) continue; // never repaint existing water
|
||||||
|
float rx = x - r.Dense[k2].x, ry = y - r.Dense[k2].y;
|
||||||
|
if (rx * rx + ry * ry > hw * hw) continue;
|
||||||
|
float ddx = x - craterCx, ddy = y - craterCy;
|
||||||
|
if (ddx * ddx + ddy * ddy < coreSq) continue;
|
||||||
|
float h = height[x, y];
|
||||||
|
float sea = SeaAt(x, y);
|
||||||
|
if (h < sea) continue; // ocean/seam territory
|
||||||
|
if (h >= level) continue; // bank above the water line
|
||||||
|
wbid[x, y] = nextId;
|
||||||
|
reach.PixelCount++;
|
||||||
|
reach.Cx += x; reach.Cy += y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reach.PixelCount > 0)
|
||||||
|
{
|
||||||
|
reach.Cx /= reach.PixelCount; reach.Cy /= reach.PixelCount;
|
||||||
|
reaches.Add(reach);
|
||||||
|
nextId++;
|
||||||
|
lastLevel = level;
|
||||||
|
}
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reaches;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue