fix: shoreline seam — water follows the RENDERED coastline (terrain-water task 15)
Closes the C1-polish carryover. The cause was confirmed by a read-only dump BEFORE any code was written, per the filed note's discipline; the chat had guessed twice from screenshots. DIAGNOSIS. WBID is classified from the UNCURVED heightmap — the classify path that keeps the biome/water oracle byte-identical through all of Phase B — while the mesh renders the CURVED one. Outside the crater those two agree EXACTLY, and provably so: the curve is identity at sea and monotonic, so Apply(raw) < sea iff raw < sea. Inside the crater they do not. The carve lerps two different bases toward one target -- classify from raw, rendered from Apply(raw) -- and Apply(raw) < raw throughout the lowland band (the toe compresses raw 0.15..0.516 into 0.15..0.206). So the rendered surface sinks FASTER than the classify surface, leaving an annulus that renders below the waterline while WBID still calls it dry. MEASURED, seed 1825907253, map-wide: wet columns 32,799,866 WATER-OVER-DRY (wet, mesh above Y) 1,101 scattered, not the story DRY-UNDER-WATER (dry, mesh below sea) 375,824 <- the seam ...of which inside the crater carve: 100 % Dump across the gradient at z=905: at x=3800 the classify surface sits at 47.55 m while the mesh renders 22.38 m -- 25 m apart, no water drawn. The ring is west of the Capitol, which is exactly the developer's "flush on the right, lifted toward the left". Neither of the task's two candidate causes, and worth saying so: the water LEVEL is flat and correct at 37.65 everywhere it is drawn (rules out B), and the per-cell flat sheet is a rounding error next to this (1,101 px vs 375,824). It is the filed note's classify-vs-render mismatch, with the crater carve as the mechanism. THE FIX. Presence now takes two clauses: the blueprint's classification, OR the rendered ground actually being under the ocean's surface. The second clause tests exactly what the mesh draws (exactSurfaceY) against the OCEAN BODY'S OWN level from WBTB -- the runtime still derives nothing, it only notices that the ground it is drawing is under a surface the blueprint gave it. By the identity above that clause can only ever fire inside the carve, which is precisely the flooded bay it exists for. MEASURED after, same seed: 241,415 -> 358,576 water columns, 454 -> 648 chunks, 117,161 seam columns recovered in the loaded region. Each run now reports its own seam count. 2D PIPELINE UNTOUCHED, verified rather than asserted: the only changed file is the runtime chunk manager, and a regeneration is byte-identical to task 13's in every section but PRMS (timestamp + git hash). All four snapshot PNGs md5-identical. Static flat sheet, no animation, no water DATA change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3b5bc5e5d6
commit
69bf91b3d6
1 changed files with 65 additions and 25 deletions
|
|
@ -34,6 +34,12 @@ namespace IslaApocalypse.Server
|
||||||
private int _chunksWithWater = 0;
|
private int _chunksWithWater = 0;
|
||||||
private float _waterMinY = float.MaxValue, _waterMaxY = float.MinValue, _waterMaxDepth = 0f;
|
private float _waterMinY = float.MaxValue, _waterMaxY = float.MinValue, _waterMaxDepth = 0f;
|
||||||
|
|
||||||
|
// Shoreline seam (task 15). The OCEAN body's own surface level, from the
|
||||||
|
// blueprint's WBTB table; -1 when the blueprint carries no ocean. Columns the
|
||||||
|
// mesh renders below this but WBID calls dry are the seam, and are counted.
|
||||||
|
private float _oceanLevelRaw = -1f;
|
||||||
|
private long _seamColumnsRecovered = 0;
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
@ -60,6 +66,13 @@ namespace IslaApocalypse.Server
|
||||||
$"vs config MapSize {ConfigManager.MapSize}.");
|
$"vs config MapSize {ConfigManager.MapSize}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The ocean's surface level, for the shoreline-seam rule below. Taken
|
||||||
|
// from the blueprint's body table (type OCEAN), never derived here —
|
||||||
|
// the runtime does not compute sea level (D-033).
|
||||||
|
if (_blueprint.WaterBodies != null)
|
||||||
|
foreach (var body in _blueprint.WaterBodies)
|
||||||
|
if (body.Type == WaterBodyInfo.TYPE_OCEAN) { _oceanLevelRaw = body.SurfaceLevel; break; }
|
||||||
|
|
||||||
GD.Print("[Server] Blueprint loaded. Locating Capitol City...");
|
GD.Print("[Server] Blueprint loaded. Locating Capitol City...");
|
||||||
|
|
||||||
// 2. Find the Capitol in the parsed data
|
// 2. Find the Capitol in the parsed data
|
||||||
|
|
@ -99,7 +112,8 @@ namespace IslaApocalypse.Server
|
||||||
GD.Print($"[Server] Water at rest: {_waterColumnsRendered} water columns across " +
|
GD.Print($"[Server] Water at rest: {_waterColumnsRendered} water columns across " +
|
||||||
$"{_chunksWithWater} of {_activeChunks.Count} chunks" +
|
$"{_chunksWithWater} of {_activeChunks.Count} chunks" +
|
||||||
(_waterColumnsRendered > 0
|
(_waterColumnsRendered > 0
|
||||||
? $"; surface Y {_waterMinY:F1}..{_waterMaxY:F1} m, depth up to {_waterMaxDepth:F1} m."
|
? $"; surface Y {_waterMinY:F1}..{_waterMaxY:F1} m, depth up to {_waterMaxDepth:F1} m; " +
|
||||||
|
$"{_seamColumnsRecovered} shoreline-seam columns recovered (mesh below the ocean surface, WBID dry)."
|
||||||
: " — nothing to draw here (check the blueprint carries WBID/WSRF)."));
|
: " — nothing to draw here (check the blueprint carries WBID/WSRF)."));
|
||||||
|
|
||||||
// 5. Teleport the Camera to look down at our creation!
|
// 5. Teleport the Camera to look down at our creation!
|
||||||
|
|
@ -268,43 +282,69 @@ namespace IslaApocalypse.Server
|
||||||
newChunk.ColumnBiomes[x, z] = columnBiome;
|
newChunk.ColumnBiomes[x, z] = columnBiome;
|
||||||
newChunk.ColumnRoadMaterial[x, z] = roadSurface;
|
newChunk.ColumnRoadMaterial[x, z] = roadSurface;
|
||||||
|
|
||||||
// --- WATER AT REST (task 13) ---------------------------------
|
// --- WATER AT REST (task 13) + SHORELINE SEAM (task 15) ------
|
||||||
// The blueprint is the AUTHORITY on where water is; the runtime
|
// The blueprint is the AUTHORITY on where water is and at what
|
||||||
// only draws it. WBID decides presence (it is the water stage's
|
// level; the runtime only draws it. WSRF gives the level per pixel,
|
||||||
// own classification output, the same set the biome grid's
|
// and the WBTB body table is the fallback if a column is flagged wet
|
||||||
// Ocean/Lake pixels form by construction), WSRF gives the level
|
// but carries the WSRF no-water sentinel.
|
||||||
// per pixel, and the WBTB body table is the fallback if a column
|
//
|
||||||
// is flagged wet but carries the WSRF no-water sentinel.
|
// PRESENCE, though, takes two clauses. WBID was classified from the UNCURVED
|
||||||
|
// heightmap — the classify path that keeps the biome/water oracle
|
||||||
|
// byte-identical — while the mesh renders the CURVED one. Outside
|
||||||
|
// the crater those two agree exactly, because the curve is identity
|
||||||
|
// at sea and monotonic, so Apply(raw) < sea iff raw < sea. INSIDE
|
||||||
|
// the crater they do not: the carve lerps two different bases toward
|
||||||
|
// one target (classify from raw, rendered from Apply(raw), and
|
||||||
|
// Apply(raw) < raw throughout the lowland band), so the rendered
|
||||||
|
// surface sinks faster and leaves a ring that renders below the
|
||||||
|
// waterline while WBID still calls it dry. Measured on seed
|
||||||
|
// 1825907253: 375,824 such columns, 100 % of them inside the carve.
|
||||||
|
//
|
||||||
|
// So presence is decided by BOTH: the blueprint's classification,
|
||||||
|
// OR the rendered ground actually being under the ocean's surface.
|
||||||
|
// The second clause can only ever fire inside the carve (see the
|
||||||
|
// identity above), which is precisely the flooded bay it exists for.
|
||||||
if (_blueprint.WaterBodyIds != null)
|
if (_blueprint.WaterBodyIds != null)
|
||||||
{
|
{
|
||||||
ushort bodyId = _blueprint.WaterBodyIds[globalX, globalZ];
|
ushort bodyId = _blueprint.WaterBodyIds[globalX, globalZ];
|
||||||
|
float levelRaw = -1f;
|
||||||
|
|
||||||
if (bodyId != 0)
|
if (bodyId != 0)
|
||||||
{
|
{
|
||||||
float levelRaw = -1f;
|
|
||||||
if (_blueprint.WaterSurfaceQ != null)
|
if (_blueprint.WaterSurfaceQ != null)
|
||||||
{
|
{
|
||||||
ushort q = _blueprint.WaterSurfaceQ[globalX, globalZ];
|
ushort q = _blueprint.WaterSurfaceQ[globalX, globalZ];
|
||||||
if (q != 0) levelRaw = BlueprintFormat.DecodeWaterLevel(q);
|
if (q != 0) levelRaw = BlueprintFormat.DecodeWaterLevel(q);
|
||||||
}
|
}
|
||||||
if (levelRaw < 0f) levelRaw = BodyLevel(bodyId);
|
if (levelRaw < 0f) levelRaw = BodyLevel(bodyId);
|
||||||
|
}
|
||||||
|
else if (_oceanLevelRaw >= 0f
|
||||||
|
&& exactSurfaceY < _oceanLevelRaw * Constants.HEIGHT_SCALE)
|
||||||
|
{
|
||||||
|
// Rendered ground below the ocean's own surface level. The
|
||||||
|
// level still comes from the blueprint (the ocean body's
|
||||||
|
// WBTB entry) — the runtime derives nothing, it only notices
|
||||||
|
// that the ground the MESH draws is under that surface.
|
||||||
|
levelRaw = _oceanLevelRaw;
|
||||||
|
_seamColumnsRecovered++;
|
||||||
|
}
|
||||||
|
|
||||||
if (levelRaw >= 0f)
|
if (levelRaw >= 0f)
|
||||||
{
|
{
|
||||||
// Same mapping as the terrain, so the sheet and the
|
// Same mapping as the terrain, so the sheet and the seabed
|
||||||
// seabed cannot drift apart.
|
// cannot drift apart.
|
||||||
newChunk.WaterSurfaceY[x, z] = Mathf.Clamp(
|
newChunk.WaterSurfaceY[x, z] = Mathf.Clamp(
|
||||||
levelRaw * Constants.HEIGHT_SCALE, 2.0f, Constants.CHUNK_HEIGHT - 2.0f);
|
levelRaw * Constants.HEIGHT_SCALE, 2.0f, Constants.CHUNK_HEIGHT - 2.0f);
|
||||||
// TRUE depth, from blueprint units — see ChunkData.
|
// TRUE depth, from blueprint units — see ChunkData.
|
||||||
newChunk.WaterDepthM[x, z] =
|
newChunk.WaterDepthM[x, z] =
|
||||||
Mathf.Max(0f, (levelRaw - _blueprint.HeightMap[globalX, globalZ]) * Constants.HEIGHT_SCALE);
|
Mathf.Max(0f, (levelRaw - _blueprint.HeightMap[globalX, globalZ]) * Constants.HEIGHT_SCALE);
|
||||||
newChunk.HasAnyWater = true;
|
newChunk.HasAnyWater = true;
|
||||||
|
|
||||||
_waterColumnsRendered++;
|
_waterColumnsRendered++;
|
||||||
float wy = newChunk.WaterSurfaceY[x, z];
|
float wy = newChunk.WaterSurfaceY[x, z];
|
||||||
if (wy < _waterMinY) _waterMinY = wy;
|
if (wy < _waterMinY) _waterMinY = wy;
|
||||||
if (wy > _waterMaxY) _waterMaxY = wy;
|
if (wy > _waterMaxY) _waterMaxY = wy;
|
||||||
if (newChunk.WaterDepthM[x, z] > _waterMaxDepth) _waterMaxDepth = newChunk.WaterDepthM[x, z];
|
if (newChunk.WaterDepthM[x, z] > _waterMaxDepth) _waterMaxDepth = newChunk.WaterDepthM[x, z];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue