feat(physics): use sloped terrain plane in FindEnvCollisions
Our previous FindEnvCollisions built a FLAT contact plane (Normal = +Z)
at the sampled terrain Z, discarding the triangle's actual slope.
Retail uses the real terrain polygon's plane (ACE Landblock.cs:125-137
find_terrain_poly → walkable.Plane) which IS sloped.
Without a true slope normal, AdjustOffset's projection of horizontal
velocity onto the plane produces no slope-aligned Z component — fine
for step-subdivision on flat ground, visibly wrong whenever the contact
plane is carried across frames (via PhysicsBody.ContactPlane persistence
from commit 93cbabb): the projection is a no-op and movement is purely
kinematic. With the real slope normal, projected motion correctly
follows the slope.
Not a user-visible bug fix by itself (DIAG LocalZ shows delta≈0 for the
local player everywhere; the "looks too high in water" issue the user
reported is actually a missing water-rendering feature, not a physics
bug). Landing it anyway because it matches retail behavior and removes
the "flat-plane-is-fine" assumption that would bite on any future
contact-plane-dependent code.
Additions:
- TerrainSurface.SampleSurface(localX, localY) → (Z, Normal), deriving
the plane normal analytically from the triangle's height gradient.
Matches the same triangle SampleZ already interpolates through.
- PhysicsEngine.SampleTerrainPlane(worldX, worldY) → System.Numerics.Plane,
the wrapper that bridges terrain space to transition space.
- TransitionTypes.FindEnvCollisions uses SampleTerrainPlane instead of
synthesizing a flat plane from SampleTerrainZ.
All 717 tests green. Flat-plane case is unchanged (Normal.Z = 1 when
the triangle is level, identical to the old plane).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
93cbabbc87
commit
40f120617d
3 changed files with 139 additions and 8 deletions
|
|
@ -116,6 +116,99 @@ public sealed class TerrainSurface
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sample both the terrain Z and the triangle-plane surface normal at
|
||||
/// (localX, localY). The normal is derived from the gradient of the same
|
||||
/// triangle <see cref="SampleZ"/> interpolates across, so returned
|
||||
/// <c>(Z, Normal)</c> is exactly the sloped plane the physics should
|
||||
/// contact against.
|
||||
///
|
||||
/// <para>
|
||||
/// This matters for <see cref="Transition"/>: when <c>AdjustOffset</c>
|
||||
/// projects the per-step movement offset onto the contact plane, a flat
|
||||
/// plane (<c>Normal = (0,0,1)</c>) cannot impart any Z component to a
|
||||
/// horizontal velocity — the character walks off a slope and the
|
||||
/// per-frame step-down budget (~4 cm) can't catch up with the slope
|
||||
/// descent rate, so the sphere floats above the terrain. A SLOPED plane
|
||||
/// gives AdjustOffset the info it needs to produce slope-aligned motion
|
||||
/// with the correct Z component baked in.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Retail does this via <c>LandCell.find_terrain_poly → walkable.Plane</c>
|
||||
/// (ACE <c>Landblock.cs:125-137</c>). We derive the equivalent plane
|
||||
/// analytically from the chosen triangle's three corner heights.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public (float Z, System.Numerics.Vector3 Normal) SampleSurface(float localX, float localY)
|
||||
{
|
||||
float fx = Math.Clamp(localX / CellSize, 0f, CellsPerSide - 0.001f);
|
||||
float fy = Math.Clamp(localY / CellSize, 0f, CellsPerSide - 0.001f);
|
||||
int cx = (int)fx;
|
||||
int cy = (int)fy;
|
||||
cx = Math.Clamp(cx, 0, CellsPerSide - 1);
|
||||
cy = Math.Clamp(cy, 0, CellsPerSide - 1);
|
||||
|
||||
float tx = fx - cx;
|
||||
float ty = fy - cy;
|
||||
|
||||
float hBL = _z[cx, cy ];
|
||||
float hBR = _z[cx + 1, cy ];
|
||||
float hTR = _z[cx + 1, cy + 1];
|
||||
float hTL = _z[cx, cy + 1];
|
||||
|
||||
bool splitSWtoNE = IsSplitSWtoNE(_landblockX, (uint)cx, _landblockY, (uint)cy);
|
||||
|
||||
// The SampleZ formula for each triangle is linear in (tx, ty):
|
||||
// Z = a + b * tx + c * ty
|
||||
// so dZ/dLocalX = b / CellSize, dZ/dLocalY = c / CellSize.
|
||||
// Surface normal = normalize((-dZ/dX, -dZ/dY, 1)) — a well-known
|
||||
// identity for a height-field plane.
|
||||
float z, dzdx, dzdy;
|
||||
|
||||
if (splitSWtoNE)
|
||||
{
|
||||
// Diagonal BL(0,0) → TR(1,1). Triangles: {BL,BR,TR} / {BL,TR,TL}.
|
||||
if (tx > ty)
|
||||
{
|
||||
// {BL,BR,TR}: Z = hBL + (hBR-hBL)·tx + (hTR-hBR)·ty
|
||||
z = hBL + (hBR - hBL) * tx + (hTR - hBR) * ty;
|
||||
dzdx = (hBR - hBL) / CellSize;
|
||||
dzdy = (hTR - hBR) / CellSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
// {BL,TR,TL}: Z = hBL + (hTR-hTL)·tx + (hTL-hBL)·ty
|
||||
z = hBL + (hTR - hTL) * tx + (hTL - hBL) * ty;
|
||||
dzdx = (hTR - hTL) / CellSize;
|
||||
dzdy = (hTL - hBL) / CellSize;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Diagonal BR(1,0) → TL(0,1). Triangles: {BL,BR,TL} / {BR,TR,TL}.
|
||||
if (tx + ty <= 1f)
|
||||
{
|
||||
// {BL,BR,TL}: Z = hBL + (hBR-hBL)·tx + (hTL-hBL)·ty
|
||||
z = hBL + (hBR - hBL) * tx + (hTL - hBL) * ty;
|
||||
dzdx = (hBR - hBL) / CellSize;
|
||||
dzdy = (hTL - hBL) / CellSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
// {BR,TR,TL}: Z = hTR + (hTL-hTR)(1-tx) + (hBR-hTR)(1-ty)
|
||||
// Equivalent linear form: Z = [hBR+hTL-hTR] + (hTR-hTL)·tx + (hTR-hBR)·ty
|
||||
z = hTR + (hTL - hTR) * (1f - tx) + (hBR - hTR) * (1f - ty);
|
||||
dzdx = (hTR - hTL) / CellSize;
|
||||
dzdy = (hTR - hBR) / CellSize;
|
||||
}
|
||||
}
|
||||
|
||||
var normal = System.Numerics.Vector3.Normalize(
|
||||
new System.Numerics.Vector3(-dzdx, -dzdy, 1f));
|
||||
return (z, normal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute the outdoor cell ID for the given landblock-local position.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue