Wire the BFS visibility root to DataCache.CellGraph.CurrCell (the physics membership answer written in W2 Task 1) rather than resolving independently from a position via FindCameraCell. Closes the render/physics disagreement that causes the "world from below" spawn-in flicker. Changes: - CellVisibility.GetVisibleCells: extracted BFS body into new private GetVisibleCellsFromRoot(LoadedCell root, Vector3 cameraPos); existing GetVisibleCells delegates to it after FindCameraCell (behavior unchanged). - CellVisibility.ComputeVisibilityFromRoot(LoadedCell? root, Vector3 fallbackPos): new public entry point; when root is null falls through to ComputeVisibility (exact today's behavior), otherwise sets _lastCameraCell = root and delegates to GetVisibleCellsFromRoot — cannot regress below baseline. - GameWindow (line 7156): replaced ComputeVisibility(visRootPos) with ComputeVisibilityFromRoot(physicsRoot, visRootPos) where physicsRoot is resolved from _physicsEngine.DataCache.CellGraph.CurrCell via TryGetCell. physicsRoot is null whenever CurrCell is null or its id is not yet in the render registry, so the fallback fires until the cell loads. - 6 new tests in CellVisibilityFromRootTests: null-root fallback equivalence (3 cases), registered root → CameraCell == root (3 cases). All 160 App.Tests pass, 0 regressions. Visual verification PENDING — behavior change; do not claim it works visually. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
150 lines
5.4 KiB
C#
150 lines
5.4 KiB
C#
// CellVisibilityFromRootTests.cs — UCG W2 Task 2: tests for
|
|
// CellVisibility.ComputeVisibilityFromRoot.
|
|
//
|
|
// Two acceptance criteria (from the W2 Task 2 spec):
|
|
// (a) ComputeVisibilityFromRoot(null, pos) is fallback-equivalent to
|
|
// ComputeVisibility(pos) — same CameraCell answer.
|
|
// (b) ComputeVisibilityFromRoot(root, pos) with a registered root returns
|
|
// a result whose CameraCell is that root, regardless of whether 'pos'
|
|
// is geometrically inside it.
|
|
//
|
|
// CellVisibility is intentionally free of GL types — it can be unit-tested
|
|
// without a GPU context (confirmed: only System.Numerics dependency).
|
|
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public class CellVisibilityFromRootTests
|
|
{
|
|
// ------------------------------------------------------------------
|
|
// Helpers
|
|
// ------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Build a minimal LoadedCell with an axis-aligned bounding box and identity
|
|
/// transform so PointInCell works for a position inside the box.
|
|
/// </summary>
|
|
private static LoadedCell MakeCell(uint cellId, Vector3 boundsMin, Vector3 boundsMax)
|
|
{
|
|
return new LoadedCell
|
|
{
|
|
CellId = cellId,
|
|
WorldTransform = Matrix4x4.Identity,
|
|
InverseWorldTransform = Matrix4x4.Identity,
|
|
LocalBoundsMin = boundsMin,
|
|
LocalBoundsMax = boundsMax,
|
|
Portals = new(),
|
|
ClipPlanes = new(),
|
|
PortalPolygons = new(),
|
|
};
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// (a) Fallback equivalence: null root → same answer as ComputeVisibility
|
|
// ------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void ComputeVisibilityFromRoot_NullRoot_FallsBackToPositionBased()
|
|
{
|
|
// Arrange: one cell covering [0,10]^3, position inside it.
|
|
var cv = new CellVisibility();
|
|
var cell = MakeCell(0xA9B40101u, Vector3.Zero, new Vector3(10, 10, 10));
|
|
cv.AddCell(cell);
|
|
|
|
var pos = new Vector3(5, 5, 5); // inside the cell
|
|
|
|
// Act: null-root and direct-position paths must agree on CameraCell.
|
|
var fromPos = cv.ComputeVisibility(pos);
|
|
var fromNull = cv.ComputeVisibilityFromRoot(null, pos);
|
|
|
|
// Assert
|
|
Assert.NotNull(fromPos);
|
|
Assert.NotNull(fromNull);
|
|
Assert.Equal(fromPos!.CameraCell?.CellId, fromNull!.CameraCell?.CellId);
|
|
Assert.Equal(cell.CellId, fromNull.CameraCell?.CellId);
|
|
}
|
|
|
|
[Fact]
|
|
public void ComputeVisibilityFromRoot_NullRoot_NoCells_ReturnsNull()
|
|
{
|
|
// With no cells registered both paths return null.
|
|
var cv = new CellVisibility();
|
|
var posOutdoors = new Vector3(100, 100, 100);
|
|
|
|
var fromPos = cv.ComputeVisibility(posOutdoors);
|
|
var fromNull = cv.ComputeVisibilityFromRoot(null, posOutdoors);
|
|
|
|
Assert.Null(fromPos);
|
|
Assert.Null(fromNull);
|
|
}
|
|
|
|
[Fact]
|
|
public void ComputeVisibilityFromRoot_NullRoot_PositionOutsideAllCells_ReturnsNull()
|
|
{
|
|
// Cell exists but position is outside it — both paths produce null CameraCell.
|
|
var cv = new CellVisibility();
|
|
var cell = MakeCell(0xA9B40102u, Vector3.Zero, new Vector3(5, 5, 5));
|
|
cv.AddCell(cell);
|
|
|
|
var posOutside = new Vector3(100, 100, 100);
|
|
|
|
var fromPos = cv.ComputeVisibility(posOutside);
|
|
var fromNull = cv.ComputeVisibilityFromRoot(null, posOutside);
|
|
|
|
// Both should return null (no grace frames built up yet)
|
|
Assert.Null(fromPos?.CameraCell);
|
|
Assert.Null(fromNull?.CameraCell);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// (b) Supplied root is used as BFS root → CameraCell == root
|
|
// ------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void ComputeVisibilityFromRoot_RegisteredRoot_CameraCellIsSuppliedRoot()
|
|
{
|
|
// Arrange: cell registered in CellVisibility.
|
|
var cv = new CellVisibility();
|
|
var cell = MakeCell(0xA9B40103u, Vector3.Zero, new Vector3(10, 10, 10));
|
|
cv.AddCell(cell);
|
|
|
|
// The position can be OUTSIDE the cell — physics already determined membership
|
|
// via BSP, we just trust that answer.
|
|
var posAnywhere = new Vector3(999, 999, 999);
|
|
|
|
// Act
|
|
var result = cv.ComputeVisibilityFromRoot(cell, posAnywhere);
|
|
|
|
// Assert: CameraCell is the supplied root.
|
|
Assert.NotNull(result);
|
|
Assert.Same(cell, result!.CameraCell);
|
|
}
|
|
|
|
[Fact]
|
|
public void ComputeVisibilityFromRoot_RegisteredRoot_IncludesRootInVisibleCells()
|
|
{
|
|
var cv = new CellVisibility();
|
|
var cell = MakeCell(0xA9B40104u, Vector3.Zero, new Vector3(10, 10, 10));
|
|
cv.AddCell(cell);
|
|
|
|
var result = cv.ComputeVisibilityFromRoot(cell, Vector3.Zero);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Contains(cell.CellId, result!.VisibleCellIds);
|
|
}
|
|
|
|
[Fact]
|
|
public void ComputeVisibilityFromRoot_RegisteredRoot_LastVisibilityResultUpdated()
|
|
{
|
|
var cv = new CellVisibility();
|
|
var cell = MakeCell(0xA9B40105u, Vector3.Zero, new Vector3(10, 10, 10));
|
|
cv.AddCell(cell);
|
|
|
|
var result = cv.ComputeVisibilityFromRoot(cell, Vector3.Zero);
|
|
|
|
Assert.Same(result, cv.LastVisibilityResult);
|
|
}
|
|
}
|