Render unification cutover, Step A (additive, behavior-neutral until Step B). When PortalVisibilityBuilder.Build roots at the synthetic outdoor node, seed OutsideView with the full-screen NDC quad so ClipFrameAssembler yields a full-screen OutsideView slice and DrawInside's DrawLandscapeThroughOutsideView draws terrain/sky/scenery/weather as the node's shell — the same callback that already draws the doorway slice for an interior root looking out. Keyed on a new explicit LoadedCell.IsOutdoorNode flag (set by OutdoorCellNode.Build), NOT a cell-id heuristic: production EnvCell ids are >= 0x100 but test fixtures use low interior ids, so an id test misfired on 4 existing PortalVisibilityBuilderTests. Nothing roots at the node until Step B, so this is behavior-neutral. Tests: App 216/0 (2 new UnifiedFloodTests incl. the spec section 10 pure-outdoor regression guard + 2 OutdoorCellNode flag assertions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
5.3 KiB
C#
109 lines
5.3 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public class UnifiedFloodTests
|
|
{
|
|
// Shared building fixture: a building cell whose interior is at Y >= 5.
|
|
// The exit portal faces -Y (Normal=(0,-1,0)); interior is where dot<=0 -> Y>=5 (InsideSide=1).
|
|
// The outdoor camera is at Y=-3, which is the OUTDOOR side (Y<5).
|
|
// OutdoorCellNode.Build flips InsideSide to 0 so the outdoor camera (dot>=0 i.e. Y<5) passes
|
|
// the side test and the flood reaches the building.
|
|
private static LoadedCell MakeBuildingCell(uint cellId)
|
|
{
|
|
var building = new LoadedCell { CellId = cellId, SeenOutside = true };
|
|
building.WorldTransform = Matrix4x4.Identity;
|
|
building.InverseWorldTransform = Matrix4x4.Identity;
|
|
building.Portals.Add(new CellPortalInfo(0xFFFF, 0, 0, 0));
|
|
// InsideSide=1: interior where (0,-1,0)·p+5 <= 0 -> Y>=5 (the building body is at Y>=5).
|
|
building.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, -1, 0), D = 5f, InsideSide = 1 });
|
|
building.PortalPolygons.Add(new[]
|
|
{
|
|
new Vector3(-1, 5, 0), new Vector3(1, 5, 0), new Vector3(1, 5, 2), new Vector3(-1, 5, 2)
|
|
});
|
|
return building;
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_RootedAtOutdoorNode_FloodsIntoBuilding()
|
|
{
|
|
var building = MakeBuildingCell(0xA9B40170);
|
|
var node = OutdoorCellNode.Build(0xA9B40031, new[] { building });
|
|
LoadedCell? Lookup(uint id) => (id & 0xFFFFu) == 0x0170 ? building : null;
|
|
|
|
var eye = new Vector3(0, -3, 1);
|
|
var view = Matrix4x4.CreateLookAt(eye, new Vector3(0, 5, 1), Vector3.UnitZ);
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 1f, 5000f);
|
|
|
|
var frame = PortalVisibilityBuilder.Build(node, eye, Lookup, view * proj);
|
|
|
|
Assert.Contains(0xA9B40031u, frame.OrderedVisibleCells); // the outdoor node itself
|
|
Assert.Contains(0xA9B40170u, frame.OrderedVisibleCells); // flooded into the building
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_OutdoorBuildingCycle_Terminates()
|
|
{
|
|
// Building's exit portal reciprocally points back near the node; assert Build
|
|
// returns (does not hang) and the visible set is bounded/small.
|
|
var building = MakeBuildingCell(0xA9B40170);
|
|
var node = OutdoorCellNode.Build(0xA9B40031, new[] { building });
|
|
LoadedCell? Lookup(uint id) => (id & 0xFFFFu) == 0x0170 ? building
|
|
: (id & 0xFFFFu) == 0x0031 ? node : null;
|
|
var eye = new Vector3(0, -3, 1);
|
|
var view = Matrix4x4.CreateLookAt(eye, new Vector3(0, 5, 1), Vector3.UnitZ);
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 1f, 5000f);
|
|
|
|
var frame = PortalVisibilityBuilder.Build(node, eye, Lookup, view * proj);
|
|
Assert.True(frame.OrderedVisibleCells.Count < 10); // bounded, no runaway
|
|
}
|
|
|
|
// Step A (cutover): rooting at the outdoor node must seed OutsideView with the FULL-SCREEN NDC
|
|
// quad so DrawLandscapeThroughOutsideView draws the landscape as the node's shell. Without this
|
|
// the outdoor-root frame would have OutsideViewSlices.Length==0 -> TerrainMode.Skip -> no terrain.
|
|
[Fact]
|
|
public void Build_RootedAtOutdoorNode_SeedsFullScreenOutsideView()
|
|
{
|
|
var building = MakeBuildingCell(0xA9B40170);
|
|
var node = OutdoorCellNode.Build(0xA9B40031, new[] { building });
|
|
LoadedCell? Lookup(uint id) => (id & 0xFFFFu) == 0x0170 ? building : null;
|
|
|
|
var eye = new Vector3(0, -3, 1);
|
|
var view = Matrix4x4.CreateLookAt(eye, new Vector3(0, 5, 1), Vector3.UnitZ);
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 1f, 5000f);
|
|
|
|
var frame = PortalVisibilityBuilder.Build(node, eye, Lookup, view * proj);
|
|
|
|
Assert.False(frame.OutsideView.IsEmpty);
|
|
// The union bounds of OutsideView must cover the whole NDC viewport (a full-screen quad),
|
|
// so ClipFrameAssembler yields TerrainMode != Skip and the terrain draws everywhere.
|
|
Assert.Equal(-1f, frame.OutsideView.MinX, 3);
|
|
Assert.Equal(-1f, frame.OutsideView.MinY, 3);
|
|
Assert.Equal(1f, frame.OutsideView.MaxX, 3);
|
|
Assert.Equal(1f, frame.OutsideView.MaxY, 3);
|
|
}
|
|
|
|
// Pure-outdoor regression guard (spec section 10): an outdoor node with NO nearby buildings must
|
|
// resolve to exactly {node} with a full-screen OutsideView -> full-screen terrain, no interior
|
|
// cells, byte-for-byte today's open-world draw. Visual-gate the open field, not just the cottage.
|
|
[Fact]
|
|
public void Build_EmptyOutdoorNode_FullScreenOutsideView_OnlyNodeVisible()
|
|
{
|
|
var node = OutdoorCellNode.Build(0xA9B40031, Array.Empty<LoadedCell>());
|
|
Assert.Empty(node.Portals); // no doorways
|
|
|
|
var eye = new Vector3(0, -3, 1);
|
|
var view = Matrix4x4.CreateLookAt(eye, new Vector3(0, 5, 1), Vector3.UnitZ);
|
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 1f, 5000f);
|
|
|
|
var frame = PortalVisibilityBuilder.Build(node, eye, _ => null, view * proj);
|
|
|
|
Assert.Equal(new[] { 0xA9B40031u }, frame.OrderedVisibleCells); // only the node
|
|
Assert.False(frame.OutsideView.IsEmpty);
|
|
Assert.Equal(-1f, frame.OutsideView.MinX, 3);
|
|
Assert.Equal(1f, frame.OutsideView.MaxX, 3);
|
|
}
|
|
}
|