From 3ae7f5e7ae70a28b336eea8f003e4c2064cdf7f8 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 12 Apr 2026 22:25:32 +0200 Subject: [PATCH] fix(physics): correct terrain Z sampling triangle layout in TerrainSurface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/AcDream.Core/Physics/TerrainSurface.cs | 31 +++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/AcDream.Core/Physics/TerrainSurface.cs b/src/AcDream.Core/Physics/TerrainSurface.cs index 8337dbe..a3c5f78 100644 --- a/src/AcDream.Core/Physics/TerrainSurface.cs +++ b/src/AcDream.Core/Physics/TerrainSurface.cs @@ -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. /// 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 } }