feat: 0_height hillshade snapshot + TCRV blueprint section + harness (terrain-water task 05)
0_height: hypsometric storm-ladder tint x Lambert NW hillshade over the curved heights, captured after topography in both curve modes — relief is finally visible. Renumbers nothing. TCRV (50 B, emitted when the curve is on): curve version + input knots + output bands, so blueprints are self-describing about the map their heights went through (H7 spirit). Parsed into WorldBlueprint.TerrainCurve (null = raw legacy profile); server treats it as metadata. Round-trip harness compares it when present. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
67868638be
commit
e7de292b3d
5 changed files with 142 additions and 1 deletions
|
|
@ -32,6 +32,7 @@ namespace IslaApocalypse.Core
|
||||||
public const uint TAG_WATER_BODY_IDS = 0x44494257; // "WBID"
|
public const uint TAG_WATER_BODY_IDS = 0x44494257; // "WBID"
|
||||||
public const uint TAG_WATER_BODY_TABLE = 0x42544257; // "WBTB"
|
public const uint TAG_WATER_BODY_TABLE = 0x42544257; // "WBTB"
|
||||||
public const uint TAG_WATER_SURFACE = 0x46525357; // "WSRF"
|
public const uint TAG_WATER_SURFACE = 0x46525357; // "WSRF"
|
||||||
|
public const uint TAG_TERRAIN_CURVE = 0x56524354; // "TCRV"
|
||||||
|
|
||||||
// WSRF quantization: u16, 0 reserved as the no-water sentinel. A real level L
|
// 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
|
// (raw blueprint height units) encodes as 1 + round(L × 32768), so a genuine
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ namespace IslaApocalypse.Core
|
||||||
writer.Write(BlueprintFormat.VERSION);
|
writer.Write(BlueprintFormat.VERSION);
|
||||||
|
|
||||||
WriteSection(writer, BlueprintFormat.TAG_PARAMS, w => WriteParams(w, bp));
|
WriteSection(writer, BlueprintFormat.TAG_PARAMS, w => WriteParams(w, bp));
|
||||||
|
if (bp.TerrainCurve != null)
|
||||||
|
WriteSection(writer, BlueprintFormat.TAG_TERRAIN_CURVE, w => WriteTerrainCurve(w, bp.TerrainCurve));
|
||||||
WriteSection(writer, BlueprintFormat.TAG_HEIGHTS, w => WriteHeights(w, bp));
|
WriteSection(writer, BlueprintFormat.TAG_HEIGHTS, w => WriteHeights(w, bp));
|
||||||
WriteSection(writer, BlueprintFormat.TAG_BIOMES, w => WriteBiomes(w, bp));
|
WriteSection(writer, BlueprintFormat.TAG_BIOMES, w => WriteBiomes(w, bp));
|
||||||
|
|
||||||
|
|
@ -122,6 +124,16 @@ namespace IslaApocalypse.Core
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void WriteTerrainCurve(BinaryWriter writer, TerrainCurveInfo c)
|
||||||
|
{
|
||||||
|
writer.Write(c.Version);
|
||||||
|
writer.Write(c.T1); writer.Write(c.T2); writer.Write(c.T3); writer.Write(c.T4);
|
||||||
|
writer.Write(c.HMaxCal);
|
||||||
|
writer.Write(c.Sea); writer.Write(c.OrangeCeil); writer.Write(c.RedCeil);
|
||||||
|
writer.Write(c.PlateauLo); writer.Write(c.PlateauHi); writer.Write(c.PeakCap);
|
||||||
|
writer.Write(c.TailSlope);
|
||||||
|
}
|
||||||
|
|
||||||
private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)
|
private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)
|
||||||
{
|
{
|
||||||
int n = bp.MapSize;
|
int n = bp.MapSize;
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,20 @@ namespace IslaApocalypse.Core
|
||||||
public Vector2 Centroid; // map pixels
|
public Vector2 Centroid; // map pixels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The height-redistribution curve that shaped this blueprint's heights (v2 TCRV
|
||||||
|
/// section, terrain-water task 05). Null when the blueprint was generated with the
|
||||||
|
/// curve off (or predates it) — heights are then the raw legacy profile. Pure
|
||||||
|
/// metadata: the server never re-applies the curve; exported heights are already
|
||||||
|
/// curved.
|
||||||
|
/// </summary>
|
||||||
|
public class TerrainCurveInfo
|
||||||
|
{
|
||||||
|
public ushort Version;
|
||||||
|
public float T1, T2, T3, T4, HMaxCal; // input knots
|
||||||
|
public float Sea, OrangeCeil, RedCeil, PlateauLo, PlateauHi, PeakCap, TailSlope; // output bands
|
||||||
|
}
|
||||||
|
|
||||||
public class WorldBlueprint
|
public class WorldBlueprint
|
||||||
{
|
{
|
||||||
public int MapSize;
|
public int MapSize;
|
||||||
|
|
@ -82,6 +96,9 @@ namespace IslaApocalypse.Core
|
||||||
public ushort[,] WaterBodyIds; // 0 = no water, 1 = ocean, 2..N = lakes
|
public ushort[,] WaterBodyIds; // 0 = no water, 1 = ocean, 2..N = lakes
|
||||||
public List<WaterBodyInfo> WaterBodies = new List<WaterBodyInfo>();
|
public List<WaterBodyInfo> WaterBodies = new List<WaterBodyInfo>();
|
||||||
public ushort[,] WaterSurfaceQ; // quantized levels — BlueprintFormat.DecodeWaterLevel
|
public ushort[,] WaterSurfaceQ; // quantized levels — BlueprintFormat.DecodeWaterLevel
|
||||||
|
|
||||||
|
// The curve that shaped HeightMap (v2 TCRV section); null = raw legacy profile.
|
||||||
|
public TerrainCurveInfo TerrainCurve;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. The Parser Utility
|
// 2. The Parser Utility
|
||||||
|
|
@ -204,6 +221,7 @@ namespace IslaApocalypse.Core
|
||||||
else if (tag == BlueprintFormat.TAG_WATER_BODY_IDS) sectionOk = ParseWaterGrid(reader, blueprint, payloadLength, isSurface: false);
|
else if (tag == BlueprintFormat.TAG_WATER_BODY_IDS) sectionOk = ParseWaterGrid(reader, blueprint, payloadLength, isSurface: false);
|
||||||
else if (tag == BlueprintFormat.TAG_WATER_SURFACE) sectionOk = ParseWaterGrid(reader, blueprint, payloadLength, isSurface: true);
|
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_WATER_BODY_TABLE) sectionOk = ParseWaterBodyTable(reader, blueprint);
|
||||||
|
else if (tag == BlueprintFormat.TAG_TERRAIN_CURVE) sectionOk = ParseTerrainCurve(reader, blueprint);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// The property the redesign exists to buy: future sections (water,
|
// The property the redesign exists to buy: future sections (water,
|
||||||
|
|
@ -366,6 +384,21 @@ namespace IslaApocalypse.Core
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ParseTerrainCurve(BinaryReader reader, WorldBlueprint blueprint)
|
||||||
|
{
|
||||||
|
var c = new TerrainCurveInfo();
|
||||||
|
c.Version = reader.ReadUInt16();
|
||||||
|
c.T1 = reader.ReadSingle(); c.T2 = reader.ReadSingle();
|
||||||
|
c.T3 = reader.ReadSingle(); c.T4 = reader.ReadSingle();
|
||||||
|
c.HMaxCal = reader.ReadSingle();
|
||||||
|
c.Sea = reader.ReadSingle(); c.OrangeCeil = reader.ReadSingle();
|
||||||
|
c.RedCeil = reader.ReadSingle(); c.PlateauLo = reader.ReadSingle();
|
||||||
|
c.PlateauHi = reader.ReadSingle(); c.PeakCap = reader.ReadSingle();
|
||||||
|
c.TailSlope = reader.ReadSingle();
|
||||||
|
blueprint.TerrainCurve = c;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool ParseRoadTier(BinaryReader reader, List<Vector2[]> into)
|
private static bool ParseRoadTier(BinaryReader reader, List<Vector2[]> into)
|
||||||
{
|
{
|
||||||
int pathCount = reader.ReadInt32();
|
int pathCount = reader.ReadInt32();
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,10 @@ public partial class MapGenerator : TextureRect
|
||||||
// A future water stage slots in as another CaptureStage call at its own boundary.
|
// A future water stage slots in as another CaptureStage call at its own boundary.
|
||||||
|
|
||||||
GenerateTopography();
|
GenerateTopography();
|
||||||
GD.Print($"{T()} Topography done (height + temperature).");
|
GD.Print($"{T()} Topography done (height + temperature). TerrainCurve: {(_curveOn ? "v1" : "off")}.");
|
||||||
|
|
||||||
|
DrawHeightStageTexture();
|
||||||
|
await CaptureStage("0_height");
|
||||||
|
|
||||||
CalculateTrueOcean();
|
CalculateTrueOcean();
|
||||||
CalculateMainland();
|
CalculateMainland();
|
||||||
|
|
@ -247,6 +250,15 @@ public partial class MapGenerator : TextureRect
|
||||||
TrailRoads = _trailPaths,
|
TrailRoads = _trailPaths,
|
||||||
WaterBodyIds = _waterBodyIds,
|
WaterBodyIds = _waterBodyIds,
|
||||||
WaterBodies = _waterBodies ?? new List<WaterBodyInfo>(),
|
WaterBodies = _waterBodies ?? new List<WaterBodyInfo>(),
|
||||||
|
TerrainCurve = _curveOn ? new TerrainCurveInfo
|
||||||
|
{
|
||||||
|
Version = HeightCurve.VERSION,
|
||||||
|
T1 = HeightCurve.T1, T2 = HeightCurve.T2, T3 = HeightCurve.T3, T4 = HeightCurve.T4,
|
||||||
|
HMaxCal = HeightCurve.HMAX_CAL,
|
||||||
|
Sea = HeightCurve.SEA, OrangeCeil = HeightCurve.ORANGE_CEIL, RedCeil = HeightCurve.RED_CEIL,
|
||||||
|
PlateauLo = HeightCurve.PLATEAU_LO, PlateauHi = HeightCurve.PLATEAU_HI,
|
||||||
|
PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE
|
||||||
|
} : null,
|
||||||
FormatVersion = 2,
|
FormatVersion = 2,
|
||||||
Params = new BlueprintParams
|
Params = new BlueprintParams
|
||||||
{
|
{
|
||||||
|
|
@ -749,6 +761,65 @@ public partial class MapGenerator : TextureRect
|
||||||
$"deeper than 0.01: {basinDepths.FindAll(d => d > 0.01f).Count}, deeper than 0.05: {basinDepths.FindAll(d => d > 0.05f).Count}.");
|
$"deeper than 0.01: {basinDepths.FindAll(d => d > 0.01f).Count}, deeper than 0.05: {basinDepths.FindAll(d => d > 0.05f).Count}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Paints the height-stage snapshot (task 05): hypsometric tint by storm-ladder
|
||||||
|
/// band × Lambert hillshade from the height gradient (NW light), over the CURVED
|
||||||
|
/// heights — this is the snapshot that makes relief visible. Runs in both curve
|
||||||
|
/// modes; with the curve off it shows the legacy profile under the same bands.
|
||||||
|
/// Pure numeric pass over the heightmap.
|
||||||
|
/// </summary>
|
||||||
|
private void DrawHeightStageTexture()
|
||||||
|
{
|
||||||
|
Image img = Image.CreateEmpty(MapSize, MapSize, false, Image.Format.Rgba8);
|
||||||
|
Vector3 light = new Vector3(-0.55f, -0.55f, 0.63f).Normalized(); // NW, ~39° up
|
||||||
|
|
||||||
|
Color deepSea = new Color(0.07f, 0.15f, 0.32f);
|
||||||
|
Color shallowSea = new Color(0.25f, 0.45f, 0.65f);
|
||||||
|
Color green = new Color(0.44f, 0.62f, 0.36f); // orange band terrain: lowland green
|
||||||
|
Color tan = new Color(0.76f, 0.70f, 0.46f); // red band: tan
|
||||||
|
Color brown = new Color(0.55f, 0.41f, 0.28f); // shoulder + plateau: brown
|
||||||
|
Color white = new Color(0.97f, 0.97f, 0.98f); // peaks
|
||||||
|
Color grey = new Color(0.72f, 0.72f, 0.70f);
|
||||||
|
|
||||||
|
int n = MapSize;
|
||||||
|
for (int x = 0; x < n; x++)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < n; y++)
|
||||||
|
{
|
||||||
|
float h = _heightMap[x, y];
|
||||||
|
float sea = GetSeaLevel(_tempMap[x, y]);
|
||||||
|
|
||||||
|
Color tint;
|
||||||
|
if (h < sea)
|
||||||
|
{
|
||||||
|
float depth = Mathf.Clamp((sea - h) / 0.5f, 0f, 1f);
|
||||||
|
tint = shallowSea.Lerp(deepSea, depth);
|
||||||
|
}
|
||||||
|
else if (h < HeightCurve.ORANGE_CEIL) tint = green;
|
||||||
|
else if (h < HeightCurve.RED_CEIL) tint = tan;
|
||||||
|
else if (h < HeightCurve.PLATEAU_HI) tint = brown;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float t2 = Mathf.Clamp((h - HeightCurve.PLATEAU_HI) / (HeightCurve.PEAK_CAP - HeightCurve.PLATEAU_HI), 0f, 1f);
|
||||||
|
tint = grey.Lerp(white, t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lambert hillshade on the world-scale gradient (1 px = 1 m, height ×251 m).
|
||||||
|
int xm = x > 0 ? x - 1 : x, xp = x < n - 1 ? x + 1 : x;
|
||||||
|
int ym = y > 0 ? y - 1 : y, yp = y < n - 1 ? y + 1 : y;
|
||||||
|
float gx = (_heightMap[xp, y] - _heightMap[xm, y]) * 251f / (xp - xm == 0 ? 1 : xp - xm);
|
||||||
|
float gy = (_heightMap[x, yp] - _heightMap[x, ym]) * 251f / (yp - ym == 0 ? 1 : yp - ym);
|
||||||
|
Vector3 nrm = new Vector3(-gx, -gy, 1f).Normalized();
|
||||||
|
float shade = Mathf.Clamp(nrm.Dot(light), 0f, 1f);
|
||||||
|
|
||||||
|
Color c = tint * (0.35f + 0.65f * shade);
|
||||||
|
c.A = 1f;
|
||||||
|
img.SetPixel(x, y, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Texture = ImageTexture.CreateFromImage(img);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Paints the water-stage snapshot from the stage's own outputs (the biome grid
|
/// Paints the water-stage snapshot from the stage's own outputs (the biome grid
|
||||||
/// does not exist yet at this point in the pipeline): ocean deep blue, lakes a
|
/// does not exist yet at this point in the pipeline): ocean deep blue, lakes a
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ public partial class RoundTripHarness : Node
|
||||||
ok &= CompareRoads("Rugged", a.RuggedRoads, b.RuggedRoads);
|
ok &= CompareRoads("Rugged", a.RuggedRoads, b.RuggedRoads);
|
||||||
ok &= CompareRoads("Trail", a.TrailRoads, b.TrailRoads);
|
ok &= CompareRoads("Trail", a.TrailRoads, b.TrailRoads);
|
||||||
ok &= CompareWater(a, b);
|
ok &= CompareWater(a, b);
|
||||||
|
ok &= CompareTerrainCurve(a, b);
|
||||||
|
|
||||||
if (ok)
|
if (ok)
|
||||||
GD.Print($"[Harness] Semantic equality holds: {a.MapSize}x{a.MapSize} grid, " +
|
GD.Print($"[Harness] Semantic equality holds: {a.MapSize}x{a.MapSize} grid, " +
|
||||||
|
|
@ -185,6 +186,29 @@ public partial class RoundTripHarness : Node
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CompareTerrainCurve(WorldBlueprint a, WorldBlueprint b)
|
||||||
|
{
|
||||||
|
if (a.TerrainCurve == null && b.TerrainCurve == null)
|
||||||
|
{
|
||||||
|
GD.Print("[Harness] TCRV: absent in source — nothing to compare (and none reappeared).");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (a.TerrainCurve == null || b.TerrainCurve == null)
|
||||||
|
{
|
||||||
|
GD.PrintErr("[Harness] TCRV presence mismatch between source and reread.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var ca = a.TerrainCurve; var cb = b.TerrainCurve;
|
||||||
|
bool same = ca.Version == cb.Version;
|
||||||
|
float[] fa = { ca.T1, ca.T2, ca.T3, ca.T4, ca.HMaxCal, ca.Sea, ca.OrangeCeil, ca.RedCeil, ca.PlateauLo, ca.PlateauHi, ca.PeakCap, ca.TailSlope };
|
||||||
|
float[] fb = { cb.T1, cb.T2, cb.T3, cb.T4, cb.HMaxCal, cb.Sea, cb.OrangeCeil, cb.RedCeil, cb.PlateauLo, cb.PlateauHi, cb.PeakCap, cb.TailSlope };
|
||||||
|
for (int i = 0; i < fa.Length; i++)
|
||||||
|
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false;
|
||||||
|
if (!same) { GD.PrintErr("[Harness] TCRV fields differ."); return false; }
|
||||||
|
GD.Print($"[Harness] TCRV equal (curve v{ca.Version}).");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private bool CompareRoads(string tier, List<Vector2[]> a, List<Vector2[]> b)
|
private bool CompareRoads(string tier, List<Vector2[]> a, List<Vector2[]> b)
|
||||||
{
|
{
|
||||||
if (a.Count != b.Count)
|
if (a.Count != b.Count)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue