fix(app): multi-point Z sampling + never-cull player landblock
1. Slope clipping: replaced single foot-forward Z sample with 4-point sampling (forward, back, left, right at 0.7 units). Takes the max Z across all samples so both uphill and downhill slopes keep feet above the terrain mesh surface. Removed the +0.1 Z bias entirely. 2. Player culling: replaced per-entity scan (alwaysVisibleEntityId) with per-landblock skip (neverCullLandblockId). The player's current landblock is computed from _playerController.Position and passed to the renderer. Simpler, faster, and more reliable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6f05c298cf
commit
a3b389603d
4 changed files with 42 additions and 40 deletions
|
|
@ -206,22 +206,32 @@ public sealed class PlayerMovementController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Foot-forward Z sampling: on slopes the visual terrain at the
|
// Multi-point Z sampling: on slopes the visual terrain mesh can be
|
||||||
// character's feet (slightly forward) can be higher than at the center.
|
// higher or lower than the center-point physics sample. Sample 4
|
||||||
// Sample a point ~1 unit ahead in the walk direction and use the MAX
|
// points around the character (forward, back, left, right at ~0.7
|
||||||
// of center and foot Z. Only when grounded and moving.
|
// units — roughly foot distance) and use the MAX Z. This prevents
|
||||||
if (!IsAirborne && (dx != 0f || dy != 0f))
|
// feet clipping on both uphill and downhill slopes.
|
||||||
|
if (!IsAirborne)
|
||||||
{
|
{
|
||||||
float footX = result.Position.X + forwardX * 1.0f;
|
const float sampleDist = 0.7f;
|
||||||
float footY = result.Position.Y + forwardY * 1.0f;
|
ReadOnlySpan<(float ox, float oy)> offsets = stackalloc (float, float)[]
|
||||||
var footResult = _physics.Resolve(
|
{
|
||||||
new Vector3(footX, footY, newZ), CellId,
|
( forwardX * sampleDist, forwardY * sampleDist), // forward
|
||||||
Vector3.Zero, StepUpHeight);
|
(-forwardX * sampleDist, -forwardY * sampleDist), // back
|
||||||
if (footResult.IsOnGround && footResult.Position.Z > newZ)
|
( forwardY * sampleDist, -forwardX * sampleDist), // right
|
||||||
newZ = footResult.Position.Z;
|
(-forwardY * sampleDist, forwardX * sampleDist), // left
|
||||||
|
};
|
||||||
|
foreach (var (ox, oy) in offsets)
|
||||||
|
{
|
||||||
|
var sampleResult = _physics.Resolve(
|
||||||
|
new Vector3(result.Position.X + ox, result.Position.Y + oy, newZ),
|
||||||
|
CellId, Vector3.Zero, StepUpHeight);
|
||||||
|
if (sampleResult.IsOnGround && sampleResult.Position.Z > newZ)
|
||||||
|
newZ = sampleResult.Position.Z;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_prevGroundZ = newZ;
|
_prevGroundZ = newZ;
|
||||||
Position = new Vector3(result.Position.X, result.Position.Y, newZ + 0.1f);
|
Position = new Vector3(result.Position.X, result.Position.Y, newZ);
|
||||||
CellId = result.CellId;
|
CellId = result.CellId;
|
||||||
|
|
||||||
// 4. Determine current motion commands.
|
// 4. Determine current motion commands.
|
||||||
|
|
|
||||||
|
|
@ -1757,8 +1757,17 @@ public sealed class GameWindow : IDisposable
|
||||||
var camera = _cameraController.Active;
|
var camera = _cameraController.Active;
|
||||||
var frustum = AcDream.App.Rendering.FrustumPlanes.FromViewProjection(camera.View * camera.Projection);
|
var frustum = AcDream.App.Rendering.FrustumPlanes.FromViewProjection(camera.View * camera.Projection);
|
||||||
_terrain?.Draw(camera, frustum);
|
_terrain?.Draw(camera, frustum);
|
||||||
|
// Never cull the landblock the player is currently on.
|
||||||
|
uint? playerLb = null;
|
||||||
|
if (_playerMode && _playerController is not null)
|
||||||
|
{
|
||||||
|
var pp = _playerController.Position;
|
||||||
|
int plx = _liveCenterX + (int)System.Math.Floor(pp.X / 192f);
|
||||||
|
int ply = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f);
|
||||||
|
playerLb = (uint)((plx << 24) | (ply << 16) | 0xFFFF);
|
||||||
|
}
|
||||||
_staticMesh?.Draw(camera, _worldState.LandblockEntries, frustum,
|
_staticMesh?.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||||
alwaysVisibleEntityId: _playerMode ? _playerServerGuid : null);
|
neverCullLandblockId: playerLb);
|
||||||
|
|
||||||
// Count visible vs total for the perf overlay.
|
// Count visible vs total for the perf overlay.
|
||||||
foreach (var entry in _worldState.LandblockEntries)
|
foreach (var entry in _worldState.LandblockEntries)
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
|
||||||
public void Draw(ICamera camera,
|
public void Draw(ICamera camera,
|
||||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList<WorldEntity> Entities)> landblockEntries,
|
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList<WorldEntity> Entities)> landblockEntries,
|
||||||
FrustumPlanes? frustum = null,
|
FrustumPlanes? frustum = null,
|
||||||
uint? alwaysVisibleEntityId = null)
|
uint? neverCullLandblockId = null)
|
||||||
{
|
{
|
||||||
_shader.Use();
|
_shader.Use();
|
||||||
_shader.SetMatrix4("uView", camera.View);
|
_shader.SetMatrix4("uView", camera.View);
|
||||||
|
|
@ -90,21 +90,11 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
|
||||||
// alpha-discard path in the fragment shader (uTranslucencyKind == 1).
|
// alpha-discard path in the fragment shader (uTranslucencyKind == 1).
|
||||||
foreach (var entry in landblockEntries)
|
foreach (var entry in landblockEntries)
|
||||||
{
|
{
|
||||||
// Per-landblock frustum cull: one AABB test skips all entities in
|
// Per-landblock frustum cull. Never cull the player's landblock.
|
||||||
// this landblock if it is fully outside the view frustum.
|
|
||||||
// Exception: never cull a landblock containing the player entity.
|
|
||||||
if (frustum is not null &&
|
if (frustum is not null &&
|
||||||
|
entry.LandblockId != neverCullLandblockId &&
|
||||||
!FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax))
|
!FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax))
|
||||||
{
|
continue;
|
||||||
// Check if this landblock contains the always-visible entity.
|
|
||||||
bool hasAlwaysVisible = false;
|
|
||||||
if (alwaysVisibleEntityId is not null)
|
|
||||||
{
|
|
||||||
foreach (var e in entry.Entities)
|
|
||||||
if (e.Id == alwaysVisibleEntityId.Value) { hasAlwaysVisible = true; break; }
|
|
||||||
}
|
|
||||||
if (!hasAlwaysVisible) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entity in entry.Entities)
|
foreach (var entity in entry.Entities)
|
||||||
{
|
{
|
||||||
|
|
@ -179,18 +169,11 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
|
||||||
|
|
||||||
foreach (var entry in landblockEntries)
|
foreach (var entry in landblockEntries)
|
||||||
{
|
{
|
||||||
// Same per-landblock frustum cull + always-visible exception for pass 2.
|
// Same per-landblock frustum cull for pass 2.
|
||||||
if (frustum is not null &&
|
if (frustum is not null &&
|
||||||
|
entry.LandblockId != neverCullLandblockId &&
|
||||||
!FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax))
|
!FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax))
|
||||||
{
|
continue;
|
||||||
bool hasAlwaysVisible = false;
|
|
||||||
if (alwaysVisibleEntityId is not null)
|
|
||||||
{
|
|
||||||
foreach (var e in entry.Entities)
|
|
||||||
if (e.Id == alwaysVisibleEntityId.Value) { hasAlwaysVisible = true; break; }
|
|
||||||
}
|
|
||||||
if (!hasAlwaysVisible) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entity in entry.Entities)
|
foreach (var entity in entry.Entities)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ public class PlayerMovementControllerTests
|
||||||
|
|
||||||
Assert.False(controller.IsAirborne, "Should have landed");
|
Assert.False(controller.IsAirborne, "Should have landed");
|
||||||
// +0.15 Z bias keeps feet above terrain surface (prevents z-fighting).
|
// +0.15 Z bias keeps feet above terrain surface (prevents z-fighting).
|
||||||
Assert.Equal(50.1f, controller.Position.Z, precision: 1);
|
Assert.Equal(50f, controller.Position.Z, precision: 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -177,6 +177,6 @@ public class PlayerMovementControllerTests
|
||||||
controller.Update(0.05f, new MovementInput(Forward: true));
|
controller.Update(0.05f, new MovementInput(Forward: true));
|
||||||
|
|
||||||
Assert.False(controller.IsAirborne, "Player should have landed");
|
Assert.False(controller.IsAirborne, "Player should have landed");
|
||||||
Assert.Equal(20.1f, controller.Position.Z, precision: 1);
|
Assert.Equal(20f, controller.Position.Z, precision: 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue