fix(physics): correct terrain Z sampling triangle layout in TerrainSurface
The SampleZ method had the two triangle-boundary conditions swapped relative to what the mesh index buffer actually renders, causing the physics Z to sample from the wrong triangle on roughly half of all terrain cells. The error could be up to 7.5 units on steep 24×24 cells, which manifested as feet clipping into rising terrain on slopes. Root cause (discovered via WorldBuilder-ACME-Edition exhaustive analysis): "SWtoNE cut" means BL and TR are the *isolated* vertices; the shared hypotenuse runs TL(0,1) → BR(1,0), so the correct dividing test is tx+ty=1, NOT ty=tx. The old code used ty≤tx for the SWtoNE branch (the BL→TR diagonal), which matches the SEtoNW mesh layout instead. Fix: swap the boundary conditions for the two split cases so they match LandblockMesh.cs's actual index buffer layout: SWtoNE: tx+ty ≤ 1 → BL+BR+TL, else TR+TL+BR SEtoNW: ty ≤ tx → BL+BR+TR, else BL+TR+TL Verified by running all three formulas (acdream, ACME GetHeight, ACME HeightSampler) against the mesh index buffer at 50 interior points across both split types: fixed version matches 0 errors, old version had 50. All 283 tests still pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
78d43a0914
commit
3ae7f5e7ae
1 changed files with 19 additions and 12 deletions
|
|
@ -51,12 +51,15 @@ public sealed class TerrainSurface
|
|||
/// interpolation within that triangle. This matches the visual terrain
|
||||
/// mesh exactly.
|
||||
///
|
||||
/// A small slope-proportional upward bias is added to compensate for
|
||||
/// the geometric fact that a point-sampled Z on a tilted triangle
|
||||
/// places the character's center at the surface, but the character's
|
||||
/// feet (which extend forward/backward) clip into the rising terrain.
|
||||
/// The bias is proportional to the max height difference across the
|
||||
/// cell — steeper slope = more lift. On flat ground it's zero.
|
||||
/// Triangle layout (from LandblockMesh.cs index buffer):
|
||||
/// SWtoNE: tri1 = {BL,TL,BR}, tri2 = {BR,TL,TR} — shared edge TL→BR (x+y=1 boundary)
|
||||
/// SEtoNW: tri1 = {BL,TR,BR}, tri2 = {BL,TL,TR} — shared edge BL→TR (y=x boundary)
|
||||
///
|
||||
/// NOTE: The SWtoNE "cut" exposes the SW(BL) and NE(TR) corners as isolated
|
||||
/// vertices — the hypotenuse runs NW(TL)→SE(BR), so the dividing test is
|
||||
/// x+y=1 (not y=x). Confusing naming aside, the formula below matches
|
||||
/// TerrainGeometryGenerator.GetHeight (ACME WorldBuilder-ACME-Edition) which
|
||||
/// was verified against the mesh index buffer.
|
||||
/// </summary>
|
||||
public float SampleZ(float localX, float localY)
|
||||
{
|
||||
|
|
@ -83,17 +86,21 @@ public sealed class TerrainSurface
|
|||
|
||||
if (splitSWtoNE)
|
||||
{
|
||||
if (ty <= tx)
|
||||
return hBL + (hBR - hBL) * tx + (hTR - hBR) * ty;
|
||||
// Mesh: {BL,TL,BR} and {BR,TL,TR}. Shared hypotenuse = TL(0,1)→BR(1,0).
|
||||
// Dividing line: tx + ty = 1.
|
||||
if (tx + ty <= 1f)
|
||||
return hBL + (hBR - hBL) * tx + (hTL - hBL) * ty; // BL+BR+TL triangle
|
||||
else
|
||||
return hBL + (hTR - hTL) * tx + (hTL - hBL) * ty;
|
||||
return hTR + (hTL - hTR) * (1f - tx) + (hBR - hTR) * (1f - ty); // TR+TL+BR triangle
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ty <= 1f - tx)
|
||||
return hBL + (hBR - hBL) * tx + (hTL - hBL) * ty;
|
||||
// Mesh: {BL,TR,BR} and {BL,TL,TR}. Shared hypotenuse = BL(0,0)→TR(1,1).
|
||||
// Dividing line: ty = tx.
|
||||
if (ty <= tx)
|
||||
return hBL + (hBR - hBL) * tx + (hTR - hBR) * ty; // BL+BR+TR triangle
|
||||
else
|
||||
return hTR + (hTL - hTR) * (1f - tx) + (hBR - hTR) * (1f - ty);
|
||||
return hBL + (hTR - hTL) * tx + (hTL - hBL) * ty; // BL+TR+TL triangle
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue