feat(render): Phase A8 — populate LoadedCell.PortalPolygons

BuildLoadedCell now reads the full portal polygon vertices from
cellStruct.Polygons[portal.PolygonId].VertexIds and stores them in
local-space on the LoadedCell. Empty arrays for unresolved polygons.
Same source as the ClipPlane block; no new dat read.

Unit test covers the data-class invariant (parallel indexing) since
the full integration is exercised only at runtime with live dat data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-26 07:43:24 +02:00
parent fee878f292
commit d834188a4e
2 changed files with 85 additions and 0 deletions

View file

@ -5539,6 +5539,7 @@ public sealed class GameWindow : IDisposable
// Build portal list and clip planes from CellPortals.
var portals = new List<CellPortalInfo>();
var clipPlanes = new List<PortalClipPlane>();
var portalPolygons = new List<System.Numerics.Vector3[]>();
// Compute cell centroid in local space for InsideSide determination.
var centroid = (boundsMin + boundsMax) * 0.5f;
@ -5596,6 +5597,34 @@ public sealed class GameWindow : IDisposable
{
clipPlanes.Add(default);
}
// Phase A8: capture the full polygon vertices in cell-local space
// for the indoor stencil pipeline. Reads the same source as the
// ClipPlane block above (polygon.VertexIds → cellStruct vertices).
System.Numerics.Vector3[] polyVerts = System.Array.Empty<System.Numerics.Vector3>();
if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var portalPoly)
&& portalPoly.VertexIds.Count >= 3)
{
polyVerts = new System.Numerics.Vector3[portalPoly.VertexIds.Count];
bool allResolved = true;
for (int vi = 0; vi < portalPoly.VertexIds.Count; vi++)
{
if (cellStruct.VertexArray.Vertices.TryGetValue(
(ushort)portalPoly.VertexIds[vi], out var pv))
{
polyVerts[vi] = new System.Numerics.Vector3(
pv.Origin.X, pv.Origin.Y, pv.Origin.Z);
}
else
{
allResolved = false;
break;
}
}
if (!allResolved)
polyVerts = System.Array.Empty<System.Numerics.Vector3>();
}
portalPolygons.Add(polyVerts);
}
var loaded = new LoadedCell
@ -5608,6 +5637,7 @@ public sealed class GameWindow : IDisposable
LocalBoundsMax = boundsMax,
Portals = portals,
ClipPlanes = clipPlanes,
PortalPolygons = portalPolygons, // Phase A8
};
_pendingCells.Add(loaded);
}

View file

@ -0,0 +1,55 @@
// CellVisibilityPortalPolygonsTests.cs — verifies BuildLoadedCell preserves
// portal polygon vertices in LoadedCell.PortalPolygons.
//
// Phase A8 — Indoor-cell visibility culling. Issue #78.
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public class CellVisibilityPortalPolygonsTests
{
[Fact]
public void LoadedCell_DefaultPortalPolygons_IsEmpty()
{
var cell = new LoadedCell();
Assert.NotNull(cell.PortalPolygons);
Assert.Empty(cell.PortalPolygons);
}
[Fact]
public void LoadedCell_PortalPolygons_ParallelIndexedToPortals()
{
// Hand-construct: two portals, one with a 4-vertex polygon, one with
// an empty polygon (resolution failure simulated by an empty array).
// The data-class semantics: PortalPolygons[i] corresponds to
// Portals[i] and ClipPlanes[i].
var cell = new LoadedCell
{
Portals = new()
{
new CellPortalInfo(0xFFFF, 100, 0), // exit portal, has geometry
new CellPortalInfo(0x0102, 101, 0), // inner portal, no geometry resolved
},
ClipPlanes = new() { default, default },
PortalPolygons = new()
{
new[]
{
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(1, 1, 0),
new Vector3(0, 1, 0),
},
System.Array.Empty<Vector3>(),
},
};
Assert.Equal(2, cell.Portals.Count);
Assert.Equal(2, cell.PortalPolygons.Count);
Assert.Equal(4, cell.PortalPolygons[0].Length);
Assert.Empty(cell.PortalPolygons[1]);
}
}