test(render): Phase U.4c — reproduce the doorway flap (RED apparatus)

Synthetic C0->C1->C2(exit) chain; two camera poses straddle the C0->C1
side-test boundary by a few cm. Pre-fix, pose B hard-culls C0->C1 and the
exit cell drops -> OutsideView empties (the flap). Gates the U.4c fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-31 09:57:44 +02:00
parent 211350b8a6
commit cd3ffe3b02

View file

@ -364,6 +364,61 @@ public class PortalVisibilityBuilderTests
$"neighbour CellView MinX {bView.MinX} must still cover the LEFT reciprocal opening (minX {leftMinX})");
}
// -----------------------------------------------------------------------
// Phase U.4c: the threshold "flap". A chain camera(C0) -> mid(C1) -> exit(C2)
// where the C0->C1 portal's clip plane sits just in front of the camera.
// BOTH poses are legitimately inside C0 and SHOULD see the exit window; they
// straddle the C0->C1 side-test boundary by a few cm. Pre-fix, the pose just
// behind the plane hard-culls C0->C1 (CameraOnInteriorSide), C2 is never
// reached, and OutsideView empties — the flap. The fix must keep the exit
// cell visible (OutsideView non-empty) at BOTH poses.
// -----------------------------------------------------------------------
private static Matrix4x4 ViewProjAt(Vector3 eye)
{
var view = Matrix4x4.CreateLookAt(eye, eye + new Vector3(0, 0, -1), Vector3.UnitY);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f);
return view * proj;
}
private static (LoadedCell cam, Dictionary<uint, LoadedCell> all) FlapChain()
{
const uint C0 = 0x0001, C1 = 0x0002, C2 = 0x0003;
// C0 -> C1 portal at z=-1, with a clip plane (normal +Z, InsideSide=0) at z=-1.
// dot = camZ + D; with D = 1 the plane is at camZ = -1: inside iff camZ >= -1 - eps.
var c0 = Cell(C0, new CellPortalInfo((ushort)C1, 0, 0, 0));
c0.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -1f));
c0.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, 0, 1), D = 1f, InsideSide = 0 });
// C1 -> C2 (no clip plane → never culled), C2 has the exit window.
var c1 = Cell(C1, new CellPortalInfo((ushort)C2, 0, 0, 0));
c1.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -4f));
var c2 = Cell(C2, new CellPortalInfo(0xFFFF, 0, 0, 0));
c2.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -7f));
var all = new Dictionary<uint, LoadedCell> { [C0] = c0, [C1] = c1, [C2] = c2 };
return (c0, all);
}
[Fact]
public void Build_NearBoundaryIntermediatePortal_ExitCellStaysVisibleAcrossPose()
{
var (cam, all) = FlapChain();
Func<uint, LoadedCell?> lookup = id => all.TryGetValue(id, out var c) ? c : null;
// Pose A: a few cm IN FRONT of the C0->C1 plane (camZ = -0.9 >= -1 → inside).
var poseA = new Vector3(0, 0, -0.9f);
var frameA = PortalVisibilityBuilder.Build(cam, poseA, lookup, ViewProjAt(poseA));
// Pose B: a few cm BEHIND it (camZ = -1.1 < -1 → pre-fix the side test culls C0->C1).
var poseB = new Vector3(0, 0, -1.1f);
var frameB = PortalVisibilityBuilder.Build(cam, poseB, lookup, ViewProjAt(poseB));
// The exit cell — and therefore OutsideView — must be present at BOTH poses.
Assert.False(frameA.OutsideView.IsEmpty, "pose A should see the exit window");
Assert.False(frameB.OutsideView.IsEmpty,
"pose B (a few cm away) must ALSO see the exit window — this is the flap: " +
"an intermediate side-test flip must not drop the exit cell from the set");
Assert.Contains(0x0003u, frameB.OrderedVisibleCells);
}
// Signed-area-magnitude (shoelace) sum over a CellView's polygons in NDC.
private static float CellViewArea(CellView view)
{