using Godot; using System; /// /// 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. /// 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 = 0.78f; 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 /// /// 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. /// 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); } /// /// 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. /// 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; } /// /// 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. /// 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; } }