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
|
|
@ -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