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:
Erik 2026-06-12 15:59:29 +02:00
parent 5135066733
commit 77cef4cd86
6 changed files with 379 additions and 32 deletions

View file

@ -480,12 +480,18 @@ public static class PortalVisibilityBuilder
/// camera cell. It keeps the same retail distance-priority traversal and
/// neighbour reciprocal clipping once inside the building.
/// </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(
IEnumerable<LoadedCell> candidateCells,
Vector3 cameraPos,
Func<uint, LoadedCell?> lookup,
Matrix4x4 viewProj,
float maxSeedDistance = float.PositiveInfinity)
float maxSeedDistance = float.PositiveInfinity,
IReadOnlyList<ViewPolygon>? seedRegion = null)
{
var frame = new PortalVisibilityFrame();
var todo = new CellTodoList();
@ -532,7 +538,7 @@ public static class PortalVisibilityBuilder
poly,
cell.WorldTransform,
viewProj,
FullScreenRegion,
seedRegion ?? FullScreenRegion,
out _);
// T2 (BR-4): empty clip = no seed, no exceptions (retail's
@ -662,8 +668,9 @@ public static class PortalVisibilityBuilder
Vector3 cameraPos,
Func<uint, LoadedCell?> lookup,
Matrix4x4 viewProj,
float maxSeedDistance = float.PositiveInfinity)
=> BuildFromExterior(buildingCells, cameraPos, lookup, viewProj, maxSeedDistance);
float maxSeedDistance = float.PositiveInfinity,
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.
private static readonly Vector2[] FullScreenQuad =