feat(render): R-A2 — per-building floods (the flap fix)
Replace the outdoor root's single unified reverse-portal flood (whose root-level portal-side test oscillated as the chase eye grazed a doorway — the measured flood 2<->6) with retail's per-building floods. - OutdoorCellNode.Build(uint): portal-less land root; floods only itself -> full-screen OutsideView -> terrain (PortalVisibilityBuilder IsOutdoorNode seed). - PortalVisibilityBuilder.ConstructViewBuilding: per-building flood seeded at a building's own finite entrance (retail ConstructView(CBldPortal) 0x5a59a0 via DrawPortal 0x5a5ab0 / portal_draw_portals_only 0x53d870). Entrance-bounded -> consistent ~2-cell depth (measured retail cell_draw_num, handoff OPTION-A 3.4). - RetailPViewRenderer.DrawInside: when the root is the outdoor node, group nearby cells by BuildingId and merge each per-building flood into the frame before assembly; existing shells/object-list draw path unchanged. 48 m seed cutoff. - GameWindow: pass flat NearbyBuildingCells only on outdoor-node frames. Tests: +3 PortalVisibilityRobustnessTests (per-building touches ~2 cells, membership stable under the measured 36 um eye jitter). UnifiedFloodTests retired (its subject, the unified flood from the outdoor node, is removed); surviving full-screen-OutsideView coverage moved to OutdoorCellNodeTests. App Rendering 207/207, Core movement 14/14. Conformance-verified sound; the grazing-doorway flap is the visual acceptance test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7fe98098f5
commit
c62663d7cb
8 changed files with 251 additions and 198 deletions
|
|
@ -1,54 +1,52 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// The OUTDOOR render root (R-A2, 2026-06-08): a portal-less <see cref="LoadedCell"/> whose only job is
|
||||
/// to root the render outdoors so the flood seeds OutsideView FULL-SCREEN (terrain draws). Buildings are
|
||||
/// flooded SEPARATELY per-building (<see cref="PortalVisibilityBuilder.ConstructViewBuilding"/>), so the
|
||||
/// node carries NO reverse portals — the pre-R-A2 reverse-portal unified flood (which oscillated at the
|
||||
/// doorway) is gone. Its tests moved here; the old <c>UnifiedFloodTests</c> was retired.
|
||||
/// </summary>
|
||||
public class OutdoorCellNodeTests
|
||||
{
|
||||
// A building cell at world-translate (10,0,0) with one exit portal (OtherCellId=0xFFFF)
|
||||
// whose local plane faces +X (InsideSide=0). The outdoor node must expose ONE portal
|
||||
// back into that building cell, with the entrance polygon moved to world space and the
|
||||
// inside-side flipped (so the outdoor half-space is "inside" the node).
|
||||
private static LoadedCell BuildingWithOneExit(uint cellId)
|
||||
{
|
||||
var cell = new LoadedCell { CellId = cellId };
|
||||
cell.WorldTransform = Matrix4x4.CreateTranslation(10f, 0f, 0f);
|
||||
cell.InverseWorldTransform = Matrix4x4.CreateTranslation(-10f, 0f, 0f);
|
||||
cell.Portals.Add(new CellPortalInfo(OtherCellId: 0xFFFF, PolygonId: 0, Flags: 0, OtherPortalId: 0));
|
||||
cell.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(1, 0, 0), D = 0f, InsideSide = 0 });
|
||||
cell.PortalPolygons.Add(new[]
|
||||
{
|
||||
new Vector3(0, -1, 0), new Vector3(0, 1, 0), new Vector3(0, 1, 2), new Vector3(0, -1, 2)
|
||||
});
|
||||
return cell;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_FromBuildingExit_AddsReversePortalIntoBuilding()
|
||||
public void Build_ReturnsPortallessOutdoorRoot()
|
||||
{
|
||||
uint outdoorId = 0xA9B40031;
|
||||
var building = BuildingWithOneExit(0xA9B40170);
|
||||
var node = OutdoorCellNode.Build(outdoorId, new[] { building });
|
||||
var node = OutdoorCellNode.Build(outdoorId);
|
||||
|
||||
Assert.Equal(outdoorId, node.CellId);
|
||||
Assert.True(node.IsOutdoorNode); // the flag PortalVisibilityBuilder keys the full-screen OutsideView on
|
||||
Assert.True(node.SeenOutside);
|
||||
Assert.True(node.IsOutdoorNode); // the flag PortalVisibilityBuilder keys the full-screen OutsideView on
|
||||
Assert.Equal(Matrix4x4.Identity, node.WorldTransform);
|
||||
Assert.Single(node.Portals);
|
||||
Assert.Equal((ushort)(0xA9B40170 & 0xFFFF), node.Portals[0].OtherCellId);
|
||||
// Reversed inside-side: the building's exit was InsideSide=0, the node's is 1.
|
||||
Assert.Equal(1, node.ClipPlanes[0].InsideSide);
|
||||
// Entrance polygon moved to world space (building translated +10 X): first vert x≈10.
|
||||
Assert.Equal(10f, node.PortalPolygons[0][0].X, 3);
|
||||
Assert.Empty(node.Portals); // R-A2: no reverse portals into buildings (buildings flood per-building)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NoBuildings_ReturnsEmptyPortalNode()
|
||||
public void Build_OutdoorRoot_SeedsFullScreenOutsideView_OnlyNodeVisible()
|
||||
{
|
||||
var node = OutdoorCellNode.Build(0xA9B40031, System.Array.Empty<LoadedCell>());
|
||||
Assert.Empty(node.Portals);
|
||||
Assert.True(node.SeenOutside);
|
||||
Assert.True(node.IsOutdoorNode);
|
||||
// The load-bearing outdoor-terrain behavior: rooting at the IsOutdoorNode node seeds OutsideView
|
||||
// with the FULL-SCREEN NDC quad, so ClipFrameAssembler yields TerrainMode != Skip and
|
||||
// DrawLandscapeThroughOutsideView draws terrain/sky/scenery everywhere. The portal-less node
|
||||
// resolves to exactly {node} — no interior cells flooded from the root (those come per-building).
|
||||
var node = OutdoorCellNode.Build(0xA9B40031);
|
||||
|
||||
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.MinY, 3);
|
||||
Assert.Equal(1f, frame.OutsideView.MaxX, 3);
|
||||
Assert.Equal(1f, frame.OutsideView.MaxY, 3);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue