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>
This commit is contained in:
Stewart Howe 2026-08-08 18:08:28 -04:00
parent c71026fd94
commit 45fe2ce034
7 changed files with 354 additions and 13 deletions

View file

@ -33,6 +33,7 @@ namespace IslaApocalypse.Core
public const uint TAG_WATER_BODY_TABLE = 0x42544257; // "WBTB"
public const uint TAG_WATER_SURFACE = 0x46525357; // "WSRF"
public const uint TAG_TERRAIN_CURVE = 0x56524354; // "TCRV"
public const uint TAG_TERRAIN_DETAIL = 0x4C544454; // "TDTL"
// WSRF quantization: u16, 0 reserved as the no-water sentinel. A real level L
// (raw blueprint height units) encodes as 1 + round(L × 32768), so a genuine

View file

@ -33,6 +33,8 @@ namespace IslaApocalypse.Core
WriteSection(writer, BlueprintFormat.TAG_PARAMS, w => WriteParams(w, bp));
if (bp.TerrainCurve != null)
WriteSection(writer, BlueprintFormat.TAG_TERRAIN_CURVE, w => WriteTerrainCurve(w, bp.TerrainCurve));
if (bp.TerrainDetail != null)
WriteSection(writer, BlueprintFormat.TAG_TERRAIN_DETAIL, w => WriteTerrainDetail(w, bp.TerrainDetail));
WriteSection(writer, BlueprintFormat.TAG_HEIGHTS, w => WriteHeights(w, bp));
WriteSection(writer, BlueprintFormat.TAG_BIOMES, w => WriteBiomes(w, bp));
@ -151,6 +153,16 @@ namespace IslaApocalypse.Core
}
}
private static void WriteTerrainDetail(BinaryWriter writer, TerrainDetailInfo d)
{
writer.Write(d.Version);
writer.Write(d.ReliefAmpM); writer.Write(d.ReliefFreqIslands);
writer.Write(d.IncK); writer.Write(d.IncP); writer.Write(d.IncCapM);
writer.Write(d.SeaClampRaw); writer.Write(d.CraterExclFactor);
writer.Write(d.ShelfIncWeight);
writer.Write(d.ReliefSeedOffset);
}
private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)
{
int n = bp.MapSize;

View file

@ -33,6 +33,13 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
// are retired. Default: v5.
public static string TerrainCurve = "v5";
// Terrain detail passes (task 10): "v1" = shelf micro-relief + drainage
// incision as one judged unit (requires the curve; no-op when it is off);
// "off" disables both. ShelfReliefAmp is the micro-relief amplitude in
// metres. Defaults: v1, 3 m.
public static string TerrainDetail = "v1";
public static float ShelfReliefAmp = 3.0f;
public static void LoadConfig()
{
string path = "res://ServerConfig.json";
@ -118,6 +125,20 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'.");
}
// Extract the terrain-detail gate + relief amplitude
if (data.ContainsKey("TerrainDetail"))
{
string detail = (string)data["TerrainDetail"];
if (detail == "off" || detail == "v1")
TerrainDetail = detail;
else
GD.PrintErr($"[ConfigManager] Unknown TerrainDetail '{detail}'. Keeping '{TerrainDetail}'.");
}
if (data.ContainsKey("ShelfReliefAmp"))
{
ShelfReliefAmp = (float)data["ShelfReliefAmp"];
}
switch (profile)
{
case "4K":

View file

@ -87,6 +87,20 @@ namespace IslaApocalypse.Core
public float K5, K6;
}
/// <summary>
/// The terrain detail passes that shaped this blueprint's HGTS (v2 TDTL section,
/// terrain-water task 10): shelf micro-relief + drainage incision parameters.
/// Null when detail was off. Metadata only — heights are already detailed.
/// </summary>
public class TerrainDetailInfo
{
public ushort Version;
public float ReliefAmpM, ReliefFreqIslands;
public float IncK, IncP, IncCapM;
public float SeaClampRaw, CraterExclFactor, ShelfIncWeight;
public int ReliefSeedOffset;
}
public class WorldBlueprint
{
public int MapSize;
@ -114,6 +128,9 @@ namespace IslaApocalypse.Core
// The curve that shaped HeightMap (v2 TCRV section); null = raw legacy profile.
public TerrainCurveInfo TerrainCurve;
// The detail passes that shaped HeightMap (TDTL section); null = no detail.
public TerrainDetailInfo TerrainDetail;
}
// 2. The Parser Utility
@ -237,6 +254,7 @@ namespace IslaApocalypse.Core
else if (tag == BlueprintFormat.TAG_WATER_SURFACE) sectionOk = ParseWaterGrid(reader, blueprint, payloadLength, isSurface: true);
else if (tag == BlueprintFormat.TAG_WATER_BODY_TABLE) sectionOk = ParseWaterBodyTable(reader, blueprint);
else if (tag == BlueprintFormat.TAG_TERRAIN_CURVE) sectionOk = ParseTerrainCurve(reader, blueprint);
else if (tag == BlueprintFormat.TAG_TERRAIN_DETAIL) sectionOk = ParseTerrainDetail(reader, blueprint);
else
{
// The property the redesign exists to buy: future sections (water,
@ -430,6 +448,19 @@ namespace IslaApocalypse.Core
return true;
}
private static bool ParseTerrainDetail(BinaryReader reader, WorldBlueprint blueprint)
{
var d = new TerrainDetailInfo();
d.Version = reader.ReadUInt16();
d.ReliefAmpM = reader.ReadSingle(); d.ReliefFreqIslands = reader.ReadSingle();
d.IncK = reader.ReadSingle(); d.IncP = reader.ReadSingle(); d.IncCapM = reader.ReadSingle();
d.SeaClampRaw = reader.ReadSingle(); d.CraterExclFactor = reader.ReadSingle();
d.ShelfIncWeight = reader.ReadSingle();
d.ReliefSeedOffset = reader.ReadInt32();
blueprint.TerrainDetail = d;
return true;
}
private static bool ParseRoadTier(BinaryReader reader, List<Vector2[]> into)
{
int pathCount = reader.ReadInt32();