First tuning run (seed 1375359975): p50 2.3 / p90 6.1 / p99 13.6 / max 30.0 m — typical gullies under the 8-15 m target. K=1.40 confirmed: p50 3.3 / p90 10.2 / p99 22.9 / max 30.0, zero sea-clamp hits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
143 lines
5.5 KiB
C#
143 lines
5.5 KiB
C#
using Godot;
|
||
using System;
|
||
|
||
/// <summary>
|
||
/// The terrain DETAIL passes (terrain-water task 10) — pure numeric array machinery
|
||
/// (D-035; a named future C++ candidate, kept standalone):
|
||
///
|
||
/// PASS A — shelf micro-relief: a medium-frequency noise skin (±ShelfReliefAmp,
|
||
/// default 3 m) weighted by shelf-ness, so the compressed shelves get their
|
||
/// rolling texture back while risers and peaks stay untouched.
|
||
///
|
||
/// PASS B — drainage incision: D8 steepest-descent flow routing + accumulation
|
||
/// over the curved terrain; depth = K · accum^p · localSlope (capped), masked to
|
||
/// the risers (feathered ~30 % onto shelves, zero on the toe and above the
|
||
/// plateau top, zero near the crater), clamped so carved terrain never drops
|
||
/// below sea + 1 m. The channels double as the future river routes (Phase C).
|
||
///
|
||
/// Ordering (enforced by the caller): curve → micro-relief → incision → crater
|
||
/// carve. The classify map never sees any of it.
|
||
/// </summary>
|
||
public static class TerrainDetailPass
|
||
{
|
||
public const ushort VERSION = 1;
|
||
|
||
// Pass A — micro-relief.
|
||
public const float RELIEF_AMP_DEFAULT_M = 3f; // config dial: ShelfReliefAmp (metres)
|
||
public const float RELIEF_FREQ_ISLANDS = 40f; // ~40 undulations per island width (~200 m features)
|
||
public const int RELIEF_SEED_OFFSET = 7409;
|
||
|
||
// Pass B — incision. K/p tuned against the depth targets (gullies 8–15 m,
|
||
// trunks ~25 m, cap 30 m); the tuning run's achieved distribution is in the
|
||
// task-10 report.
|
||
public const float INC_K = 1.40f;
|
||
public const float INC_P = 0.45f; // concave: many fingers, few deep trunks
|
||
public const float INC_CAP_M = 30f; // IncisionMax
|
||
public const float SEA_CLAMP = 0.15f + 1f / 251f; // carved height ≥ sea + 1 m
|
||
public const float SHELF_INC_WEIGHT = 0.3f; // shelves get washes, not gorges
|
||
public const float CRATER_EXCL_FACTOR = 1.2f; // zero incision inside this × CraterRadius
|
||
public const float CRATER_FEATHER_FACTOR = 1.4f; // ...feathering to full by this × CraterRadius
|
||
|
||
/// <summary>
|
||
/// Shelf-ness weight from the RAW input height: 1 mid-shelf, feathering to 0
|
||
/// through the risers (feather extends 30 % of the band half-width past each
|
||
/// shelf edge). Covers both shelves.
|
||
/// </summary>
|
||
public static float ShelfWeight(float raw, CurveKnots k)
|
||
{
|
||
return Mathf.Max(BandBump(raw, k.K3, k.K4), BandBump(raw, k.K5, k.K6));
|
||
}
|
||
|
||
private static float BandBump(float h, float lo, float hi)
|
||
{
|
||
float half = (hi - lo) * 0.5f;
|
||
float t = Mathf.Abs(h - (lo + half)) / half; // 0 centre, 1 at band edge
|
||
// full inside 60 % of the band, linear feather to zero at 130 %
|
||
return Mathf.Clamp(1f - (t - 0.6f) / 0.7f, 0f, 1f);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Incision mask from the RAW input height: 0 below the red-ceiling input (K2)
|
||
/// and above the plateau top (K6); 1 on the riser bands; SHELF_INC_WEIGHT on the
|
||
/// shelf bands; smooth feathers (15 % of the local band width) at every boundary.
|
||
/// </summary>
|
||
public static float IncisionWeight(float raw, CurveKnots k)
|
||
{
|
||
if (raw <= k.K2 || raw >= k.K6) return 0f;
|
||
if (raw < k.K3) // foothill riser: feather in from K2, feather toward shelf weight at K3
|
||
return EdgeBlend(raw, k.K2, k.K3, 0f, 1f, SHELF_INC_WEIGHT);
|
||
if (raw < k.K4) // bench
|
||
return SHELF_INC_WEIGHT;
|
||
if (raw < k.K5) // mid riser
|
||
return EdgeBlend(raw, k.K4, k.K5, SHELF_INC_WEIGHT, 1f, SHELF_INC_WEIGHT);
|
||
// plateau band: shelf weight, feathering to zero at K6
|
||
float w = (k.K6 - raw) / ((k.K6 - k.K5) * 0.15f);
|
||
return Mathf.Min(SHELF_INC_WEIGHT, Mathf.Clamp(w, 0f, 1f) * SHELF_INC_WEIGHT);
|
||
}
|
||
|
||
private static float EdgeBlend(float h, float lo, float hi, float wIn, float wMid, float wOut)
|
||
{
|
||
float f = (hi - lo) * 0.15f;
|
||
if (h < lo + f) return Mathf.Lerp(wIn, wMid, (h - lo) / f);
|
||
if (h > hi - f) return Mathf.Lerp(wMid, wOut, (h - (hi - f)) / f);
|
||
return wMid;
|
||
}
|
||
|
||
/// <summary>
|
||
/// D8 flow accumulation over a height field (row-major idx = x·n + y).
|
||
/// Steepest-descent routing (drop / distance, diagonals ÷√2), deterministic
|
||
/// tie-break (fixed neighbour order, first winner). Cells with no lower
|
||
/// neighbour are pits/outlets (no outflow). accum = upslope contributing cells
|
||
/// including self; steepestDrop = drop per pixel toward the chosen neighbour.
|
||
/// </summary>
|
||
public static int[] FlowAccumulation(float[] h, int n, out float[] steepestDrop)
|
||
{
|
||
int total = n * n;
|
||
int[] downstream = new int[total];
|
||
steepestDrop = new float[total];
|
||
int[] dx = { 1, -1, 0, 0, 1, 1, -1, -1 };
|
||
int[] dy = { 0, 0, 1, -1, 1, -1, 1, -1 };
|
||
float[] invDist = { 1f, 1f, 1f, 1f, 0.7071068f, 0.7071068f, 0.7071068f, 0.7071068f };
|
||
|
||
for (int x = 0; x < n; x++)
|
||
{
|
||
for (int y = 0; y < n; y++)
|
||
{
|
||
int i = x * n + y;
|
||
float hc = h[i];
|
||
float best = 0f;
|
||
int bestIdx = -1;
|
||
for (int d = 0; d < 8; d++)
|
||
{
|
||
int nx = x + dx[d], ny = y + dy[d];
|
||
if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
|
||
int ni = nx * n + ny;
|
||
float grade = (hc - h[ni]) * invDist[d];
|
||
if (grade > best)
|
||
{
|
||
best = grade;
|
||
bestIdx = ni;
|
||
}
|
||
}
|
||
downstream[i] = bestIdx;
|
||
steepestDrop[i] = best;
|
||
}
|
||
}
|
||
|
||
// Height-descending order: each cell pushes its accumulated count downstream.
|
||
float[] keys = (float[])h.Clone();
|
||
int[] order = new int[total];
|
||
for (int i = 0; i < total; i++) order[i] = i;
|
||
Array.Sort(keys, order); // ascending
|
||
|
||
int[] accum = new int[total];
|
||
for (int i = 0; i < total; i++) accum[i] = 1;
|
||
for (int i = total - 1; i >= 0; i--)
|
||
{
|
||
int c = order[i];
|
||
int d = downstream[c];
|
||
if (d >= 0) accum[d] += accum[c];
|
||
}
|
||
return accum;
|
||
}
|
||
}
|