Per-landblock frustum culling for the streaming renderer. Extracts
6 normalized view-frustum planes from a View×Projection matrix using
the standard Gribb-Hartmann method. IsAabbVisible tests the AABB's
most-positive vertex against each plane — conservative (no false
negatives) and zero-allocation.
Key implementation note: System.Numerics.Matrix4x4 uses ROW-VECTOR
convention (clip = worldPos * VP), so Gribb-Hartmann must operate on
the COLUMNS of the matrix (not rows). The spec's row-based pseudocode
assumed column-major (OpenGL) convention; the fix is col4 ± col{1..3}.
7 new tests covering ortho, perspective (front/behind/left/far/
near-straddling), and acdream's actual Z-up camera convention.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
133 lines
4.7 KiB
C#
133 lines
4.7 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Rendering;
|
|
|
|
public class FrustumCullerTests
|
|
{
|
|
[Fact]
|
|
public void IdentityVP_EverythingVisible()
|
|
{
|
|
// Identity VP: all planes at infinity, every box is visible.
|
|
// Actually identity VP has degenerate planes — instead use a
|
|
// simple ortho that covers [-1,1]³.
|
|
var vp = Matrix4x4.CreateOrthographic(2f, 2f, 0.1f, 100f);
|
|
var planes = FrustumPlanes.FromViewProjection(vp);
|
|
|
|
// Box at origin, well within the frustum.
|
|
Assert.True(FrustumCuller.IsAabbVisible(planes,
|
|
new Vector3(-0.5f, -0.5f, -50f),
|
|
new Vector3(0.5f, 0.5f, -1f)));
|
|
}
|
|
|
|
[Fact]
|
|
public void PerspectiveCamera_BoxInFront_Visible()
|
|
{
|
|
var view = Matrix4x4.CreateLookAt(
|
|
new Vector3(0, 0, 0),
|
|
new Vector3(0, 0, -1),
|
|
Vector3.UnitY);
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
|
|
MathF.PI / 3f, 1f, 1f, 1000f);
|
|
var planes = FrustumPlanes.FromViewProjection(view * proj);
|
|
|
|
// Box directly in front of the camera.
|
|
Assert.True(FrustumCuller.IsAabbVisible(planes,
|
|
new Vector3(-10f, -10f, -100f),
|
|
new Vector3(10f, 10f, -10f)));
|
|
}
|
|
|
|
[Fact]
|
|
public void PerspectiveCamera_BoxBehind_NotVisible()
|
|
{
|
|
var view = Matrix4x4.CreateLookAt(
|
|
new Vector3(0, 0, 0),
|
|
new Vector3(0, 0, -1),
|
|
Vector3.UnitY);
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
|
|
MathF.PI / 3f, 1f, 1f, 1000f);
|
|
var planes = FrustumPlanes.FromViewProjection(view * proj);
|
|
|
|
// Box entirely behind the camera (positive Z).
|
|
Assert.False(FrustumCuller.IsAabbVisible(planes,
|
|
new Vector3(-10f, -10f, 10f),
|
|
new Vector3(10f, 10f, 100f)));
|
|
}
|
|
|
|
[Fact]
|
|
public void PerspectiveCamera_BoxFarLeft_NotVisible()
|
|
{
|
|
var view = Matrix4x4.CreateLookAt(
|
|
new Vector3(0, 0, 0),
|
|
new Vector3(0, 0, -1),
|
|
Vector3.UnitY);
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
|
|
MathF.PI / 3f, 1f, 1f, 1000f);
|
|
var planes = FrustumPlanes.FromViewProjection(view * proj);
|
|
|
|
// Box way off to the left.
|
|
Assert.False(FrustumCuller.IsAabbVisible(planes,
|
|
new Vector3(-1000f, -10f, -50f),
|
|
new Vector3(-500f, 10f, -10f)));
|
|
}
|
|
|
|
[Fact]
|
|
public void PerspectiveCamera_BoxStraddlingNearPlane_Visible()
|
|
{
|
|
var view = Matrix4x4.CreateLookAt(
|
|
new Vector3(0, 0, 0),
|
|
new Vector3(0, 0, -1),
|
|
Vector3.UnitY);
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
|
|
MathF.PI / 3f, 1f, 1f, 1000f);
|
|
var planes = FrustumPlanes.FromViewProjection(view * proj);
|
|
|
|
// Box that straddles the near plane (conservative: visible).
|
|
Assert.True(FrustumCuller.IsAabbVisible(planes,
|
|
new Vector3(-1f, -1f, -2f),
|
|
new Vector3(1f, 1f, 0.5f)));
|
|
}
|
|
|
|
[Fact]
|
|
public void PerspectiveCamera_BoxBeyondFarPlane_NotVisible()
|
|
{
|
|
var view = Matrix4x4.CreateLookAt(
|
|
new Vector3(0, 0, 0),
|
|
new Vector3(0, 0, -1),
|
|
Vector3.UnitY);
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
|
|
MathF.PI / 3f, 1f, 1f, 100f);
|
|
var planes = FrustumPlanes.FromViewProjection(view * proj);
|
|
|
|
// Box far beyond the far plane.
|
|
Assert.False(FrustumCuller.IsAabbVisible(planes,
|
|
new Vector3(-10f, -10f, -500f),
|
|
new Vector3(10f, 10f, -200f)));
|
|
}
|
|
|
|
[Fact]
|
|
public void AcdreamCamera_LandblockInFront_Visible()
|
|
{
|
|
// Simulate acdream's actual camera setup (Z-up, looking +Y,
|
|
// positioned at Holtburg). A landblock-sized AABB at the
|
|
// camera's forward should be visible.
|
|
var view = Matrix4x4.CreateLookAt(
|
|
new Vector3(96f, 96f, 150f), // camera pos (center of a landblock, elevated)
|
|
new Vector3(96f, 288f, 100f), // looking roughly +Y (forward toward next landblock)
|
|
Vector3.UnitZ); // Z-up
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
|
|
MathF.PI / 3f, 16f / 9f, 1f, 5000f);
|
|
var planes = FrustumPlanes.FromViewProjection(view * proj);
|
|
|
|
// Landblock-sized AABB 192 units ahead in Y.
|
|
Assert.True(FrustumCuller.IsAabbVisible(planes,
|
|
new Vector3(0f, 192f, 50f),
|
|
new Vector3(192f, 384f, 200f)));
|
|
|
|
// Landblock behind the camera (negative Y relative to camera).
|
|
Assert.False(FrustumCuller.IsAabbVisible(planes,
|
|
new Vector3(0f, -384f, 50f),
|
|
new Vector3(192f, -192f, 200f)));
|
|
}
|
|
}
|