fix(core+app): slope gradient compensation for feet clipping
The triangle-aware Z sampling from the previous commit produces exact terrain-surface Z values, but a point-sampled Z on a tilted surface places the character's center on the surface while their feet (which extend horizontally) clip into the rising terrain ahead/behind. Fix: in PlayerMovementController, sample Z 1 unit ahead in the walk direction and add 40% of the gradient as an upward bias. This compensates for the character's collision cylinder radius on slopes while producing zero bias on flat ground. The bias is applied in the movement controller (gameplay concern) not in TerrainSurface.SampleZ (which stays exact for physics/tests). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c5de445e5c
commit
78d43a0914
2 changed files with 22 additions and 28 deletions
|
|
@ -204,10 +204,21 @@ public sealed class PlayerMovementController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Triangle-aware Z sampling in TerrainSurface now matches the visual
|
// Slope compensation: on tilted terrain the character's collision
|
||||||
// terrain mesh exactly (using AC2D's FSplitNESW render formula +
|
// cylinder extends horizontally beyond the center sample point.
|
||||||
// barycentric interpolation within each triangle). No multi-point
|
// The lowest point of the cylinder is ~0.3 units below the center
|
||||||
// sampling hacks needed.
|
// on steep slopes. Add a small upward bias proportional to the
|
||||||
|
// Z change rate to keep feet above the surface. Only when grounded.
|
||||||
|
if (!IsAirborne && result.IsOnGround)
|
||||||
|
{
|
||||||
|
// Sample Z 1 unit ahead and behind to estimate local gradient.
|
||||||
|
float aheadZ = _physics.Resolve(
|
||||||
|
new Vector3(result.Position.X + MathF.Cos(Yaw) * 1f,
|
||||||
|
result.Position.Y + MathF.Sin(Yaw) * 1f, newZ),
|
||||||
|
CellId, Vector3.Zero, StepUpHeight).Position.Z;
|
||||||
|
float gradient = MathF.Abs(aheadZ - newZ);
|
||||||
|
newZ += gradient * 0.4f; // 40% of the gradient as foot-radius compensation
|
||||||
|
}
|
||||||
Position = new Vector3(result.Position.X, result.Position.Y, newZ);
|
Position = new Vector3(result.Position.X, result.Position.Y, newZ);
|
||||||
CellId = result.CellId;
|
CellId = result.CellId;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,13 @@ public sealed class TerrainSurface
|
||||||
/// determine which triangle the point falls in, then does barycentric
|
/// determine which triangle the point falls in, then does barycentric
|
||||||
/// interpolation within that triangle. This matches the visual terrain
|
/// interpolation within that triangle. This matches the visual terrain
|
||||||
/// mesh exactly.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float SampleZ(float localX, float localY)
|
public float SampleZ(float localX, float localY)
|
||||||
{
|
{
|
||||||
|
|
@ -76,41 +83,17 @@ public sealed class TerrainSurface
|
||||||
|
|
||||||
if (splitSWtoNE)
|
if (splitSWtoNE)
|
||||||
{
|
{
|
||||||
// Diagonal from BL(0,0) to TR(1,1)
|
|
||||||
// Triangle 1: BL, BR, TR (below the diagonal: tx >= ty... wait)
|
|
||||||
// Actually: the diagonal goes from (0,0) to (1,1).
|
|
||||||
// Points where ty <= tx are in the bottom-right triangle (BL, BR, TR).
|
|
||||||
// Points where ty > tx are in the top-left triangle (BL, TL, TR).
|
|
||||||
if (ty <= tx)
|
if (ty <= tx)
|
||||||
{
|
|
||||||
// Bottom-right triangle: BL(0,0), BR(1,0), TR(1,1)
|
|
||||||
// Z = hBL + (hBR - hBL) * tx + (hTR - hBR) * ty
|
|
||||||
return hBL + (hBR - hBL) * tx + (hTR - hBR) * ty;
|
return hBL + (hBR - hBL) * tx + (hTR - hBR) * ty;
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
// Top-left triangle: BL(0,0), TL(0,1), TR(1,1)
|
|
||||||
// Z = hBL + (hTR - hTL) * tx + (hTL - hBL) * ty
|
|
||||||
return hBL + (hTR - hTL) * tx + (hTL - hBL) * ty;
|
return hBL + (hTR - hTL) * tx + (hTL - hBL) * ty;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Diagonal from BR(1,0) to TL(0,1)
|
|
||||||
// Points where ty <= (1 - tx) are in the bottom-left triangle (BL, BR, TL).
|
|
||||||
// Points where ty > (1 - tx) are in the top-right triangle (BR, TL, TR).
|
|
||||||
if (ty <= 1f - tx)
|
if (ty <= 1f - tx)
|
||||||
{
|
|
||||||
// Bottom-left triangle: BL(0,0), BR(1,0), TL(0,1)
|
|
||||||
// Z = hBL + (hBR - hBL) * tx + (hTL - hBL) * ty
|
|
||||||
return hBL + (hBR - hBL) * tx + (hTL - hBL) * ty;
|
return hBL + (hBR - hBL) * tx + (hTL - hBL) * ty;
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
// Top-right triangle: BR(1,0), TL(0,1), TR(1,1)
|
|
||||||
// Z = hTR + (hTL - hTR) * (1 - tx) + (hBR - hTR) * (1 - ty)
|
|
||||||
return hTR + (hTL - hTR) * (1f - tx) + (hBR - hTR) * (1f - ty);
|
return hTR + (hTL - hTR) * (1f - tx) + (hBR - hTR) * (1f - ty);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue