fix #124: interior-root building look-ins as a landscape-stage sub-pass
From inside a building, looking out at ANOTHER building with an opening showed its back walls missing (see-through to the world): per-building look-in floods only ran for outdoor roots; under an interior root the far building's interior never flooded. Decomp anchor (named-retail, this session's read): retail runs the look-in INSIDE the landscape stage for ANY root - LScape::draw is the FIRST call of PView::DrawCells' outside-view branch (pc:432719), strictly BEFORE the depth clear (pc:432732) and the exit-portal seals (pc:432785). ConstructView(CBldPortal) (0x005a59a0) clips each aperture via GetClip against the INSTALLED view - the accumulated doorway region when looked into from inside - and build_draw_portals_only pass 1 far-Z punches ALL apertures before pass 2 floods + draws any interior cell. The nested DrawCells has an empty outside view (PView ctor draw_landscape=0): no recursive landscape/clear/seal. Port: - GameWindow's per-building gather (frustum pre-gate on Building.PortalBounds) now runs for interior roots too; the root's own doorway self-excludes via the seed eye-side test (the eye is on its interior side). - PortalVisibilityBuilder.BuildFromExterior/ConstructViewBuilding gain seedRegion - the installed-view clip: interior-root look-ins seed against the OutsideView polygons (a building not visible through the doorway never floods); null = full screen (outdoor roots unchanged). - RetailPViewRenderer.DrawBuildingLookIns: a landscape-stage sub-pass (before ClearDepthForInterior + seals) - per building, punch ALL apertures (new DrawLookInPortalPunch callback, always forceFarZ=true, closing the ISSUES "forceFarZ keys on root kind, under-punches" gap), then draw the flooded cells' shells + statics far->near. Look-in frames are NEVER merged into the main frame: a merged cell would draw post-clear and z-fail against the root's seal (the old ledger portShape sketch was wrong on this point). - Look-in cells join the Prepare + partition set so shells have batches and statics route to ByCell (consumed only by the sub-pass; the main cell-object pass iterates the main flood's cells). Register: AP-33 added in the same commit - look-in statics draw WHOLE (no per-part viewcone; over-include is the safe direction) and look-in DYNAMICS are deferred (an NPC inside a far building stays invisible - retail draws objects per overlapped cell in the landscape stage). Pins: Issue124LookInSeedRegionTests on the real corner-building door - a seed region containing the aperture floods (and never more than the full-screen seed), a disjoint region floods NOTHING, and an interior-side eye never seeds its own exit portal. Suites: App 259+1skip / Core 1439+2skip / UI 420 / Net 294 green. Awaiting the user gate: far-building interiors visible through their apertures from inside; #130 re-gate (top-edge strip) rides the same launch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5135066733
commit
77cef4cd86
6 changed files with 379 additions and 32 deletions
|
|
@ -4304,28 +4304,48 @@ of which draw list the building's shell left.
|
||||||
|
|
||||||
## #124 — Looking out through an opening: far buildings with openings show missing/transparent back walls
|
## #124 — Looking out through an opening: far buildings with openings show missing/transparent back walls
|
||||||
|
|
||||||
**Status:** OPEN
|
**Status:** FIX SHIPPED — awaiting user visual gate
|
||||||
**Severity:** MEDIUM
|
**Severity:** MEDIUM
|
||||||
**Filed:** 2026-06-11 (re-gate; pre-existing — "still have that issue")
|
**Filed:** 2026-06-11 (re-gate; pre-existing — "still have that issue";
|
||||||
|
user 2026-06-12: "especially visible when I look out through a door
|
||||||
|
opening when inside a building")
|
||||||
**Component:** render — per-building look-in floods under INTERIOR roots
|
**Component:** render — per-building look-in floods under INTERIOR roots
|
||||||
|
|
||||||
From inside a building, looking out through a door/window at ANOTHER
|
From inside a building, looking out through a door/window at ANOTHER
|
||||||
building that has an opening: the far building's back walls are
|
building that has an opening: the far building's back walls are
|
||||||
missing/transparent (see the world through it). **Lead (by read):** the
|
missing/transparent. The lead confirmed by decomp: retail runs the
|
||||||
per-building look-in floods (`MergeNearbyBuildingFloods`) run ONLY for
|
look-in INSIDE the landscape stage for ANY root — `LScape::draw` is the
|
||||||
outdoor roots — `RetailPViewDrawContext.NearbyBuildingCells` is
|
FIRST call of `PView::DrawCells`' outside-view branch (pc:432719),
|
||||||
documented "Null for interior roots." So under an interior root the far
|
strictly BEFORE the depth clear (pc:432732) and the seals (pc:432785);
|
||||||
building's INTERIOR never floods: through its window you see the shell
|
`ConstructView(CBldPortal)`'s GetClip runs under the INSTALLED view
|
||||||
only, and a shell has no interior back-wall faces → transparent.
|
(the doorway region), and all apertures far-Z punch (pass 1) before any
|
||||||
Retail runs the building look-in inside `LScape::draw` (DrawBlock →
|
interior cell draws (pass 2).
|
||||||
DrawPortal → ConstructView(CBldPortal)), which executes for ANY root
|
|
||||||
whose outside view is non-empty — including interior roots looking out
|
**Fix (2026-06-12):**
|
||||||
a doorway. Fix shape: provide the nearby-building gather + per-building
|
- The per-building gather (frustum pre-gate on `Building.PortalBounds`)
|
||||||
floods for interior roots too, with look-in apertures getting PUNCH
|
now runs for interior roots too; the root's own doorway self-excludes
|
||||||
semantics (the `forceFarZ` selector currently keys on
|
via the seed eye-side test.
|
||||||
`clipRoot.IsOutdoorNode`, which under-punches this case). Needs its own
|
- `BuildFromExterior` gained `seedRegion` — the port of retail's
|
||||||
focused pass — touches the gather, the merge, and the depth-mask
|
installed-view clip: interior-root look-ins seed clipped against the
|
||||||
selector.
|
OutsideView (doorway) polygons, so a building not visible through the
|
||||||
|
doorway never floods. Outdoor roots keep the full-screen default.
|
||||||
|
- NEW `DrawBuildingLookIns` sub-pass inside the LANDSCAPE stage (before
|
||||||
|
the depth clear + seals): per building, punch ALL apertures
|
||||||
|
(`DrawLookInPortalPunch`, always far-Z), then draw the flooded cells'
|
||||||
|
shells + statics far→near. NOT merged into the main frame — a merged
|
||||||
|
cell would draw post-clear and z-fail against the root's seal.
|
||||||
|
- Look-in cells join the Prepare/partition set (shells get batches,
|
||||||
|
statics route to ByCell, consumed only by the sub-pass).
|
||||||
|
|
||||||
|
Pins: `Issue124LookInSeedRegionTests` (containing region floods ⊆
|
||||||
|
full-screen flood; disjoint region floods nothing; interior-side eye
|
||||||
|
never seeds its own exit door). Register: AP-33 (look-in statics drawn
|
||||||
|
whole — no per-part viewcone; look-in DYNAMICS deferred — an NPC inside
|
||||||
|
a far building stays invisible; both documented).
|
||||||
|
|
||||||
|
**Gate:** from inside a building, look out the door at another building
|
||||||
|
with an open door/window — its interior/back walls render through its
|
||||||
|
aperture instead of see-through to the world behind.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Documented approximation (AP) — 32 rows
|
## 3. Documented approximation (AP) — 33 rows
|
||||||
|
|
||||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
|
|
@ -128,6 +128,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
| AP-30 | AutonomousPosition diff cadence compares with epsilons (1 mm pos, 1e-4 normal, 1 mm dist); retail's `Frame::is_equal` is an exact float compare | `src/AcDream.App/Input/PlayerMovementController.cs:1541` | Sub-millimeter epsilon is well below any movement worth suppressing; comparisons are against last-SENT state so drift accumulates past the epsilon | Sub-epsilon drift suppresses an AP send retail would have made — negligible today; a consumer expecting retail's exact send-on-any-change cadence sees fewer packets | `Frame::is_equal` pc:700263 |
|
| AP-30 | AutonomousPosition diff cadence compares with epsilons (1 mm pos, 1e-4 normal, 1 mm dist); retail's `Frame::is_equal` is an exact float compare | `src/AcDream.App/Input/PlayerMovementController.cs:1541` | Sub-millimeter epsilon is well below any movement worth suppressing; comparisons are against last-SENT state so drift accumulates past the epsilon | Sub-epsilon drift suppresses an AP send retail would have made — negligible today; a consumer expecting retail's exact send-on-any-change cadence sees fewer packets | `Frame::is_equal` pc:700263 |
|
||||||
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter |
|
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter |
|
||||||
| AP-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (`ShellDrawLiftZ`, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, **#119**-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via `Build(drawLiftZ)`, seal/punch fans — **#130**) | `src/AcDream.App/Rendering/GameWindow.cs:5604` (const at `PortalVisibilityBuilder.ShellDrawLiftZ`) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) |
|
| AP-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (`ShellDrawLiftZ`, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, **#119**-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via `Build(drawLiftZ)`, seal/punch fans — **#130**) | `src/AcDream.App/Rendering/GameWindow.cs:5604` (const at `PortalVisibilityBuilder.ShellDrawLiftZ`) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) |
|
||||||
|
| AP-33 | Interior-root look-in statics (**#124** sub-pass) draw WHOLE — no per-part viewcone check; retail viewconeCheck's each part vs the installed view. Look-in DYNAMICS are not drawn at all (deferred; retail draws objects per overlapped cell in the landscape stage) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawBuildingLookIns`) | The main viewcone has no entries for look-in cells; over-include is the safe direction (z-correct, repainted outside apertures by the root's shells); look-in cell counts are small (~1-3 cells) | Statics: a few wasted draws only. Dynamics: an NPC inside a far building seen through two openings is invisible where retail shows it | `viewconeCheck` 0x0054c250; nested `DrawCells` objects pc:432878 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7628,9 +7628,9 @@ public sealed class GameWindow : IDisposable
|
||||||
// OutdoorCellNode.Build filters to exit portals internally. The clipRoot flip +
|
// OutdoorCellNode.Build filters to exit portals internally. The clipRoot flip +
|
||||||
// OutsideView terrain integration that consumes this is the next (cutover) step.
|
// OutsideView terrain integration that consumes this is the next (cutover) step.
|
||||||
_outdoorNode = null;
|
_outdoorNode = null;
|
||||||
if (viewerRoot is null && viewerCellId != 0u)
|
|
||||||
{
|
|
||||||
_outdoorNodeBuildingCells.Clear();
|
_outdoorNodeBuildingCells.Clear();
|
||||||
|
if (viewerRoot is not null || viewerCellId != 0u)
|
||||||
|
{
|
||||||
// T2 (BR-4): draw-driven flood gating. Retail floods a building's
|
// T2 (BR-4): draw-driven flood gating. Retail floods a building's
|
||||||
// interior exactly when its shell DRAWS and an aperture survives
|
// interior exactly when its shell DRAWS and an aperture survives
|
||||||
// the view (DrawBuilding Ghidra 0x0059f2a0: per-view viewconeCheck
|
// the view (DrawBuilding Ghidra 0x0059f2a0: per-view viewconeCheck
|
||||||
|
|
@ -7645,6 +7645,12 @@ public sealed class GameWindow : IDisposable
|
||||||
// Per-building iteration is also the FPS fix the 2026-06-07
|
// Per-building iteration is also the FPS fix the 2026-06-07
|
||||||
// Chebyshev hack approximated: dozens of AABB tests instead of an
|
// Chebyshev hack approximated: dozens of AABB tests instead of an
|
||||||
// O(all loaded cells) portal sweep.
|
// O(all loaded cells) portal sweep.
|
||||||
|
// #124: the gather now runs for INTERIOR roots too — retail's
|
||||||
|
// look-in executes inside LScape::draw for ANY root with a
|
||||||
|
// non-empty outside view (DrawCells pc:432719). The renderer
|
||||||
|
// routes interior-root look-ins to its landscape-stage sub-pass
|
||||||
|
// (DrawBuildingLookIns); the root's own building self-excludes
|
||||||
|
// via the seed eye-side test.
|
||||||
foreach (var registry in _buildingRegistries.Values)
|
foreach (var registry in _buildingRegistries.Values)
|
||||||
{
|
{
|
||||||
foreach (var b in registry.All())
|
foreach (var b in registry.All())
|
||||||
|
|
@ -7659,10 +7665,11 @@ public sealed class GameWindow : IDisposable
|
||||||
_outdoorNodeBuildingCells.Add(bc);
|
_outdoorNodeBuildingCells.Add(bc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (viewerRoot is null)
|
||||||
_outdoorNode = AcDream.App.Rendering.OutdoorCellNode.Build(viewerCellId);
|
_outdoorNode = AcDream.App.Rendering.OutdoorCellNode.Build(viewerCellId);
|
||||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
|
||||||
Console.WriteLine(System.FormattableString.Invariant(
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
$"[outdoor-node] cell=0x{viewerCellId:X8} nearbyCells={_outdoorNodeBuildingCells.Count} (T2 frustum-gated per-building floods)"));
|
$"[outdoor-node] cell=0x{viewerCellId:X8} root={(viewerRoot is null ? "OUT" : "IN")} nearbyCells={_outdoorNodeBuildingCells.Count} (T2 frustum-gated per-building floods)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
uint playerCellId = _physicsEngine.DataCache?.CellGraph.CurrCell?.Id ?? 0u;
|
uint playerCellId = _physicsEngine.DataCache?.CellGraph.CurrCell?.Id ?? 0u;
|
||||||
|
|
@ -7788,10 +7795,10 @@ public sealed class GameWindow : IDisposable
|
||||||
var pviewResult = _retailPViewRenderer.DrawInside(new AcDream.App.Rendering.RetailPViewDrawContext
|
var pviewResult = _retailPViewRenderer.DrawInside(new AcDream.App.Rendering.RetailPViewDrawContext
|
||||||
{
|
{
|
||||||
RootCell = clipRoot,
|
RootCell = clipRoot,
|
||||||
// R-A2: outdoor root floods each nearby building per-building (not via the root). The
|
// R-A2: outdoor root floods each nearby building per-building (not via the root).
|
||||||
// gather above populates _outdoorNodeBuildingCells only on outdoor-node frames, so it
|
// #124: interior roots get the gather too — the renderer routes them to the
|
||||||
// is fresh here exactly when clipRoot.IsOutdoorNode; null for interior roots.
|
// landscape-stage look-in sub-pass instead of the merge.
|
||||||
NearbyBuildingCells = clipRoot.IsOutdoorNode ? _outdoorNodeBuildingCells : null,
|
NearbyBuildingCells = _outdoorNodeBuildingCells,
|
||||||
ViewerEyePos = viewerEyePos,
|
ViewerEyePos = viewerEyePos,
|
||||||
ViewProjection = envCellViewProj,
|
ViewProjection = envCellViewProj,
|
||||||
CellLookup = id => _cellVisibility.TryGetCell(id, out var c) ? c : null,
|
CellLookup = id => _cellVisibility.TryGetCell(id, out var c) ? c : null,
|
||||||
|
|
@ -7837,6 +7844,11 @@ public sealed class GameWindow : IDisposable
|
||||||
DrawExitPortalMasks = sliceCtx =>
|
DrawExitPortalMasks = sliceCtx =>
|
||||||
DrawRetailPViewPortalDepthWrite(sliceCtx, envCellViewProj,
|
DrawRetailPViewPortalDepthWrite(sliceCtx, envCellViewProj,
|
||||||
forceFarZ: clipRoot.IsOutdoorNode),
|
forceFarZ: clipRoot.IsOutdoorNode),
|
||||||
|
// #124: look-in apertures are ALWAYS the punch (retail
|
||||||
|
// maxZ1), independent of the root-keyed selector above.
|
||||||
|
DrawLookInPortalPunch = sliceCtx =>
|
||||||
|
DrawRetailPViewPortalDepthWrite(sliceCtx, envCellViewProj,
|
||||||
|
forceFarZ: true),
|
||||||
DrawCellParticles = sliceCtx =>
|
DrawCellParticles = sliceCtx =>
|
||||||
DrawRetailPViewCellParticles(sliceCtx, camera, camPos),
|
DrawRetailPViewCellParticles(sliceCtx, camera, camPos),
|
||||||
DrawDynamicsParticles = survivors =>
|
DrawDynamicsParticles = survivors =>
|
||||||
|
|
|
||||||
|
|
@ -480,12 +480,18 @@ public static class PortalVisibilityBuilder
|
||||||
/// camera cell. It keeps the same retail distance-priority traversal and
|
/// camera cell. It keeps the same retail distance-priority traversal and
|
||||||
/// neighbour reciprocal clipping once inside the building.
|
/// neighbour reciprocal clipping once inside the building.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="seedRegion">Optional NDC region the seed apertures clip against —
|
||||||
|
/// retail's GetClip runs under the CURRENTLY INSTALLED view (PView::GetClip
|
||||||
|
/// 0x005a4320): full screen when the viewer is outdoors, the accumulated
|
||||||
|
/// outside (doorway) view when a building is looked into from an interior
|
||||||
|
/// root (#124). Null = full screen (the outdoor-root behavior).</param>
|
||||||
public static PortalVisibilityFrame BuildFromExterior(
|
public static PortalVisibilityFrame BuildFromExterior(
|
||||||
IEnumerable<LoadedCell> candidateCells,
|
IEnumerable<LoadedCell> candidateCells,
|
||||||
Vector3 cameraPos,
|
Vector3 cameraPos,
|
||||||
Func<uint, LoadedCell?> lookup,
|
Func<uint, LoadedCell?> lookup,
|
||||||
Matrix4x4 viewProj,
|
Matrix4x4 viewProj,
|
||||||
float maxSeedDistance = float.PositiveInfinity)
|
float maxSeedDistance = float.PositiveInfinity,
|
||||||
|
IReadOnlyList<ViewPolygon>? seedRegion = null)
|
||||||
{
|
{
|
||||||
var frame = new PortalVisibilityFrame();
|
var frame = new PortalVisibilityFrame();
|
||||||
var todo = new CellTodoList();
|
var todo = new CellTodoList();
|
||||||
|
|
@ -532,7 +538,7 @@ public static class PortalVisibilityBuilder
|
||||||
poly,
|
poly,
|
||||||
cell.WorldTransform,
|
cell.WorldTransform,
|
||||||
viewProj,
|
viewProj,
|
||||||
FullScreenRegion,
|
seedRegion ?? FullScreenRegion,
|
||||||
out _);
|
out _);
|
||||||
|
|
||||||
// T2 (BR-4): empty clip = no seed, no exceptions (retail's
|
// T2 (BR-4): empty clip = no seed, no exceptions (retail's
|
||||||
|
|
@ -662,8 +668,9 @@ public static class PortalVisibilityBuilder
|
||||||
Vector3 cameraPos,
|
Vector3 cameraPos,
|
||||||
Func<uint, LoadedCell?> lookup,
|
Func<uint, LoadedCell?> lookup,
|
||||||
Matrix4x4 viewProj,
|
Matrix4x4 viewProj,
|
||||||
float maxSeedDistance = float.PositiveInfinity)
|
float maxSeedDistance = float.PositiveInfinity,
|
||||||
=> BuildFromExterior(buildingCells, cameraPos, lookup, viewProj, maxSeedDistance);
|
IReadOnlyList<ViewPolygon>? seedRegion = null)
|
||||||
|
=> BuildFromExterior(buildingCells, cameraPos, lookup, viewProj, maxSeedDistance, seedRegion);
|
||||||
|
|
||||||
// The NDC [-1,1] viewport quad (CCW), reused by the flap probe's clip recompute.
|
// The NDC [-1,1] viewport quad (CCW), reused by the flap probe's clip recompute.
|
||||||
private static readonly Vector2[] FullScreenQuad =
|
private static readonly Vector2[] FullScreenQuad =
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,12 @@ public sealed class RetailPViewRenderer
|
||||||
// R-A2: per-building flood grouping, reused across frames (inner lists cleared each frame).
|
// R-A2: per-building flood grouping, reused across frames (inner lists cleared each frame).
|
||||||
private readonly Dictionary<uint, List<LoadedCell>> _buildingGroups = new();
|
private readonly Dictionary<uint, List<LoadedCell>> _buildingGroups = new();
|
||||||
|
|
||||||
|
// #124: per-building look-in frames under an INTERIOR root, drawn as a
|
||||||
|
// landscape-stage sub-pass (DrawBuildingLookIns) — never merged into the
|
||||||
|
// main frame (see DrawInside). Rebuilt each interior-root frame.
|
||||||
|
private readonly List<PortalVisibilityFrame> _lookInFrames = new();
|
||||||
|
private readonly HashSet<uint> _lookInPrepareScratch = new();
|
||||||
|
|
||||||
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
||||||
// (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
|
// (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
|
||||||
// GetClip + GetVisible only). The old 48 m seed cap is replaced by the
|
// GetClip + GetVisible only). The old 48 m seed cap is replaced by the
|
||||||
|
|
@ -67,6 +73,26 @@ public sealed class RetailPViewRenderer
|
||||||
if (ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null)
|
if (ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null)
|
||||||
MergeNearbyBuildingFloods(ctx, pvFrame);
|
MergeNearbyBuildingFloods(ctx, pvFrame);
|
||||||
|
|
||||||
|
// #124: interior-root building look-ins. Retail runs the look-in INSIDE
|
||||||
|
// the landscape stage for ANY root — LScape::draw is the FIRST call of
|
||||||
|
// DrawCells' outside-view branch (pc:432719), strictly BEFORE the depth
|
||||||
|
// clear (pc:432732) and the exit-portal seals (pc:432785); a far
|
||||||
|
// building seen through our doorway floods clipped to the INSTALLED
|
||||||
|
// outside view (GetClip vs current view, ConstructView(CBldPortal)
|
||||||
|
// 0x005a59a0). These frames therefore draw in DrawBuildingLookIns
|
||||||
|
// (inside the landscape stage), NEVER merged into the main frame — a
|
||||||
|
// merged cell would draw post-clear and z-fail against the root's seal
|
||||||
|
// (its geometry is beyond the door plane). The eye-side seed test
|
||||||
|
// self-excludes the root's own building (the eye is on its interior
|
||||||
|
// side). Outdoor roots keep the MergeNearbyBuildingFloods path above
|
||||||
|
// (no depth clear under outdoor roots — the merged form is equivalent
|
||||||
|
// there).
|
||||||
|
_lookInFrames.Clear();
|
||||||
|
if (!ctx.RootCell.IsOutdoorNode
|
||||||
|
&& ctx.NearbyBuildingCells is not null
|
||||||
|
&& pvFrame.OutsideView.Polygons.Count > 0)
|
||||||
|
BuildInteriorRootLookIns(ctx, pvFrame);
|
||||||
|
|
||||||
var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame);
|
var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame);
|
||||||
UploadClipFrame(ctx.SetTerrainClipUbo);
|
UploadClipFrame(ctx.SetTerrainClipUbo);
|
||||||
|
|
||||||
|
|
@ -78,15 +104,31 @@ public sealed class RetailPViewRenderer
|
||||||
var drawableCells = new HashSet<uint>(pvFrame.OrderedVisibleCells);
|
var drawableCells = new HashSet<uint>(pvFrame.OrderedVisibleCells);
|
||||||
UseIndoorMembershipOnlyRouting();
|
UseIndoorMembershipOnlyRouting();
|
||||||
|
|
||||||
|
// #124: look-in cells need prepared shell batches + their statics routed
|
||||||
|
// into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main
|
||||||
|
// cell-object pass iterates pvFrame.OrderedVisibleCells, which never
|
||||||
|
// contains them). drawableCells itself stays the MAIN flood: it feeds the
|
||||||
|
// seals, the outside-stage predicate, and the frame result.
|
||||||
|
var prepareCells = drawableCells;
|
||||||
|
if (_lookInFrames.Count > 0)
|
||||||
|
{
|
||||||
|
_lookInPrepareScratch.Clear();
|
||||||
|
_lookInPrepareScratch.UnionWith(drawableCells);
|
||||||
|
foreach (var f in _lookInFrames)
|
||||||
|
foreach (uint c in f.OrderedVisibleCells)
|
||||||
|
_lookInPrepareScratch.Add(c);
|
||||||
|
prepareCells = _lookInPrepareScratch;
|
||||||
|
}
|
||||||
|
|
||||||
_envCells.PrepareRenderBatches(
|
_envCells.PrepareRenderBatches(
|
||||||
ctx.ViewProjection,
|
ctx.ViewProjection,
|
||||||
ctx.CameraWorldPosition,
|
ctx.CameraWorldPosition,
|
||||||
filter: drawableCells,
|
filter: prepareCells,
|
||||||
centerLbX: ctx.RenderCenterLbX,
|
centerLbX: ctx.RenderCenterLbX,
|
||||||
centerLbY: ctx.RenderCenterLbY,
|
centerLbY: ctx.RenderCenterLbY,
|
||||||
renderRadius: ctx.RenderRadius);
|
renderRadius: ctx.RenderRadius);
|
||||||
|
|
||||||
var partition = InteriorEntityPartition.Partition(drawableCells, ctx.LandblockEntries);
|
var partition = InteriorEntityPartition.Partition(prepareCells, ctx.LandblockEntries);
|
||||||
var result = new RetailPViewFrameResult
|
var result = new RetailPViewFrameResult
|
||||||
{
|
{
|
||||||
PortalFrame = pvFrame,
|
PortalFrame = pvFrame,
|
||||||
|
|
@ -215,6 +257,105 @@ public sealed class RetailPViewRenderer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #124: per-building look-in floods for an INTERIOR root, seeded clipped
|
||||||
|
// against the OutsideView (retail: GetClip runs under the INSTALLED view —
|
||||||
|
// the accumulated doorway region — so a far building floods only within the
|
||||||
|
// doorway, ConstructView(CBldPortal) 0x005a59a0 via PView::GetClip
|
||||||
|
// 0x005a4320). Same grouping as MergeNearbyBuildingFloods; the root's own
|
||||||
|
// building self-excludes via the seed eye-side test.
|
||||||
|
private void BuildInteriorRootLookIns(RetailPViewDrawContext ctx, PortalVisibilityFrame pvFrame)
|
||||||
|
{
|
||||||
|
foreach (var group in _buildingGroups.Values)
|
||||||
|
group.Clear();
|
||||||
|
|
||||||
|
foreach (var cell in ctx.NearbyBuildingCells!)
|
||||||
|
{
|
||||||
|
uint groupKey = cell.BuildingId ?? cell.CellId;
|
||||||
|
if (!_buildingGroups.TryGetValue(groupKey, out var group))
|
||||||
|
{
|
||||||
|
group = new List<LoadedCell>();
|
||||||
|
_buildingGroups[groupKey] = group;
|
||||||
|
}
|
||||||
|
group.Add(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in _buildingGroups.Values)
|
||||||
|
{
|
||||||
|
if (group.Count == 0)
|
||||||
|
continue;
|
||||||
|
var frame = PortalVisibilityBuilder.ConstructViewBuilding(
|
||||||
|
group, ctx.ViewerEyePos, ctx.CellLookup, ctx.ViewProjection,
|
||||||
|
OutdoorBuildingSeedDistance, pvFrame.OutsideView.Polygons);
|
||||||
|
if (frame.OrderedVisibleCells.Count > 0)
|
||||||
|
_lookInFrames.Add(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #124: draw the interior-root look-ins INSIDE the landscape stage —
|
||||||
|
// retail's placement (LScape::draw → DrawBlock → DrawSortCell →
|
||||||
|
// DrawBuilding runs as the FIRST call of DrawCells' outside-view branch,
|
||||||
|
// pc:432719, before the depth clear + seals). Per building: punch ALL
|
||||||
|
// apertures first (retail finishes build_draw_portals_only pass 1 — the
|
||||||
|
// far-Z maxZ1 punch — across the whole building BSP before pass 2 floods),
|
||||||
|
// then draw the flooded cells' shells + statics far→near (the nested
|
||||||
|
// DrawCells' DrawEnvCell + DrawObjCellForDummies; its outside_view is
|
||||||
|
// empty by construction — PView ctor draw_landscape=0 — so no recursive
|
||||||
|
// landscape/clear/seal). Anything rasterized outside an aperture is
|
||||||
|
// repainted by the root's own shells after the depth clear, so over-draw
|
||||||
|
// here is color-safe; statics draw whole (the main viewcone has no entry
|
||||||
|
// for look-in cells; over-include is the safe direction).
|
||||||
|
private void DrawBuildingLookIns(RetailPViewDrawContext ctx, InteriorEntityPartition.Result partition)
|
||||||
|
{
|
||||||
|
if (_lookInFrames.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var frame in _lookInFrames)
|
||||||
|
{
|
||||||
|
// Pass 1: far-Z punch every aperture of this building.
|
||||||
|
if (ctx.DrawLookInPortalPunch is not null)
|
||||||
|
{
|
||||||
|
foreach (uint cellId in frame.OrderedVisibleCells)
|
||||||
|
{
|
||||||
|
if (!frame.CellViews.TryGetValue(cellId, out var view))
|
||||||
|
continue;
|
||||||
|
foreach (var poly in view.Polygons)
|
||||||
|
{
|
||||||
|
var single = new CellView();
|
||||||
|
single.Add(poly);
|
||||||
|
var cps = ClipPlaneSet.From(single);
|
||||||
|
if (cps.IsNothingVisible)
|
||||||
|
continue;
|
||||||
|
var planes = new Vector4[cps.Count];
|
||||||
|
for (int p = 0; p < cps.Count; p++)
|
||||||
|
planes[p] = cps.Planes[p];
|
||||||
|
ctx.DrawLookInPortalPunch(new RetailPViewCellSliceContext(
|
||||||
|
cellId,
|
||||||
|
new ClipViewSlice(0, new Vector4(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY), planes),
|
||||||
|
Array.Empty<WorldEntity>()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2: shells + statics, far→near.
|
||||||
|
UseIndoorMembershipOnlyRouting();
|
||||||
|
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
uint cellId = frame.OrderedVisibleCells[i];
|
||||||
|
_oneCell.Clear();
|
||||||
|
_oneCell.Add(cellId);
|
||||||
|
_envCells.Render(WbRenderPass.Opaque, _oneCell);
|
||||||
|
_envCells.Render(WbRenderPass.Transparent, _oneCell);
|
||||||
|
|
||||||
|
if (partition.ByCell.TryGetValue(cellId, out var bucket) && bucket.Count > 0)
|
||||||
|
{
|
||||||
|
_cellStaticScratch.Clear();
|
||||||
|
_cellStaticScratch.AddRange(bucket);
|
||||||
|
DrawEntityBucket(ctx, _cellStaticScratch, _oneCell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawLandscapeThroughOutsideView(
|
private void DrawLandscapeThroughOutsideView(
|
||||||
RetailPViewDrawContext ctx,
|
RetailPViewDrawContext ctx,
|
||||||
ClipFrameAssembly clipAssembly,
|
ClipFrameAssembly clipAssembly,
|
||||||
|
|
@ -260,6 +401,12 @@ public sealed class RetailPViewRenderer
|
||||||
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, _outdoorStaticScratch));
|
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, _outdoorStaticScratch));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #124: far-building look-ins draw HERE — still inside the landscape
|
||||||
|
// stage (their punches mark against the terrain/exterior depth just
|
||||||
|
// drawn), strictly BEFORE the depth clear + seals below, matching
|
||||||
|
// retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785).
|
||||||
|
DrawBuildingLookIns(ctx, partition);
|
||||||
|
|
||||||
// T1: retail clears the FULL depth buffer ONCE between the outside
|
// T1: retail clears the FULL depth buffer ONCE between the outside
|
||||||
// stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
|
// stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
|
||||||
// Clear gated on portalsDrawnCount; exact gate semantics is a plan
|
// Clear gated on portalsDrawnCount; exact gate semantics is a plan
|
||||||
|
|
@ -667,6 +814,12 @@ public interface IRetailPViewCellDrawCallbacks
|
||||||
{
|
{
|
||||||
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; }
|
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; }
|
||||||
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; }
|
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; }
|
||||||
|
|
||||||
|
/// <summary>#124: far-Z punch one look-in aperture (a clipped view polygon
|
||||||
|
/// of a looked-into building cell) — always the PUNCH variant regardless
|
||||||
|
/// of root kind (retail maxZ1; the root-keyed forceFarZ selector only
|
||||||
|
/// governs the MAIN frame's exit-portal masks).</summary>
|
||||||
|
public Action<RetailPViewCellSliceContext>? DrawLookInPortalPunch { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IRetailPViewCellDrawContext : IRetailPViewCellDrawCallbacks
|
public interface IRetailPViewCellDrawContext : IRetailPViewCellDrawCallbacks
|
||||||
|
|
@ -713,6 +866,7 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
||||||
public Action? ClearDepthForInterior { get; init; }
|
public Action? ClearDepthForInterior { get; init; }
|
||||||
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; init; }
|
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; init; }
|
||||||
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; init; }
|
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; init; }
|
||||||
|
public Action<RetailPViewCellSliceContext>? DrawLookInPortalPunch { get; init; }
|
||||||
public Action<IReadOnlyList<WorldEntity>>? DrawDynamicsParticles { get; init; }
|
public Action<IReadOnlyList<WorldEntity>>? DrawDynamicsParticles { get; init; }
|
||||||
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.Options;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #124 — far-building interiors under an INTERIOR root. Retail seeds the
|
||||||
|
/// look-in flood by clipping a building's aperture against the CURRENTLY
|
||||||
|
/// INSTALLED view (PView::GetClip 0x005a4320 inside ConstructView(CBldPortal)
|
||||||
|
/// 0x005a59a0): full screen outdoors, the accumulated doorway (outside) view
|
||||||
|
/// when looked into from inside. These tests pin BuildFromExterior's
|
||||||
|
/// seedRegion parameter — the port of that installed-view clip — against the
|
||||||
|
/// real Holtburg corner-building door.
|
||||||
|
/// </summary>
|
||||||
|
public class Issue124LookInSeedRegionTests
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _out;
|
||||||
|
public Issue124LookInSeedRegionTests(ITestOutputHelper output) => _out = output;
|
||||||
|
|
||||||
|
private const uint ExitCellId = CornerFloodReplayTests.Landblock | 0x0170u;
|
||||||
|
|
||||||
|
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
|
||||||
|
{
|
||||||
|
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
|
||||||
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 1f, 5000f);
|
||||||
|
return view * proj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Dictionary<uint, LoadedCell> cells, LoadedCell exitCell, int exitIdx,
|
||||||
|
Vector3 centroid, Vector3 outward) LoadFixture(DatCollection dats)
|
||||||
|
{
|
||||||
|
var cells = CornerFloodReplayTests.LoadBuilding(dats);
|
||||||
|
var exitCell = cells[ExitCellId];
|
||||||
|
|
||||||
|
int exitIdx = -1;
|
||||||
|
for (int i = 0; i < exitCell.Portals.Count; i++)
|
||||||
|
{
|
||||||
|
if (exitCell.Portals[i].OtherCellId == 0xFFFF && i < exitCell.PortalPolygons.Count
|
||||||
|
&& exitCell.PortalPolygons[i].Length >= 3)
|
||||||
|
{ exitIdx = i; break; }
|
||||||
|
}
|
||||||
|
Assert.True(exitIdx >= 0);
|
||||||
|
|
||||||
|
var localPoly = exitCell.PortalPolygons[exitIdx];
|
||||||
|
Vector3 centroid = Vector3.Zero;
|
||||||
|
foreach (var lp in localPoly)
|
||||||
|
centroid += Vector3.Transform(lp, exitCell.WorldTransform);
|
||||||
|
centroid /= localPoly.Length;
|
||||||
|
|
||||||
|
var plane = exitCell.ClipPlanes[exitIdx];
|
||||||
|
var normal = Vector3.TransformNormal(plane.Normal, exitCell.WorldTransform);
|
||||||
|
var cellCenter = Vector3.Transform(
|
||||||
|
(exitCell.LocalBoundsMin + exitCell.LocalBoundsMax) * 0.5f, exitCell.WorldTransform);
|
||||||
|
// outward = away from the cell interior.
|
||||||
|
if (Vector3.Dot(normal, cellCenter - centroid) > 0)
|
||||||
|
normal = -normal;
|
||||||
|
return (cells, exitCell, exitIdx, centroid, Vector3.Normalize(normal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector4 ApertureNdcAabb(LoadedCell cell, int idx, Matrix4x4 viewProj)
|
||||||
|
{
|
||||||
|
float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue;
|
||||||
|
foreach (var lp in cell.PortalPolygons[idx])
|
||||||
|
{
|
||||||
|
var w = Vector3.Transform(lp, cell.WorldTransform);
|
||||||
|
var c = Vector4.Transform(new Vector4(w, 1f), viewProj);
|
||||||
|
Assert.True(c.W > 0.05f, "fixture eye must keep the aperture fully in front");
|
||||||
|
minX = MathF.Min(minX, c.X / c.W); maxX = MathF.Max(maxX, c.X / c.W);
|
||||||
|
minY = MathF.Min(minY, c.Y / c.W); maxY = MathF.Max(maxY, c.Y / c.W);
|
||||||
|
}
|
||||||
|
return new Vector4(minX, minY, maxX, maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ViewPolygon Quad(float minX, float minY, float maxX, float maxY) =>
|
||||||
|
new(new[]
|
||||||
|
{
|
||||||
|
new Vector2(minX, minY), new Vector2(maxX, minY),
|
||||||
|
new Vector2(maxX, maxY), new Vector2(minX, maxY),
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeedRegion_ContainingAperture_Floods_DisjointRegion_DoesNot()
|
||||||
|
{
|
||||||
|
var datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||||
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
var (cells, exitCell, exitIdx, centroid, outward) = LoadFixture(dats);
|
||||||
|
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out var c) ? c : null;
|
||||||
|
|
||||||
|
// Eye OUTSIDE the building, 3 m in front of the exit door, gaze at it
|
||||||
|
// — the look-in geometry of a viewer peering at this building through
|
||||||
|
// some other opening.
|
||||||
|
var eye = centroid + outward * 3f;
|
||||||
|
var viewProj = ViewProjFor(eye, centroid);
|
||||||
|
var ap = ApertureNdcAabb(exitCell, exitIdx, viewProj);
|
||||||
|
_out.WriteLine(FormattableString.Invariant(
|
||||||
|
$"aperture ndc=({ap.X:F3},{ap.Y:F3},{ap.Z:F3},{ap.W:F3})"));
|
||||||
|
|
||||||
|
// Sanity: the full-screen (outdoor-root) seed floods.
|
||||||
|
var full = PortalVisibilityBuilder.BuildFromExterior(
|
||||||
|
cells.Values, eye, Lookup, viewProj);
|
||||||
|
Assert.True(full.OrderedVisibleCells.Count > 0, "full-screen seed must flood");
|
||||||
|
|
||||||
|
// A region containing the aperture floods — and never MORE than the
|
||||||
|
// full-screen seed (region-restricting can only shrink the flood).
|
||||||
|
var containing = new[] { Quad(ap.X - 0.05f, ap.Y - 0.05f, ap.Z + 0.05f, ap.W + 0.05f) };
|
||||||
|
var seeded = PortalVisibilityBuilder.BuildFromExterior(
|
||||||
|
cells.Values, eye, Lookup, viewProj, float.PositiveInfinity, containing);
|
||||||
|
Assert.True(seeded.OrderedVisibleCells.Count > 0, "containing region must flood");
|
||||||
|
Assert.True(seeded.OrderedVisibleCells.Count <= full.OrderedVisibleCells.Count);
|
||||||
|
|
||||||
|
// A region strictly disjoint from the aperture must not flood — the
|
||||||
|
// doorway doesn't show this building, so its interior never builds
|
||||||
|
// (retail: GetClip vs the installed view returns empty → no look-in).
|
||||||
|
Assert.True(ap.Z < 0.70f || ap.X > -0.70f, "fixture aperture unexpectedly fills the screen");
|
||||||
|
var disjoint = ap.Z < 0.70f
|
||||||
|
? new[] { Quad(0.75f, 0.75f, 0.99f, 0.99f) }
|
||||||
|
: new[] { Quad(-0.99f, -0.99f, -0.75f, -0.75f) };
|
||||||
|
var none = PortalVisibilityBuilder.BuildFromExterior(
|
||||||
|
cells.Values, eye, Lookup, viewProj, float.PositiveInfinity, disjoint);
|
||||||
|
Assert.True(none.OrderedVisibleCells.Count == 0,
|
||||||
|
FormattableString.Invariant($"disjoint region flooded {none.OrderedVisibleCells.Count} cells"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EyeOnInteriorSide_ExitDoorNeverSeeds()
|
||||||
|
{
|
||||||
|
// The root's own doorway must not look-in on itself: the seed eye-side
|
||||||
|
// test (retail ConstructView's sidedness vs portal_side) excludes any
|
||||||
|
// aperture the eye is on the interior side of — this is what lets the
|
||||||
|
// interior-root gather pass ALL nearby buildings including the
|
||||||
|
// viewer's own without special-casing.
|
||||||
|
var datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||||
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
var (cells, exitCell, _, centroid, outward) = LoadFixture(dats);
|
||||||
|
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out var c) ? c : null;
|
||||||
|
|
||||||
|
var eye = centroid - outward * 2f; // 2 m INSIDE the doorway
|
||||||
|
var viewProj = ViewProjFor(eye, centroid);
|
||||||
|
|
||||||
|
var frame = PortalVisibilityBuilder.BuildFromExterior(
|
||||||
|
new[] { exitCell }, eye, Lookup, viewProj);
|
||||||
|
Assert.True(frame.OrderedVisibleCells.Count == 0,
|
||||||
|
"an interior-side eye must not seed its own cell's exit portal");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue