Compare commits
No commits in common. "dc35389ecc3fffd4d31ace2e05874880cbfb6239" and "1eacd2297226a01c6eb70ecc328d2d854d3d6939" have entirely different histories.
dc35389ecc
...
1eacd22972
7 changed files with 25 additions and 515 deletions
|
|
@ -59,7 +59,7 @@ Every section: `[u32 tag][u64 payload-length in bytes][payload]`.
|
||||||
| `TDTL` / `0x4C544454` | 30 B: `u16 detailVersion` · `f32 reliefAmpM` · `f32 reliefFreqIslands` · `i32 reliefSeedOffset` · `f32 edgeAmpM` · `f32 edgeFreqIslands` · `i32 edgeSeedOffset` · `f32 edgeMaxShiftM` | Optional — present iff the terrain detail passes shaped this blueprint's `HGTS` (config `TerrainDetail: "v1"`, requires the curve). **`detailVersion` selects the body layout and a reader that does not recognise it SKIPS the section** (leaving detail metadata null) rather than misreading a differently shaped payload — the one section whose body is versioned rather than extended, because v1's layout was retired rather than grown. **v1 (retired, never shipped)** — shelf micro-relief + D8 drainage incision, 38 B; the incision produced grid-aligned artifacts and was reverted whole, so the only v1 payloads that exist are in that batch's own tree. **v2 (current)** — shelf micro-relief (±`reliefAmpM` output metres, shelf-ness weighted) + shelf-edge variation: a per-column shift of the curve's shelf/riser knot block K3/K4/K5, drawn from a Simplex field seeded `resolvedWorldSeed + edgeSeedOffset` at `edgeFreqIslands/MapSize`, amplitude ±`edgeAmpM` **metres of INPUT height** — a displacement of the shelf boundary contour, not an elevation change. `edgeAmpM` is recorded **as applied**, after the clamp to `edgeMaxShiftM` (the preset's band-squeeze bound), so the record always describes the terrain rather than the request. **Metadata only** — heights are already detailed. Machinery: `Tools/Scripts/TerrainDetailPass.cs`. |
|
| `TDTL` / `0x4C544454` | 30 B: `u16 detailVersion` · `f32 reliefAmpM` · `f32 reliefFreqIslands` · `i32 reliefSeedOffset` · `f32 edgeAmpM` · `f32 edgeFreqIslands` · `i32 edgeSeedOffset` · `f32 edgeMaxShiftM` | Optional — present iff the terrain detail passes shaped this blueprint's `HGTS` (config `TerrainDetail: "v1"`, requires the curve). **`detailVersion` selects the body layout and a reader that does not recognise it SKIPS the section** (leaving detail metadata null) rather than misreading a differently shaped payload — the one section whose body is versioned rather than extended, because v1's layout was retired rather than grown. **v1 (retired, never shipped)** — shelf micro-relief + D8 drainage incision, 38 B; the incision produced grid-aligned artifacts and was reverted whole, so the only v1 payloads that exist are in that batch's own tree. **v2 (current)** — shelf micro-relief (±`reliefAmpM` output metres, shelf-ness weighted) + shelf-edge variation: a per-column shift of the curve's shelf/riser knot block K3/K4/K5, drawn from a Simplex field seeded `resolvedWorldSeed + edgeSeedOffset` at `edgeFreqIslands/MapSize`, amplitude ±`edgeAmpM` **metres of INPUT height** — a displacement of the shelf boundary contour, not an elevation change. `edgeAmpM` is recorded **as applied**, after the clamp to `edgeMaxShiftM` (the preset's band-squeeze bound), so the record always describes the terrain rather than the request. **Metadata only** — heights are already detailed. Machinery: `Tools/Scripts/TerrainDetailPass.cs`. |
|
||||||
| `EROS` / `0x534F5245` | 67 B: `u16 erosionVersion` · `i32 dropletCount` · `i32 lifetime` · `i32 brushRadius` · `i32 seedOffset` · `f32 carveCapM` · `f32 depositCapM` · `f32 seaMarginM` · `f32 inertia` · `f32 capacityFactor` · `f32 minSlopeM` · `f32 erodeRate` · `f32 depositRate` · `f32 evaporation` · `f32 gravity` · `f32 craterCoreFactor` · `f32 craterFeatherFactor` · `u8 craterMode` | Optional — present iff the droplet hydraulic-erosion pass shaped this blueprint's `HGTS` (config `Erosion: "v1"`, terrain-water tasks 17–19). **`erosionVersion` selects the body layout; an unrecognised version is SKIPPED whole** (erosion metadata left null), same rule as `TDTL`. **v1 (task 17)** — 58 B, no `depositCapM`; deposition was bilinear over 4 cells and unbounded, which built isolated cones (measured 15.5 m). **v2 (task 18)** — 62 B; deposition brush-spread and per-cell bounded. **v3 (task 19, current)** — replaces v2's single `craterExclFactor` with `craterCoreFactor` (the protected strike core), `craterFeatherFactor` and `craterMode` (`0` = full, `1` = feather). Only v2+ payloads exist outside tasks 17–18's own batch trees. The FOUR governors are `dropletCount`/`lifetime`/`carveCapM`/`depositCapM`; both caps are enforced against one per-cell NET displacement ledger (positive = carved below the height the pass found, negative = built up above it) and asserted on exit. `seaMarginM` is the flood-guard clamp — no cell is carved below sea + margin, and below-sea cells are untouched in BOTH directions, so the rendered coastline cannot move **and the crater's flooded bay can be neither carved open nor silted shut regardless of `craterMode`**. Crater radii are FACTORS of `PRMS.CraterRadius`: nothing inside `craterCoreFactor ×` it is modified; `feather` ramps erosion 0→full from there out to `craterFeatherFactor ×` it, `full` applies full strength immediately. Droplets are deterministic from `resolvedWorldSeed + seedOffset` (PCG32). Remaining fields are the droplet-model strength dials; slopes/amounts in metres (1 raw = 251 m). All values recorded **as applied** (post config clamping). **Metadata only** — heights are already eroded, and the classify-side sections (`BIOM`/`WBID`/…) never saw the pass by design. Machinery: `Tools/Scripts/HydraulicErosion.cs`. |
|
| `EROS` / `0x534F5245` | 67 B: `u16 erosionVersion` · `i32 dropletCount` · `i32 lifetime` · `i32 brushRadius` · `i32 seedOffset` · `f32 carveCapM` · `f32 depositCapM` · `f32 seaMarginM` · `f32 inertia` · `f32 capacityFactor` · `f32 minSlopeM` · `f32 erodeRate` · `f32 depositRate` · `f32 evaporation` · `f32 gravity` · `f32 craterCoreFactor` · `f32 craterFeatherFactor` · `u8 craterMode` | Optional — present iff the droplet hydraulic-erosion pass shaped this blueprint's `HGTS` (config `Erosion: "v1"`, terrain-water tasks 17–19). **`erosionVersion` selects the body layout; an unrecognised version is SKIPPED whole** (erosion metadata left null), same rule as `TDTL`. **v1 (task 17)** — 58 B, no `depositCapM`; deposition was bilinear over 4 cells and unbounded, which built isolated cones (measured 15.5 m). **v2 (task 18)** — 62 B; deposition brush-spread and per-cell bounded. **v3 (task 19, current)** — replaces v2's single `craterExclFactor` with `craterCoreFactor` (the protected strike core), `craterFeatherFactor` and `craterMode` (`0` = full, `1` = feather). Only v2+ payloads exist outside tasks 17–18's own batch trees. The FOUR governors are `dropletCount`/`lifetime`/`carveCapM`/`depositCapM`; both caps are enforced against one per-cell NET displacement ledger (positive = carved below the height the pass found, negative = built up above it) and asserted on exit. `seaMarginM` is the flood-guard clamp — no cell is carved below sea + margin, and below-sea cells are untouched in BOTH directions, so the rendered coastline cannot move **and the crater's flooded bay can be neither carved open nor silted shut regardless of `craterMode`**. Crater radii are FACTORS of `PRMS.CraterRadius`: nothing inside `craterCoreFactor ×` it is modified; `feather` ramps erosion 0→full from there out to `craterFeatherFactor ×` it, `full` applies full strength immediately. Droplets are deterministic from `resolvedWorldSeed + seedOffset` (PCG32). Remaining fields are the droplet-model strength dials; slopes/amounts in metres (1 raw = 251 m). All values recorded **as applied** (post config clamping). **Metadata only** — heights are already eroded, and the classify-side sections (`BIOM`/`WBID`/…) never saw the pass by design. Machinery: `Tools/Scripts/HydraulicErosion.cs`. |
|
||||||
| `WBID` / `0x44494257` | `MapSize²` × `u16` water-body id, same pixel order as `HGTS` | Optional (absent = no water data, e.g. a legacy re-encode). `0` = no water, `1` = **the** ocean body, `2..N` = lakes. Ids assigned in deterministic scan order (X outer / Y inner, first-encountered pixel), lakes labeled with the **same 4-connectivity as `CalculateTrueOcean`**. Membership is exactly the generator's water classification — the biome grid's Ocean/Lake pixels and this grid's nonzero pixels are the same set **by construction** (shared predicates). Length must equal `2·MapSize²`. |
|
| `WBID` / `0x44494257` | `MapSize²` × `u16` water-body id, same pixel order as `HGTS` | Optional (absent = no water data, e.g. a legacy re-encode). `0` = no water, `1` = **the** ocean body, `2..N` = lakes. Ids assigned in deterministic scan order (X outer / Y inner, first-encountered pixel), lakes labeled with the **same 4-connectivity as `CalculateTrueOcean`**. Membership is exactly the generator's water classification — the biome grid's Ocean/Lake pixels and this grid's nonzero pixels are the same set **by construction** (shared predicates). Length must equal `2·MapSize²`. |
|
||||||
| `WBTB` / `0x42544257` | `i32 count`, then per body (20 B): `u16 id` · `u8 type` (0 ocean, 1 lake, 2 river — a stepped river REACH, one flat level per reach; task 23) · `u8 salinity` (0 fresh, 1 salt) · `f32 surfaceLevel` · `i32 pixelCount` · `f32 centroidX` · `f32 centroidY` | Optional, paired with `WBID`. **`surfaceLevel` is a documented TRANSITIONAL rule:** one flat level per body — `GetSeaLevel` at the body's pixel centroid (ocean: at the map centre) under the still-live latitude field; superseded by the flat-scalar sea model (minted, lands with the coast change set). The field's per-pixel slope is deliberately NOT baked into any section. **Salinity is a provisional default** (ocean salt, lake fresh) — a placeholder for the future fresh/salt irrigation mechanic, not a mechanic. |
|
| `WBTB` / `0x42544257` | `i32 count`, then per body (20 B): `u16 id` · `u8 type` (0 ocean, 1 lake) · `u8 salinity` (0 fresh, 1 salt) · `f32 surfaceLevel` · `i32 pixelCount` · `f32 centroidX` · `f32 centroidY` | Optional, paired with `WBID`. **`surfaceLevel` is a documented TRANSITIONAL rule:** one flat level per body — `GetSeaLevel` at the body's pixel centroid (ocean: at the map centre) under the still-live latitude field; superseded by the flat-scalar sea model (minted, lands with the coast change set). The field's per-pixel slope is deliberately NOT baked into any section. **Salinity is a provisional default** (ocean salt, lake fresh) — a placeholder for the future fresh/salt irrigation mechanic, not a mechanic. |
|
||||||
| `WSRF` / `0x46525357` | `MapSize²` × `u16` quantized water-surface elevation, same pixel order | Optional, paired with `WBID`. `0` is the reserved **no-water sentinel**; a real level `L` (raw height units) encodes as `1 + round(L × 32768)` so it can never encode to 0; decode `(q − 1)/32768` (`BlueprintFormat.EncodeWaterLevel`/`DecodeWaterLevel`). Covers `[0 … ~1.99997]` raw at `1/32768` raw ≈ **7.7 mm** of world height (1 raw = 251 m) — far finer than the 1 m voxel. Nonzero exactly where `WBID` is nonzero; the value is the pixel's body level. |
|
| `WSRF` / `0x46525357` | `MapSize²` × `u16` quantized water-surface elevation, same pixel order | Optional, paired with `WBID`. `0` is the reserved **no-water sentinel**; a real level `L` (raw height units) encodes as `1 + round(L × 32768)` so it can never encode to 0; decode `(q − 1)/32768` (`BlueprintFormat.EncodeWaterLevel`/`DecodeWaterLevel`). Covers `[0 … ~1.99997]` raw at `1/32768` raw ≈ **7.7 mm** of world height (1 raw = 251 m) — far finer than the 1 m voxel. Nonzero exactly where `WBID` is nonzero; the value is the pixel's body level. |
|
||||||
| `RDHW` / `0x57484452` | `i32 pathCount`, then per path: `i32 pointCount` + `pointCount` × (`f32 X` · `f32 Y`) | Highway tier. **Tags, not file position, identify the tier** — the v1 order-fragility is gone. Road sections may legitimately be **present but empty** (zero paths): the `SkipRoads` config toggle exports a road-less iteration blueprint. |
|
| `RDHW` / `0x57484452` | `i32 pathCount`, then per path: `i32 pointCount` + `pointCount` × (`f32 X` · `f32 Y`) | Highway tier. **Tags, not file position, identify the tier** — the v1 order-fragility is gone. Road sections may legitimately be **present but empty** (zero paths): the `SkipRoads` config toggle exports a road-less iteration blueprint. |
|
||||||
| `RDBR` / `0x52424452` | same layout | Branch tier. |
|
| `RDBR` / `0x52424452` | same layout | Branch tier. |
|
||||||
|
|
|
||||||
|
|
@ -113,42 +113,14 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
||||||
// terrain-aware), "lowground" follows the lowest ground and wanders like a
|
// terrain-aware), "lowground" follows the lowest ground and wanders like a
|
||||||
// real river — the task-22 gate decides which ships; "lowground" is the
|
// real river — the task-22 gate decides which ships; "lowground" is the
|
||||||
// provisional default pending that verdict. Width/depth scales are taste
|
// provisional default pending that verdict. Width/depth scales are taste
|
||||||
// dials on the flow-proportional bed profile. LOWGROUND is the LOCKED
|
// dials on the flow-proportional bed profile. RiverSeaMargin is the bed's
|
||||||
// default — the task-22/23 gate verdict ("short" stays available for the
|
|
||||||
// record). RiverSeaMargin is the bed's
|
|
||||||
// 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.75f; // widened at the task-23 gate's ask
|
public static float RiverWidthScale = 1.0f;
|
||||||
// Task 24 (the gate's polish): finer steps read as a descending river rather
|
public static float RiverDepthScale = 1.0f;
|
||||||
// than a pond staircase; deeper water sits contained in its banks.
|
|
||||||
// RiverTribWaterMinFlow is the drainage a TRIBUTARY reach needs to carry
|
|
||||||
// water (0 = water them end to end); above it the water TAPERS to dry over
|
|
||||||
// RiverTribTaperPx so a stream head fades instead of ending in a wall.
|
|
||||||
// RiverLakeMinTargetPx is the smallest water body a lake-ender may target.
|
|
||||||
// Task 25: RiverFillFraction fills the channel to a fraction of its LOCAL bed
|
|
||||||
// depth (a fixed height was a trickle at deep mouths and overtopped shallow
|
|
||||||
// heads); RiverWaterDepthM stays as the absolute minimum. RiverBankFlare
|
|
||||||
// widens the bank shoulder (half-widths beyond the channel) and it now uses a
|
|
||||||
// smootherstep, so water meets land as a shore instead of a wall.
|
|
||||||
// RiverTribWaterMinFlow defaults to 0 — tributaries carry water their full
|
|
||||||
// promoted length (the gate's preference), still fading at the tip.
|
|
||||||
public static float RiverStepDropM = 0.6f;
|
|
||||||
public static float RiverWaterDepthM = 1.0f;
|
|
||||||
public static float RiverFillFraction = 0.80f;
|
|
||||||
public static float RiverBankFlare = 3.2f;
|
|
||||||
public static float RiverBankMaxCutM = 3.0f; // shoulder-only cut cap (task 25)
|
|
||||||
public static int RiverTribWaterMinFlow = 0;
|
|
||||||
public static int RiverTribTaperPx = 120;
|
|
||||||
public static int RiverLakeMinTargetPx = 20000;
|
|
||||||
public static float RiverDepthScale = 1.5f; // deepened at the task-24 gate's ask
|
|
||||||
public static float RiverSeaMargin = 0.2f; // m above sea, bed floor
|
public static float RiverSeaMargin = 0.2f; // m above sea, bed floor
|
||||||
|
|
||||||
// Island falloff shaping (task 11).
|
// Island falloff shaping (task 11).
|
||||||
|
|
@ -356,22 +328,6 @@ 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"];
|
|
||||||
if (data.ContainsKey("RiverTribWaterMinFlow")) RiverTribWaterMinFlow = (int)data["RiverTribWaterMinFlow"];
|
|
||||||
if (data.ContainsKey("RiverTribTaperPx")) RiverTribTaperPx = (int)data["RiverTribTaperPx"];
|
|
||||||
if (data.ContainsKey("RiverLakeMinTargetPx")) RiverLakeMinTargetPx = (int)data["RiverLakeMinTargetPx"];
|
|
||||||
if (data.ContainsKey("RiverFillFraction")) RiverFillFraction = (float)data["RiverFillFraction"];
|
|
||||||
if (data.ContainsKey("RiverBankFlare")) RiverBankFlare = (float)data["RiverBankFlare"];
|
|
||||||
RiverFillFraction = Mathf.Clamp(RiverFillFraction, 0.1f, 1f);
|
|
||||||
if (data.ContainsKey("RiverBankMaxCutM")) RiverBankMaxCutM = (float)data["RiverBankMaxCutM"];
|
|
||||||
RiverBankFlare = Mathf.Clamp(RiverBankFlare, 1.2f, 8f);
|
|
||||||
RiverBankMaxCutM = Mathf.Clamp(RiverBankMaxCutM, 0f, 30f);
|
|
||||||
RiverTribWaterMinFlow = Mathf.Max(RiverTribWaterMinFlow, 0);
|
|
||||||
RiverTribTaperPx = Mathf.Clamp(RiverTribTaperPx, 0, 2000);
|
|
||||||
RiverLakeMinTargetPx = Mathf.Max(RiverLakeMinTargetPx, 0);
|
|
||||||
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,7 +47,6 @@ 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;
|
||||||
|
|
||||||
|
|
@ -163,24 +162,6 @@ namespace IslaApocalypse.Core
|
||||||
{
|
{
|
||||||
public static WorldBlueprint LoadMapData(string seedStr)
|
public static WorldBlueprint LoadMapData(string seedStr)
|
||||||
{
|
{
|
||||||
// ISLA_BLUEPRINT_PATH (task 25, FILE SAFETY): load an explicit blueprint
|
|
||||||
// FILE instead of the seed-named one in the runtime root. Capture and A/B
|
|
||||||
// tooling points this straight at a batch folder, so it never has to copy
|
|
||||||
// a blueprint over — or delete — the file the developer has staged at the
|
|
||||||
// root to view a world in 3D. Loud, so a forgotten env var cannot be
|
|
||||||
// mistaken for the root blueprint.
|
|
||||||
string envPath = OS.GetEnvironment("ISLA_BLUEPRINT_PATH");
|
|
||||||
if (!string.IsNullOrEmpty(envPath))
|
|
||||||
{
|
|
||||||
if (File.Exists(envPath))
|
|
||||||
{
|
|
||||||
GD.Print($"[MapDataParser] ⚠ ISLA_BLUEPRINT_PATH override: loading '{envPath}' " +
|
|
||||||
"(NOT the runtime-root blueprint).");
|
|
||||||
return LoadMapDataFromPath(envPath);
|
|
||||||
}
|
|
||||||
GD.PrintErr($"[MapDataParser] ISLA_BLUEPRINT_PATH set but '{envPath}' does not exist — " +
|
|
||||||
"falling back to the runtime-root blueprint.");
|
|
||||||
}
|
|
||||||
string filePath = ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}.dat");
|
string filePath = ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}.dat");
|
||||||
return LoadMapDataFromPath(filePath);
|
return LoadMapDataFromPath(filePath);
|
||||||
}
|
}
|
||||||
|
|
@ -451,7 +432,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_RIVER)
|
if (body.Type > WaterBodyInfo.TYPE_LAKE)
|
||||||
{
|
{
|
||||||
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,16 +237,7 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
@ -335,12 +326,12 @@ public partial class MapGenerator : TextureRect
|
||||||
// 6. Pull the image from the invisible monitor and save it!
|
// 6. Pull the image from the invisible monitor and save it!
|
||||||
Image capture = offscreenVP.GetTexture().GetImage();
|
Image capture = offscreenVP.GetTexture().GetImage();
|
||||||
string seedStr = _noise.Seed.ToString();
|
string seedStr = _noise.Seed.ToString();
|
||||||
string fileName = System.IO.Path.Combine(OutputDir(), $"Map_Seed_{seedStr}_{label}.png");
|
string fileName = $"user://Map_Seed_{seedStr}_{label}.png";
|
||||||
|
|
||||||
Error saveResult = capture.SavePng(fileName);
|
Error saveResult = capture.SavePng(fileName);
|
||||||
|
|
||||||
if (saveResult == Error.Ok)
|
if (saveResult == Error.Ok)
|
||||||
GD.Print($"{T()} Map saved: {fileName}");
|
GD.Print($"{T()} Map saved: {ProjectSettings.GlobalizePath(fileName)}");
|
||||||
else
|
else
|
||||||
GD.PrintErr($"{T()} Failed to save map. Godot Error code: {saveResult}");
|
GD.PrintErr($"{T()} Failed to save map. Godot Error code: {saveResult}");
|
||||||
|
|
||||||
|
|
@ -348,40 +339,18 @@ public partial class MapGenerator : TextureRect
|
||||||
offscreenVP.QueueFree();
|
offscreenVP.QueueFree();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Where generated artifacts go. Normally the runtime root (`user://`), but
|
|
||||||
/// ISLA_EXPORT_DIR redirects EVERYTHING this run writes — blueprints and stage
|
|
||||||
/// snapshots — into a task folder instead (task 25, FILE SAFETY). Batch and A/B
|
|
||||||
/// tooling sets it so a generation can never overwrite the blueprint the
|
|
||||||
/// developer has staged at the root to view a world in 3D, and so no cleanup
|
|
||||||
/// move/delete is needed afterwards. Loud on use.
|
|
||||||
/// </summary>
|
|
||||||
private string OutputDir()
|
|
||||||
{
|
|
||||||
string dir = OS.GetEnvironment("ISLA_EXPORT_DIR");
|
|
||||||
if (!string.IsNullOrEmpty(dir))
|
|
||||||
{
|
|
||||||
if (System.IO.Directory.Exists(dir)) return dir;
|
|
||||||
GD.PrintErr($"[MapGenerator] ISLA_EXPORT_DIR '{dir}' does not exist — writing to the runtime root instead.");
|
|
||||||
}
|
|
||||||
return ProjectSettings.GlobalizePath("user://");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExportMapData()
|
private void ExportMapData()
|
||||||
{
|
{
|
||||||
string seedStr = _noise.Seed.ToString();
|
string seedStr = _noise.Seed.ToString();
|
||||||
string outDir = OutputDir();
|
|
||||||
if (outDir != ProjectSettings.GlobalizePath("user://"))
|
|
||||||
GD.Print($"{T()} ⚠ ISLA_EXPORT_DIR override: writing artifacts to '{outDir}' (NOT the runtime root).");
|
|
||||||
|
|
||||||
// PRIMARY: the v2 tagged-section container (Core/Scripts/BLUEPRINT_FORMAT.md),
|
// PRIMARY: the v2 tagged-section container (Core/Scripts/BLUEPRINT_FORMAT.md),
|
||||||
// under the name the server looks for.
|
// under the name the server looks for.
|
||||||
string v2Path = System.IO.Path.Combine(outDir, $"MapData_Seed_{seedStr}.dat");
|
string v2Path = ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}.dat");
|
||||||
BlueprintWriter.WriteV2(v2Path, BuildBlueprint());
|
BlueprintWriter.WriteV2(v2Path, BuildBlueprint());
|
||||||
|
|
||||||
// SAFETY NET: the legacy v1 format beside it, until the developer has lived
|
// SAFETY NET: the legacy v1 format beside it, until the developer has lived
|
||||||
// with v2 across several regenerations. Removal is a future task.
|
// with v2 across several regenerations. Removal is a future task.
|
||||||
ExportMapDataV1(System.IO.Path.Combine(outDir, $"MapData_Seed_{seedStr}_v1.dat"));
|
ExportMapDataV1(ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}_v1.dat"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -867,17 +836,6 @@ public partial class MapGenerator : TextureRect
|
||||||
isClassifyWater[x * MapSize + y] = IsWaterPixel(x, y);
|
isClassifyWater[x * MapSize + y] = IsWaterPixel(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task 24: SIGNIFICANT water — cells of bodies at least RiverLakeMinTargetPx
|
|
||||||
// in size, from the water-bodies table the stage above already built. A
|
|
||||||
// lake-ender routes to this so it enters the lagoon, not a puddle.
|
|
||||||
var bigBodies = new System.Collections.Generic.HashSet<ushort>();
|
|
||||||
foreach (var b in _waterBodies)
|
|
||||||
if (b.PixelCount >= ConfigManager.RiverLakeMinTargetPx) bigBodies.Add(b.Id);
|
|
||||||
bool[] isSignificantWater = new bool[MapSize * MapSize];
|
|
||||||
for (int x = 0; x < MapSize; x++)
|
|
||||||
for (int y = 0; y < MapSize; y++)
|
|
||||||
isSignificantWater[x * MapSize + y] = bigBodies.Contains(_waterBodyIds[x, y]);
|
|
||||||
|
|
||||||
float southX = -1f, southY = -1f;
|
float southX = -1f, southY = -1f;
|
||||||
foreach (var t in _towns)
|
foreach (var t in _towns)
|
||||||
if (t.Position.Y > southY) { southX = t.Position.X; southY = t.Position.Y; }
|
if (t.Position.Y > southY) { southX = t.Position.X; southY = t.Position.Y; }
|
||||||
|
|
@ -888,16 +846,10 @@ public partial class MapGenerator : TextureRect
|
||||||
? RiverCarvePass.STYLE_SHORT : RiverCarvePass.STYLE_LOWGROUND,
|
? RiverCarvePass.STYLE_SHORT : RiverCarvePass.STYLE_LOWGROUND,
|
||||||
WidthScale = ConfigManager.RiverWidthScale,
|
WidthScale = ConfigManager.RiverWidthScale,
|
||||||
DepthScale = ConfigManager.RiverDepthScale,
|
DepthScale = ConfigManager.RiverDepthScale,
|
||||||
SeaMarginM = ConfigManager.RiverSeaMargin,
|
SeaMarginM = ConfigManager.RiverSeaMargin
|
||||||
TribWaterMinFlowPx = ConfigManager.RiverTribWaterMinFlow,
|
|
||||||
TribTaperPx = ConfigManager.RiverTribTaperPx,
|
|
||||||
FillFraction = ConfigManager.RiverFillFraction,
|
|
||||||
BankFlare = ConfigManager.RiverBankFlare,
|
|
||||||
BankMaxCutM = ConfigManager.RiverBankMaxCutM,
|
|
||||||
LakeMinTargetPx = ConfigManager.RiverLakeMinTargetPx
|
|
||||||
};
|
};
|
||||||
var st = RiverCarvePass.Apply(_heightMap, MapSize, isOcean, isClassifyWater,
|
var st = RiverCarvePass.Apply(_heightMap, MapSize, isOcean, isClassifyWater,
|
||||||
isSignificantWater, southX, southY, seaMap, seaFlat,
|
southX, southY, seaMap, seaFlat,
|
||||||
_impactCenter.X, _impactCenter.Y,
|
_impactCenter.X, _impactCenter.Y,
|
||||||
_impactRadius * ConfigManager.CraterErosionCore,
|
_impactRadius * ConfigManager.CraterErosionCore,
|
||||||
() => (Time.GetTicksMsec()) / 1000.0, p);
|
() => (Time.GetTicksMsec()) / 1000.0, p);
|
||||||
|
|
@ -911,45 +863,6 @@ 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,
|
|
||||||
ConfigManager.RiverFillFraction);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
int mainCarved = 0, tribCarved = 0;
|
|
||||||
foreach (var cr in st.Carved) { if (cr.Kind == "tributary") tribCarved++; else mainCarved++; }
|
|
||||||
int tribReaches = 0; long tribWet = 0;
|
|
||||||
foreach (var reach in reaches)
|
|
||||||
if (reach.River == "trib") { tribReaches++; tribWet += reach.PixelCount; }
|
|
||||||
GD.Print($"{T()} [Rivers] water: {reaches.Count} stepped reaches ({tribReaches} on tributaries) " +
|
|
||||||
$"across {mainCarved} mains + {tribCarved} tributaries, {riverWetPx} wet px " +
|
|
||||||
$"({tribWet} tributary), step drop {ConfigManager.RiverStepDropM:F2} m, depth {ConfigManager.RiverWaterDepthM:F1} m, " +
|
|
||||||
$"trib flow threshold {ConfigManager.RiverTribWaterMinFlow} px taper {ConfigManager.RiverTribTaperPx} px, " +
|
|
||||||
$"fill {ConfigManager.RiverFillFraction:P0} of bed depth (min {ConfigManager.RiverWaterDepthM:F1} m), bank flare {ConfigManager.RiverBankFlare:F1}x (shoulder cut cap {ConfigManager.RiverBankMaxCutM: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). " +
|
||||||
|
|
|
||||||
|
|
@ -109,48 +109,7 @@ Generates the entire 2D blueprint. Roughly in order:
|
||||||
sea + `RiverSeaMargin` everywhere (the erosion flood-guard discipline: below-sea cells
|
sea + `RiverSeaMargin` everywhere (the erosion flood-guard discipline: below-sea cells
|
||||||
read-only, zero new below-sea cells, coastline provably fixed; asserted per generation).
|
read-only, zero new below-sea cells, coastline provably fixed; asserted per generation).
|
||||||
Slots AFTER towns (town placement reads the render map and must not shift) and BEFORE roads.
|
Slots AFTER towns (town placement reads the render map and must not shift) and BEFORE roads.
|
||||||
Crater core excluded.
|
Crater core excluded. **NO WATER yet** — part 2b waters the gated routing style.
|
||||||
|
|
||||||
**River water (task 23, C0b finale)** — the payoff. Each carved main river becomes a chain of
|
|
||||||
**stepped flat water-body REACHES**: a new reach starts every `RiverStepDropM` (2 m) of bed
|
|
||||||
descent and sits `RiverWaterDepthM` (1.2 m) above its bed, each reach strictly lower than the
|
|
||||||
one upstream, stepping down to the sea (small vertical drops — riffles — smoothable purely by
|
|
||||||
the step dial; tilted continuous-slope water is the deferred model B). Reaches are ordinary
|
|
||||||
water bodies: WBID cells + a WBTB entry (`type 2 = river`, fresh) each; WSRF derives from body
|
|
||||||
levels exactly as for lakes, and river water renders through the C1/task-13 path with the
|
|
||||||
task-15 presence rule at its banks. Lake-enders route the last reach into their lake by the
|
|
||||||
lowground Dijkstra (a pooling terminal IS a local minimum — blind descent dead-ends there);
|
|
||||||
rivers meet lakes and the sea without repainting them (existing bodies are never overwritten).
|
|
||||||
Only the lowland reaches are ROUTE-SMOOTHED (RDP+Chaikin, the road pipeline): the upland stems
|
|
||||||
already thread carved valley floors, and smoothing them off-line cut valley walls (measured).
|
|
||||||
The BIOME oracle holds (classify untouched; `1_biomes` md5-identical); `0_water` and the
|
|
||||||
re-captured `0_height` change — that IS the river water and its beds (both snapshots are
|
|
||||||
re-captured after the river pass so the exported PNGs show them). Max-cut is now reported
|
|
||||||
CUMULATIVELY vs the pre-pass surface.
|
|
||||||
|
|
||||||
**Polish (task 24)** — tributaries carry water too, gated by along-course flow
|
|
||||||
(`RiverTribWaterMinFlow`, 0 = water them end to end) and **tapered** at the wet→dry
|
|
||||||
transition over `RiverTribTaperPx`: the strip narrows and its surface drops to the bed, so a
|
|
||||||
stream head fades instead of ending in a wall of water. Lake-enders route to the nearest
|
|
||||||
**significant** water body (`RiverLakeMinTargetPx`, 20k cells) — "nearest wet cell" was
|
|
||||||
satisfied by a 322-px puddle beside a 197k-px lagoon. Steps are finer (`RiverStepDropM` 0.6 m)
|
|
||||||
and beds deeper (`RiverDepthScale` 1.5, `RiverWaterDepthM` 2.2 m) so the chain reads as a graded
|
|
||||||
descent rather than a pond staircase: 1034 reaches, adjacent-reach level gaps median 0.12 m.
|
|
||||||
|
|
||||||
**Polish r2 (task 25)** — tributaries carry water their **full** promoted length
|
|
||||||
(`RiverTribWaterMinFlow` 0) while still fading at the tip (the taper zone is always
|
|
||||||
`RiverTribTaperPx` long, starting wherever water begins). The water surface is
|
|
||||||
`RiverFillFraction` (0.80) of the **local** bed depth rather than a fixed height, floored by
|
|
||||||
`RiverWaterDepthM` — a fixed height was a trickle in a 13 m mouth and over the rim at a 1.5 m
|
|
||||||
head. Bank shoulders flare `RiverBankFlare` (3.2) half-widths with a smootherstep so water meets
|
|
||||||
land as a shore, and the **shoulder** may not lower a cell more than `RiverBankMaxCutM` (3 m)
|
|
||||||
**measured against the pre-pass surface** — gentle ground flares, a ridge crossing keeps steep
|
|
||||||
walls, and max cut fell to 16.8 m with no cell past 20 m.
|
|
||||||
|
|
||||||
**FILE SAFETY** — `ISLA_BLUEPRINT_PATH` (load a specific blueprint file) and `ISLA_EXPORT_DIR`
|
|
||||||
(write all of a run's blueprints and snapshots into a task folder) exist so tooling never writes
|
|
||||||
to, or deletes from, the runtime root — the developer stages `MapData_Seed_*.dat` there to view
|
|
||||||
worlds in 3D. Batch drivers set both and contain no delete of any kind.
|
|
||||||
|
|
||||||
3. **Sea level and water** — sea level per the configured model (`SeaLevelModel`: `"flat"` scalar
|
3. **Sea level and water** — sea level per the configured model (`SeaLevelModel`: `"flat"` scalar
|
||||||
— the default, `SeaLevelValue` 0.15 — or the legacy `"field"` latitude Lerp); flood fill
|
— the default, `SeaLevelValue` 0.15 — or the legacy `"field"` latitude Lerp); flood fill
|
||||||
|
|
|
||||||
|
|
@ -66,30 +66,6 @@ public static class RiverCarvePass
|
||||||
public float WidthScale = 1.0f;
|
public float WidthScale = 1.0f;
|
||||||
public float DepthScale = 1.0f;
|
public float DepthScale = 1.0f;
|
||||||
public float SeaMarginM = 0.2f; // bed floor above sea, everywhere
|
public float SeaMarginM = 0.2f; // bed floor above sea, everywhere
|
||||||
// Task 24: a tributary reach is WET where its along-course flow exceeds this;
|
|
||||||
// upstream of that it TAPERS to dry over TribTaperPx rather than ending in a
|
|
||||||
// wall of water. 0 = water tributaries end to end.
|
|
||||||
public int TribWaterMinFlowPx = 0; // task 25: 0 = tributaries wet full length
|
|
||||||
public int TribTaperPx = 120;
|
|
||||||
// Task 25 (the gate's "trickle at the bottom of a ditch" note): the reach's
|
|
||||||
// water surface is a FRACTION of the local bed depth rather than a fixed
|
|
||||||
// height — the channel reads FILLED at every scale and cannot overfill onto
|
|
||||||
// the plain, which a fixed depth does at the shallow heads. WaterDepthM
|
|
||||||
// survives as the absolute minimum so tiny channels still hold water.
|
|
||||||
public float FillFraction = 0.80f;
|
|
||||||
// Bank shoulders flare this many half-widths beyond the channel (was a hard
|
|
||||||
// 2×) with a gentler-than-smoothstep curve, so water meets land as a shore.
|
|
||||||
public float BankFlare = 3.2f;
|
|
||||||
// ...but the SHOULDER may never lower a cell by more than this. A gentle flare
|
|
||||||
// across a ridge would otherwise cut a big notch: widening the flare alone took
|
|
||||||
// cells deeper than 20 m from 339 to 1917 (measured). With the cap, gentle
|
|
||||||
// ground still flares into a shore — which is where the "water in a groove"
|
|
||||||
// complaint lives — while a ridge crossing keeps steep walls, which is what a
|
|
||||||
// gorge actually looks like. The channel bed itself is not affected by this.
|
|
||||||
public float BankMaxCutM = 3.0f;
|
|
||||||
// Lake-enders route to the nearest water body of at least this size — the
|
|
||||||
// nearest wet PIXEL was a puddle (task-23 gate finding).
|
|
||||||
public int LakeMinTargetPx = 20_000;
|
|
||||||
public DrainageAnalysis.Params PlanParams = new();
|
public DrainageAnalysis.Params PlanParams = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,43 +84,17 @@ 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 float[] DepthM; // local bed depth, metres (task 25 fill)
|
|
||||||
public bool ReachedWaterTerminal; // lake-enders: extension reached classify water
|
|
||||||
// Task 24: along-course flow (px of drainage) per sample, and the first index
|
|
||||||
// that carries water. Between WetFrom-TaperPx and WetFrom the water tapers
|
|
||||||
// (narrowing and shallowing to the bed) so a stream head fades out.
|
|
||||||
public float[] Flow;
|
|
||||||
public int WetFrom;
|
|
||||||
public int TaperPx;
|
|
||||||
}
|
|
||||||
|
|
||||||
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; // CUMULATIVE vs pre-pass heights (task-22 nit 1 fixed)
|
public float MaxCutM;
|
||||||
public double AnalysisSeconds, RoutingSeconds, CarveSeconds;
|
public double AnalysisSeconds, RoutingSeconds, CarveSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <param name="isSignificantWater">Row-major mask of classify water belonging to
|
|
||||||
/// bodies of at least LakeMinTargetPx cells (task 24). Lake-enders route to THIS,
|
|
||||||
/// not to any wet pixel: the task-23 build routed one into a puddle a few hundred
|
|
||||||
/// px short of the obvious lagoon, because "nearest classify water" is satisfied
|
|
||||||
/// by a 3-cell pond.</param>
|
|
||||||
public static Stats Apply(float[,] height, int mapSize, bool[] isOcean,
|
public static Stats Apply(float[,] height, int mapSize, bool[] isOcean,
|
||||||
bool[] isClassifyWater, bool[] isSignificantWater, float southX, float southY,
|
bool[] isClassifyWater, float southX, float southY,
|
||||||
float[,] seaMap, float seaFlat,
|
float[,] seaMap, float seaFlat,
|
||||||
float craterCx, float craterCy, float craterCoreRadius,
|
float craterCx, float craterCy, float craterCoreRadius,
|
||||||
Func<double> secondsNow, Params p)
|
Func<double> secondsNow, Params p)
|
||||||
|
|
@ -172,14 +122,6 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
@ -198,31 +140,10 @@ 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")
|
|
||||||
{
|
|
||||||
// Target SIGNIFICANT water (task 24). Fall back to any classify water
|
|
||||||
// only if no significant body is reachable, so a seed whose lake-ender
|
|
||||||
// genuinely has only small ponds still connects rather than dead-ending.
|
|
||||||
var ext = RouteToOcean(height, n, isSignificantWater,
|
|
||||||
(int)g.Terminal.x, (int)g.Terminal.y, STYLE_LOWGROUND, SeaAt);
|
|
||||||
if (ext.Count == 0)
|
|
||||||
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) : reachedLake;
|
rs.ReachedOcean = g.Kind != "routed" || (route != null && route.Count > 0);
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
@ -230,61 +151,13 @@ public static class RiverCarvePass
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Route smoothing (task 23): the road pass's AAA pipeline, numerically ----
|
|
||||||
// RDP(4.0) decimation + 4 Chaikin corner-cutting passes, endpoints pinned —
|
|
||||||
// the same constants and structure as MapGenerator.SmoothPath, mirrored here
|
|
||||||
// because this pass is Godot-free. Kills the 8-connected Dijkstra 45° kinks;
|
|
||||||
// the bed then carves along the smoothed centreline.
|
|
||||||
private static List<(float x, float y)> SmoothCourse(List<(float x, float y)> raw)
|
|
||||||
{
|
|
||||||
if (raw.Count < 3) return raw;
|
|
||||||
var dec = Rdp(raw, 0, raw.Count - 1, 4.0f);
|
|
||||||
if (dec.Count < 3) return raw;
|
|
||||||
var sm = dec;
|
|
||||||
for (int pass = 0; pass < 4; pass++)
|
|
||||||
{
|
|
||||||
var nxt = new List<(float x, float y)>(sm.Count * 2) { sm[0] };
|
|
||||||
for (int i = 0; i + 1 < sm.Count; i++)
|
|
||||||
{
|
|
||||||
var a = sm[i]; var b = sm[i + 1];
|
|
||||||
nxt.Add((a.x * 0.75f + b.x * 0.25f, a.y * 0.75f + b.y * 0.25f));
|
|
||||||
nxt.Add((a.x * 0.25f + b.x * 0.75f, a.y * 0.25f + b.y * 0.75f));
|
|
||||||
}
|
|
||||||
nxt.Add(sm[^1]);
|
|
||||||
sm = nxt;
|
|
||||||
}
|
|
||||||
return sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<(float x, float y)> Rdp(List<(float x, float y)> pts, int i0, int i1, float tol)
|
|
||||||
{
|
|
||||||
if (i1 - i0 <= 1) return new List<(float x, float y)> { pts[i0], pts[i1] };
|
|
||||||
var a = pts[i0]; var b = pts[i1];
|
|
||||||
float abx = b.x - a.x, aby = b.y - a.y;
|
|
||||||
float abLen = MathF.Sqrt(abx * abx + aby * aby);
|
|
||||||
float maxD = 0f; int maxI = i0;
|
|
||||||
for (int i = i0 + 1; i < i1; i++)
|
|
||||||
{
|
|
||||||
float d = abLen < 1e-6f
|
|
||||||
? MathF.Sqrt((pts[i].x - a.x) * (pts[i].x - a.x) + (pts[i].y - a.y) * (pts[i].y - a.y))
|
|
||||||
: MathF.Abs(abx * (a.y - pts[i].y) - (a.x - pts[i].x) * aby) / abLen;
|
|
||||||
if (d > maxD) { maxD = d; maxI = i; }
|
|
||||||
}
|
|
||||||
if (maxD <= tol) return new List<(float x, float y)> { pts[i0], pts[i1] };
|
|
||||||
var left = Rdp(pts, i0, maxI, tol);
|
|
||||||
var right = Rdp(pts, maxI, i1, tol);
|
|
||||||
left.RemoveAt(left.Count - 1);
|
|
||||||
left.AddRange(right);
|
|
||||||
return left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deterministic Dijkstra from the start cell to the nearest ocean cell under
|
/// Deterministic Dijkstra from the start cell to the nearest ocean cell under
|
||||||
/// the selected style's cost model. Returns the path start → ocean (1-px steps),
|
/// the selected style's cost model. Returns the path start → ocean (1-px steps),
|
||||||
/// 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[] targets, int sx, int sy, byte style, Func<int, int, float> seaAt)
|
bool[] isOcean, 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];
|
||||||
|
|
@ -306,7 +179,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 (targets[c]) { goal = c; break; }
|
if (isOcean[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++)
|
||||||
|
|
@ -346,10 +219,7 @@ public static class RiverCarvePass
|
||||||
{
|
{
|
||||||
var course = new List<(float x, float y)>(trib.Course);
|
var course = new List<(float x, float y)>(trib.Course);
|
||||||
course.Reverse(); // head → confluence
|
course.Reverse(); // head → confluence
|
||||||
// Task 24: tributaries are registered as carved rivers (name "trib") so the
|
CarveRiver(null, "tributary", trib.DrainageAreaPx, course, null,
|
||||||
// water stage sees them — they carved but stayed dry in task 23. They are NOT
|
|
||||||
// added to stats.Rivers, so the per-river console table stays the 6 mains.
|
|
||||||
CarveRiver("trib", "tributary", trib.DrainageAreaPx, course, null,
|
|
||||||
height, n, seaAt, coreSq, craterCx, craterCy, p, stats);
|
height, n, seaAt, coreSq, craterCx, craterCy, p, stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,16 +234,9 @@ public static class RiverCarvePass
|
||||||
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.
|
||||||
// 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));
|
||||||
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)>();
|
||||||
|
|
@ -446,7 +309,7 @@ public static class RiverCarvePass
|
||||||
for (int i = 0; i < m; i++)
|
for (int i = 0; i < m; i++)
|
||||||
{
|
{
|
||||||
float hw = halfW[i];
|
float hw = halfW[i];
|
||||||
float outer = hw * p.BankFlare;
|
float outer = hw * 2f;
|
||||||
int cx0 = (int)MathF.Floor(dense[i].x - outer), cx1 = (int)MathF.Ceiling(dense[i].x + outer);
|
int cx0 = (int)MathF.Floor(dense[i].x - outer), cx1 = (int)MathF.Ceiling(dense[i].x + outer);
|
||||||
int cy0 = (int)MathF.Floor(dense[i].y - outer), cy1 = (int)MathF.Ceiling(dense[i].y + outer);
|
int cy0 = (int)MathF.Floor(dense[i].y - outer), cy1 = (int)MathF.Ceiling(dense[i].y + outer);
|
||||||
float rimH = bed[i] + depth[i] / M_PER_UNIT;
|
float rimH = bed[i] + depth[i] / M_PER_UNIT;
|
||||||
|
|
@ -472,188 +335,27 @@ public static class RiverCarvePass
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Shoulder: 0 at the rim → 1 at natural ground, over a flare of
|
float f = (r - hw) / hw; // 0..1 across the shoulder
|
||||||
// (BankFlare-1) half-widths. Smootherstep (6t⁵−15t⁴+10t³) leaves
|
f = f * f * (3f - 2f * f); // smoothstep
|
||||||
// the rim nearly tangent to the water plane, so the bank reads as
|
|
||||||
// a shore rather than the wall a plain smoothstep left.
|
|
||||||
float f = (r - hw) / MathF.Max(1e-3f, hw * (p.BankFlare - 1f));
|
|
||||||
if (f > 1f) f = 1f;
|
|
||||||
f = f * f * f * (f * (6f * f - 15f) + 10f);
|
|
||||||
target = rimH + (old - rimH) * f;
|
target = rimH + (old - rimH) * f;
|
||||||
// Clamp against the PRE-PASS surface, not the current height:
|
|
||||||
// overlapping stamps re-visit a cell, so a per-write cap lets each
|
|
||||||
// pass take another BankMaxCut (measured: capping against `old`
|
|
||||||
// changed the carved volume by 1 m³ out of 1.29 M — i.e. nothing).
|
|
||||||
float shoulderFloor = stats.PrePass[x * n + y] - p.BankMaxCutM / M_PER_UNIT;
|
|
||||||
if (target < shoulderFloor) target = shoulderFloor;
|
|
||||||
}
|
}
|
||||||
float floor = sea + p.SeaMarginM / M_PER_UNIT;
|
float floor = sea + p.SeaMarginM / M_PER_UNIT;
|
||||||
if (target < floor) target = floor;
|
if (target < floor) target = floor;
|
||||||
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 (cumM > stats.MaxCutM) stats.MaxCutM = cumM;
|
if (cutM > stats.MaxCutM) stats.MaxCutM = cutM;
|
||||||
if (cumM > rs.MaxCutM) rs.MaxCutM = cumM;
|
if (cutM > rs.MaxCutM) rs.MaxCutM = cutM;
|
||||||
rs.VolumeM3 += cutM;
|
rs.VolumeM3 += cutM;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name != null)
|
if (name != null) stats.Rivers.Add(rs);
|
||||||
{
|
|
||||||
if (kind != "tributary") stats.Rivers.Add(rs);
|
|
||||||
// Along-course flow: drainage grows from a headwater trickle to the full
|
|
||||||
// figure at the mouth. Quadratic in t so the substantial lower half
|
|
||||||
// dominates — a first-order stand-in for real accumulation, which the
|
|
||||||
// plan only records per-river.
|
|
||||||
var flow = new float[m];
|
|
||||||
for (int i = 0; i < m; i++)
|
|
||||||
{
|
|
||||||
float t = m > 1 ? (float)i / (m - 1) : 1f;
|
|
||||||
flow[i] = drainagePx * (0.05f + 0.95f * t * t);
|
|
||||||
}
|
|
||||||
int wetFrom = 0;
|
|
||||||
if (kind == "tributary" && p.TribWaterMinFlowPx > 0)
|
|
||||||
{
|
|
||||||
wetFrom = m; // dry unless the threshold is met
|
|
||||||
for (int i = 0; i < m; i++)
|
|
||||||
if (flow[i] >= p.TribWaterMinFlowPx) { wetFrom = i; break; }
|
|
||||||
}
|
|
||||||
stats.Carved.Add(new CarvedRiver
|
|
||||||
{
|
|
||||||
Name = name, Kind = kind, DrainagePx = drainagePx,
|
|
||||||
Dense = dense, Bed = bed, HalfW = halfW, DepthM = depth,
|
|
||||||
Flow = flow, WetFrom = wetFrom, TaperPx = p.TribTaperPx
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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, float fillFraction)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
// Task 24: start at the wet-from index (tributary threshold; 0 for mains),
|
|
||||||
// but back up by the taper length so the transition is a FADE, not a wall.
|
|
||||||
int i = Math.Max(0, r.WetFrom - r.TaperPx);
|
|
||||||
if (i >= m) continue; // entirely below threshold: dry
|
|
||||||
// The fade zone is always TaperPx long starting at i. With a threshold it
|
|
||||||
// spans (WetFrom-Taper → WetFrom); with tributaries fully watered
|
|
||||||
// (WetFrom = 0, task 25) it spans the first TaperPx from the HEAD, so a
|
|
||||||
// full-length tributary still fades in at its tip instead of starting as a
|
|
||||||
// wall of water.
|
|
||||||
int taperStart = i, taperEnd = Math.Min(m - 1, i + r.TaperPx);
|
|
||||||
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];
|
|
||||||
// Fill the channel: the surface sits at FillFraction of the LOCAL bed
|
|
||||||
// depth, never below the absolute minimum. A fixed height made deep
|
|
||||||
// mouths a trickle and overtopped shallow heads; a fraction is right at
|
|
||||||
// both ends and cannot spill onto the plain.
|
|
||||||
float localDepthM = r.DepthM != null ? r.DepthM[i] : waterDepthM;
|
|
||||||
float fillM = MathF.Max(waterDepthM, localDepthM * fillFraction);
|
|
||||||
if (fillM > localDepthM) fillM = localDepthM; // never above the rim
|
|
||||||
float level = startBed + fillM / 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++)
|
|
||||||
{
|
|
||||||
// Taper: 0 at the dry end of the fade zone → 1 at full water. The
|
|
||||||
// water narrows AND its surface drops to the bed, so a stream head
|
|
||||||
// thins out and disappears instead of ending in a flat wall.
|
|
||||||
float taper = 1f;
|
|
||||||
if (taperEnd > taperStart && k2 < taperEnd)
|
|
||||||
taper = (float)(k2 - taperStart) / (taperEnd - taperStart);
|
|
||||||
if (taper <= 0.02f) continue;
|
|
||||||
float hw = r.HalfW[k2] * (0.25f + 0.75f * taper);
|
|
||||||
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
|
|
||||||
// The tapered surface sits between the bed and the reach
|
|
||||||
// level, so the wet strip shallows as it narrows.
|
|
||||||
float localLevel = taper >= 1f
|
|
||||||
? level
|
|
||||||
: r.Bed[k2] + (level - r.Bed[k2]) * taper;
|
|
||||||
if (h >= localLevel) 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://dufsppmyn4g8v
|
|
||||||
Loading…
Reference in a new issue