feat: tributaries wet full length; water fills the channel; banks flare as shores (terrain-water task 25)
The gate's two river notes. FILL THE TRIBUTARIES. RiverTribWaterMinFlow defaults to 0, so every promoted tributary carries water its whole length (800 of 1574 reaches are now on tributaries, 65,364 of 268,863 wet px). The graceful tip survives: the fade zone is now always TaperPx long starting wherever water begins, so with a threshold it spans (WetFrom-Taper -> WetFrom) and with full watering it spans the first TaperPx from the HEAD. Fill is about LENGTH, taper is about the END. FILL THE CHANNEL, NOT THE FLOOR. The water surface is now RiverFillFraction (80%) of the LOCAL bed depth rather than a fixed height, floored by RiverWaterDepthM as an absolute minimum. A fixed height is wrong at both ends — a trickle in a 13 m mouth and over the rim at a 1.5 m head — where a fraction is right at every scale and cannot spill onto the plain. BANKS AS SHORES. The shoulder flares RiverBankFlare (3.2, was a hard 2x) half-widths with a smootherstep instead of smoothstep, so the rim leaves nearly tangent to the water plane. That alone cut too much where a course crosses a ridge (cells deeper than 20 m went 339 -> 1917, max 26 -> 37 m), so the SHOULDER may not lower a cell more than RiverBankMaxCutM (3 m) — measured against the PRE-PASS surface, because a per-write cap let overlapping stamps each take another 3 m and changed the carved volume by 1 m3 out of 1.29 M. Gentle ground still flares into a shore; a ridge crossing keeps steep walls, which is what a gorge looks like. Net result is better than BOTH predecessors: max cut 16.8 m (task 24: 26.0), zero cells past 20 m (task 24: 339), carve p95 5.26 m. Guards unchanged: BIOME oracle md5-identical to the task-22 baseline, 0 newly-below-sea, 0 below-sea cells modified, lowest carved cell exactly sea+margin, island top 457.65 m exact, crater core excluded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3fd14c0389
commit
c1f62f6899
2 changed files with 66 additions and 10 deletions
|
|
@ -133,9 +133,19 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
|||
// 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 = 2.2f;
|
||||
public static int RiverTribWaterMinFlow = 40000;
|
||||
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
|
||||
|
|
@ -351,6 +361,12 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -69,8 +69,24 @@ public static class RiverCarvePass
|
|||
// 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 = 40_000;
|
||||
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;
|
||||
|
|
@ -101,6 +117,7 @@ public static class RiverCarvePass
|
|||
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
|
||||
|
|
@ -429,7 +446,7 @@ public static class RiverCarvePass
|
|||
for (int i = 0; i < m; i++)
|
||||
{
|
||||
float hw = halfW[i];
|
||||
float outer = hw * 2f;
|
||||
float outer = hw * p.BankFlare;
|
||||
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);
|
||||
float rimH = bed[i] + depth[i] / M_PER_UNIT;
|
||||
|
|
@ -455,9 +472,20 @@ public static class RiverCarvePass
|
|||
}
|
||||
else
|
||||
{
|
||||
float f = (r - hw) / hw; // 0..1 across the shoulder
|
||||
f = f * f * (3f - 2f * f); // smoothstep
|
||||
// Shoulder: 0 at the rim → 1 at natural ground, over a flare of
|
||||
// (BankFlare-1) half-widths. Smootherstep (6t⁵−15t⁴+10t³) leaves
|
||||
// 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;
|
||||
// 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;
|
||||
if (target < floor) target = floor;
|
||||
|
|
@ -501,7 +529,7 @@ public static class RiverCarvePass
|
|||
stats.Carved.Add(new CarvedRiver
|
||||
{
|
||||
Name = name, Kind = kind, DrainagePx = drainagePx,
|
||||
Dense = dense, Bed = bed, HalfW = halfW,
|
||||
Dense = dense, Bed = bed, HalfW = halfW, DepthM = depth,
|
||||
Flow = flow, WetFrom = wetFrom, TaperPx = p.TribTaperPx
|
||||
});
|
||||
}
|
||||
|
|
@ -535,7 +563,7 @@ public static class RiverCarvePass
|
|||
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 craterCoreRadius, float stepDropM, float waterDepthM, float fillFraction)
|
||||
{
|
||||
int n = mapSize;
|
||||
float SeaAt(int x, int y) => seaMap != null ? seaMap[x, y] : seaFlat;
|
||||
|
|
@ -551,7 +579,12 @@ public static class RiverCarvePass
|
|||
// 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
|
||||
int taperStart = i, taperEnd = Math.Min(m - 1, r.WetFrom);
|
||||
// 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)
|
||||
{
|
||||
|
|
@ -559,7 +592,14 @@ public static class RiverCarvePass
|
|||
// 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;
|
||||
// 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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue