islaApocalypse/Tools/Scripts/TerrainDetailPass.cs
beezm 45fe2ce034 feat: shelf micro-relief + drainage incision (terrain-water task 10)
Two paired detail passes on the locked v5 skeleton, output-heights
only (classify path untouched):

PASS A — shelf micro-relief: +-ShelfReliefAmp (default 3 m) Simplex
skin (seed resolved+7409, ~40 undulations/island ~ 200 m features),
weighted by shelf-ness (full mid-shelf, feathered to zero 30% of a
band half-width into the risers) — both shelves get their rolling
texture back; risers and peaks untouched.

PASS B — drainage incision: D8 steepest-descent routing + height-
ordered flow accumulation over the curved+relieved land
(TerrainDetailPass — pure array machinery, named C++ candidate per
D-035); depth = K*accum^p*slope (K=0.78, p=0.45 concave, cap 30 m),
masked full on risers / 30% on shelves / zero on the toe, above the
plateau top, and within 1.2x CraterRadius (feathered to 1.4x); hard
clamp: carved height >= sea + 1 m. Ordering: curve -> relief ->
incision -> crater carve (the carve stays the final authority; carve
moved to its own pass 2c, bit-identical expression on both maps).
The pass prints its own MEASURED depth distribution.

Gate TerrainDetail "off"|"v1" (default v1; both passes one judged
unit; no-op without the curve) + ShelfReliefAmp dial. New TDTL section
(38 B: version + relief/incision params + seed offset) across
writer/parser/harness — blueprints stay self-describing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 18:08:28 -04:00

143 lines
5.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 815 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
/// <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;
}
}