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:
Erik 2026-04-12 21:29:54 +02:00
parent 6f05c298cf
commit a3b389603d
4 changed files with 42 additions and 40 deletions

View file

@ -1757,8 +1757,17 @@ public sealed class GameWindow : IDisposable
var camera = _cameraController.Active;
var frustum = AcDream.App.Rendering.FrustumPlanes.FromViewProjection(camera.View * camera.Projection);
_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,
alwaysVisibleEntityId: _playerMode ? _playerServerGuid : null);
neverCullLandblockId: playerLb);
// Count visible vs total for the perf overlay.
foreach (var entry in _worldState.LandblockEntries)

View file

@ -79,7 +79,7 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
public void Draw(ICamera camera,
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList<WorldEntity> Entities)> landblockEntries,
FrustumPlanes? frustum = null,
uint? alwaysVisibleEntityId = null)
uint? neverCullLandblockId = null)
{
_shader.Use();
_shader.SetMatrix4("uView", camera.View);
@ -90,21 +90,11 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
// alpha-discard path in the fragment shader (uTranslucencyKind == 1).
foreach (var entry in landblockEntries)
{
// Per-landblock frustum cull: one AABB test skips all entities in
// this landblock if it is fully outside the view frustum.
// Exception: never cull a landblock containing the player entity.
// Per-landblock frustum cull. Never cull the player's landblock.
if (frustum is not null &&
entry.LandblockId != neverCullLandblockId &&
!FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax))
{
// 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;
}
continue;
foreach (var entity in entry.Entities)
{
@ -179,18 +169,11 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
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 &&
entry.LandblockId != neverCullLandblockId &&
!FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax))
{
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;
}
continue;
foreach (var entity in entry.Entities)
{