fix(scenery): #48 unify scenery Z with physics-path triangle picker
Closes #48. Trees on sloped cells visibly hovered above the visible
terrain because GameWindow.SampleTerrainZ (the bilinear fallback used
during scenery hydration before physics registers a landblock) had
its diagonal arms swapped — used the SEtoNW triangle test on SWtoNE
cells and vice versa. The ACDREAM_DUMP_SCENERY_Z=1 diagnostic showed
every scenery line ran through the bilinear path (streaming race),
so on hilly terrain scenery was placed at a Z up to ~1.5 m off from
the visible mesh.
Latent since ff325ab (2026-04-17 "feat(ui): debug overlay + refined
input controls" carrying along the upgrade). That commit reached for
WorldBuilder TerrainUtils.GetHeight as the secondary oracle and
re-derived the triangle-pair tests; the named-retail / ACE algorithm
in TerrainSurface.SampleZ (used by the physics path / player Z) was
always correct, so player feet stayed flush — the two paths just
disagreed and only scenery noticed.
Fix:
- TerrainSurface.InterpolateZInTriangle (private static) — single
source of truth for the triangle pick + barycentric Z, sourced
from FUN_00532a50 / ACE LandblockStruct.ConstructPolygons.
- TerrainSurface.SampleZFromHeightmap (public static) — heightmap-
byte-array variant for the scenery hydration fallback. Both this
and TerrainSurface.SampleZ (instance) now delegate to the same
InterpolateZInTriangle.
- GameWindow.SampleTerrainZ — thin wrapper over the new static.
- TerrainSurfaceTests.SampleZFromHeightmap_AgreesWithInstance_AcrossWholeLandblock
asserts both sampler paths agree at 1500 sample points across both
diagonals, so future drift gets caught.
The ACDREAM_DUMP_SCENERY_Z=1 diagnostic in BuildSceneryEntitiesForStreaming
is kept committed (env-var gated, zero cost when off) — useful for
the related #49 scenery (X, Y) placement investigation filed in the
same commit.
Visual verified at Holtburg landblock 0xA9B30001 2026-05-06: the
formerly floating 32 m pines (setups 0x020002D3 / 0x020002D9) now
sit flush on the visible terrain mesh.
Test baseline: dotnet test reports the same 8 pre-existing motion /
BSP step-up failures as the handoff doc warned about — no new
failures introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c1bb43ab89
commit
a4693954d8
5 changed files with 352 additions and 60 deletions
|
|
@ -67,6 +67,64 @@ public class TerrainSurfaceTests
|
|||
Assert.Equal(42f, surface.SampleZ(300f, 300f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SampleZFromHeightmap_AgreesWithInstance_AcrossWholeLandblock()
|
||||
{
|
||||
// Issue #48 conformance: the static SampleZFromHeightmap (bilinear
|
||||
// fallback used at scenery hydration before physics registers a
|
||||
// landblock) must produce the same Z as the instance SampleZ
|
||||
// (player physics path) at every (x, y). The previous fallback in
|
||||
// GameWindow had its diagonal arms swapped — this test pins both
|
||||
// paths to one source of truth.
|
||||
//
|
||||
// Heightmap with distinct per-(x,y) values so every triangle plane
|
||||
// is genuinely different from the others; flat / planar heightmaps
|
||||
// would mask a triangle-pick bug because all four corners would
|
||||
// give the same interpolated Z.
|
||||
var heights = new byte[81];
|
||||
for (int x = 0; x < 9; x++)
|
||||
for (int y = 0; y < 9; y++)
|
||||
heights[x * 9 + y] = (byte)((x * 17 + y * 13) % 256);
|
||||
|
||||
var hTable = LinearHeightTable();
|
||||
|
||||
// Pick a landblock where IsSplitSWtoNE(...) returns BOTH true and
|
||||
// false across the 64 cells — Holtburg coords (0xA9, 0xB3) work.
|
||||
const uint lbX = 0xA9, lbY = 0xB3;
|
||||
var instance = new TerrainSurface(heights, hTable, lbX, lbY);
|
||||
|
||||
// Sample on a fine grid (~1500 points) covering all 64 cells and
|
||||
// crossing every cell's diagonal boundary. A triangle-pick bug
|
||||
// would show up as a >0.5 m Z mismatch on the diagonal-spanning
|
||||
// cells (the corner heights vary by ~10 bytes = 10 Z each cell).
|
||||
for (float lx = 0.5f; lx < 192f; lx += 5f)
|
||||
for (float ly = 0.5f; ly < 192f; ly += 5f)
|
||||
{
|
||||
float instanceZ = instance.SampleZ(lx, ly);
|
||||
float staticZ = TerrainSurface.SampleZFromHeightmap(
|
||||
heights, hTable, lbX, lbY, lx, ly);
|
||||
Assert.True(
|
||||
Math.Abs(instanceZ - staticZ) < 0.0001f,
|
||||
$"Z mismatch at ({lx:F1},{ly:F1}) lb=(0x{lbX:X},0x{lbY:X}): instance={instanceZ:F4} static={staticZ:F4}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SampleZFromHeightmap_RejectsBadInputs()
|
||||
{
|
||||
var goodHeights = new byte[81];
|
||||
var goodTable = LinearHeightTable();
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
TerrainSurface.SampleZFromHeightmap(null!, goodTable, 0, 0, 0f, 0f));
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
TerrainSurface.SampleZFromHeightmap(goodHeights, null!, 0, 0, 0f, 0f));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
TerrainSurface.SampleZFromHeightmap(new byte[80], goodTable, 0, 0, 0f, 0f));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
TerrainSurface.SampleZFromHeightmap(goodHeights, new float[255], 0, 0, 0f, 0f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SampleSurfacePolygon_ReturnsContainingTriangleVertices()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue