`FrustumPlanes.FromViewProjection` extracted the near plane with the Gribb-Hartmann form written for OpenGL's `[-1,1]` clip-space z range. Every acdream projection comes from `Matrix4x4.CreatePerspectiveFieldOfView` or `CreateOrthographic`, whose range is `[0,1]`. Under `[-1,1]` the near plane is the locus of `clip.z = -clip.w`, which is `col4 + col3`; under `[0,1]` it is `clip.z = 0`, which is `col3` alone. Concretely, the mismatch put the effective near threshold at `-n·f/(2f-n)` — about 0.5 m where the retail chase camera asks for 1.0 m. That error only ever kept geometry the true frustum would have dropped, never the reverse, which is why it produced no visible defect and was filed instead of hot-fixed during Campaign V. It is still wrong, and it is the same mistake that *was* visible in `PortalProjection`, where it culled the cell behind a doorway the camera stood close to. The far plane is `col4 - col3` under both conventions and is untouched. A test pins it anyway, so that a future edit to this function cannot drift it while nobody is looking. The acceptance criterion asked for a unit test pinning the extracted near distance to the camera's near value, and that is what landed: a theory over four near/far pairs asserting the plane is unit-length, faces down -Z, and stands off the eye by exactly `nearDistance`, plus a kept/dropped pair straddling it. The test was checked against the old formula before commit and fails all four cases there — it measures the fix rather than merely accompanying it. The other half of the acceptance criterion — unchanged culling in the offline pixel gate and the connected route — could not be run: #259 has Win32 surface creation failing machine-wide, so no gate that needs a window is available tonight. Recorded as outstanding rather than assumed. Solution build 0 errors; `AcDream.Core.Tests` 3,898 passed / 2 skipped / 3,900. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
200 lines
7.5 KiB
C#
200 lines
7.5 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)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0.1f, 1000f)]
|
|
[InlineData(1f, 1000f)]
|
|
[InlineData(1f, 100f)]
|
|
[InlineData(2.5f, 5000f)]
|
|
public void PerspectiveCamera_ExtractedNearPlane_SitsAtTheCameraNearDistance(
|
|
float nearDistance, float farDistance)
|
|
{
|
|
// #248: the extracted near plane must sit at exactly the camera's near
|
|
// value. The camera sits at the origin looking down -Z, so the near
|
|
// plane's signed distance from the eye IS `nearDistance`, and for a
|
|
// plane stored as (normal, d) with the eye at the origin that signed
|
|
// distance is just `-d`.
|
|
//
|
|
// This is what distinguishes the [0,1] extraction from the [-1,1] one:
|
|
// the GL form `col4 + col3` would report -n·f/(2f-n) here — about 0.5
|
|
// for a 1.0 near — so any regression back to it fails this outright.
|
|
var view = Matrix4x4.CreateLookAt(
|
|
new Vector3(0, 0, 0),
|
|
new Vector3(0, 0, -1),
|
|
Vector3.UnitY);
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
|
|
MathF.PI / 3f, 16f / 9f, nearDistance, farDistance);
|
|
var planes = FrustumPlanes.FromViewProjection(view * proj);
|
|
|
|
var near = planes.Near;
|
|
|
|
// The plane is normalized and faces down -Z, into the frustum.
|
|
Assert.Equal(1f, new Vector3(near.X, near.Y, near.Z).Length(), 4);
|
|
Assert.Equal(0f, near.X, 4);
|
|
Assert.Equal(0f, near.Y, 4);
|
|
Assert.Equal(-1f, near.Z, 4);
|
|
|
|
// And it stands off the eye by exactly the camera's near distance.
|
|
Assert.Equal(nearDistance, -near.W, 3);
|
|
|
|
// A point just inside the near plane is kept; one just outside is not.
|
|
float eps = nearDistance * 0.01f;
|
|
Assert.True(FrustumCuller.IsAabbVisible(planes,
|
|
new Vector3(-0.01f, -0.01f, -(nearDistance + eps)),
|
|
new Vector3(0.01f, 0.01f, -(nearDistance + eps))));
|
|
Assert.False(FrustumCuller.IsAabbVisible(planes,
|
|
new Vector3(-0.01f, -0.01f, -(nearDistance - eps)),
|
|
new Vector3(0.01f, 0.01f, -(nearDistance - eps))));
|
|
}
|
|
|
|
[Fact]
|
|
public void PerspectiveCamera_FarPlane_IsUnchangedByTheNearFix()
|
|
{
|
|
// #248 touches NEAR only. `col4 - col3` is the far plane under both
|
|
// clip-space conventions, so pin it alongside to prove the fix did not
|
|
// drift it.
|
|
const float FarDistance = 100f;
|
|
var view = Matrix4x4.CreateLookAt(
|
|
new Vector3(0, 0, 0),
|
|
new Vector3(0, 0, -1),
|
|
Vector3.UnitY);
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
|
|
MathF.PI / 3f, 16f / 9f, 1f, FarDistance);
|
|
var planes = FrustumPlanes.FromViewProjection(view * proj);
|
|
|
|
var far = planes.Far;
|
|
Assert.Equal(1f, new Vector3(far.X, far.Y, far.Z).Length(), 4);
|
|
Assert.Equal(1f, far.Z, 4);
|
|
Assert.Equal(FarDistance, far.W, 3);
|
|
}
|
|
|
|
[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)));
|
|
}
|
|
}
|