From 0030dacaaaa5144a0ce2e87f48460ffe7db2be31 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 7 Jun 2026 21:08:43 +0200 Subject: [PATCH] fix(render): outdoor look-in draws interior cells only through real doorway apertures (no see-through walls) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cutover-flip follow-up: see-through buildings from outside. When the outdoor-node flood reaches a building, each interior cell is meant to draw clipped to its doorway aperture. But DrawEnvCellShells falls back to the no-clip slot 0 (full-screen) when a cell's aperture degenerates — screen-covering when you get close, or edge-on. Indoors that fallback is load-bearing (it seals the room the camera stands in; near walls hide the over-draw). From OUTSIDE it paints the building interior across the whole screen, depth-tested, so it shows wherever the solid exterior does not cover — the see-through walls, appearing 'past a threshold' exactly where the aperture degenerates. Fix: for the outdoor-node root only, skip a flooded interior cell with no real plane-clip slot (HasRealClipSlot). From outside, 'no real aperture' means 'do not paint this interior', not 'paint it everywhere'. Interior roots keep the seal-everything slot-0 fallback unchanged. Applied to DrawEnvCellShells AND DrawCellObjectLists so a skipped cell shows neither walls nor furniture; the dead DrawPortal exterior look-in gets the same gate. Root cause traced over the WB EnvCell render path: CellMesh.cs is physics-only; ObjectMeshManager.PrepareCellStructMeshData builds double-sided walls, so this was never a culling bug. App 216/0, build green. Visual gate pending. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Rendering/RetailPViewRenderer.cs | 56 ++++++++++++++++--- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 024ec3d9..e7c0e0a2 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -76,11 +76,18 @@ public sealed class RetailPViewRenderer ctx.EmitDiagnostics?.Invoke(result); + // Render unification: from the OUTDOOR-node root a flooded interior cell must draw ONLY within + // its real doorway aperture (a plane-clip slot). A cell whose aperture degenerates to the + // no-clip slot 0 (screen-covering / edge-on) would otherwise draw FULL-SCREEN and paint the + // building interior across the whole screen — the see-through-walls bug. Indoor roots keep the + // seal-everything slot-0 fallback (load-bearing: it seals the room the camera stands in). + bool outdoorRoot = ctx.RootCell.IsOutdoorNode; + DrawLandscapeThroughOutsideView(ctx, clipAssembly, partition); UseIndoorMembershipOnlyRouting(); DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells); - DrawEnvCellShells(ctx, pvFrame, clipAssembly, drawableCells); - DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition); + DrawEnvCellShells(ctx, pvFrame, clipAssembly, drawableCells, outdoorRoot); + DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition, outdoorRoot); return result; } @@ -128,8 +135,11 @@ public sealed class RetailPViewRenderer ctx.EmitDiagnostics?.Invoke(result); DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells); - DrawEnvCellShells(ctx, pvFrame, clipAssembly, drawableCells); - DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition); + // DrawPortal is the exterior look-in (camera outside, peering in) → same outdoor gate: an + // interior cell with no real doorway aperture must not draw full-screen. (This path is dead + // after the cutover flip; kept compiling until the Step-D deletion.) + DrawEnvCellShells(ctx, pvFrame, clipAssembly, drawableCells, outdoorRoot: true); + DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition, outdoorRoot: true); RestoreNoClip(ctx.SetTerrainClipUbo); return result; @@ -181,15 +191,19 @@ public sealed class RetailPViewRenderer IRetailPViewCellDrawCallbacks ctx, PortalVisibilityFrame pvFrame, ClipFrameAssembly clipAssembly, - HashSet drawableCells) // param kept this task; removed in Task 4 + HashSet drawableCells, // param kept this task; removed in Task 4 + bool outdoorRoot) { // Retail DrawCells Loop 2: every visible cell's shell, reverse cell_draw_list - // (far→near), per portal_view slice. No drawableCells filter — a cell without a - // clip-slot falls through GetCellSlicesOrNoClip to NoClipSlice and draws unclipped - // (sealed; per-slice trim returns in Task 4). + // (far→near), per portal_view slice. Indoors a cell without a clip-slot falls through + // GetCellSlicesOrNoClip to NoClipSlice and draws unclipped (sealed — load-bearing R1 seal). + // Outdoors (outdoor-node root) that same unclipped draw IS the see-through bug, so a cell with + // no real plane-clip aperture is skipped entirely (see DrawInside). foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame)) { uint cellId = entry.CellId; + if (outdoorRoot && !HasRealClipSlot(clipAssembly, cellId)) + continue; _oneCell.Clear(); _oneCell.Add(cellId); @@ -207,7 +221,8 @@ public sealed class RetailPViewRenderer PortalVisibilityFrame pvFrame, ClipFrameAssembly clipAssembly, HashSet drawableCells, - InteriorEntityPartition.Result partition) + InteriorEntityPartition.Result partition, + bool outdoorRoot) { for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--) { @@ -215,6 +230,11 @@ public sealed class RetailPViewRenderer if (!drawableCells.Contains(cellId)) continue; + // Outdoor-node root: skip a cell with no real doorway aperture (would draw full-screen) — + // matches DrawEnvCellShells so a skipped interior cell shows neither walls nor furniture. + if (outdoorRoot && !HasRealClipSlot(clipAssembly, cellId)) + continue; + if (!partition.ByCell.TryGetValue(cellId, out var bucket) || bucket.Count == 0) continue; @@ -240,6 +260,24 @@ public sealed class RetailPViewRenderer return new[] { NoClipSlice }; } + /// + /// True iff has at least one view slice backed by a real plane-clip + /// slot (slot != 0) in this frame's assembly — i.e. a genuine doorway aperture. A cell with no + /// entry, or only the no-clip slot 0 (screen-covering / degenerate aperture → scissor fallback), + /// returns false. Used by the outdoor-node root to refuse drawing an interior cell that would + /// otherwise paint full-screen (the see-through-walls bug). Slot 0 is reserved as no-clip + /// (), so "real aperture" is precisely "some slot != 0". + /// + private static bool HasRealClipSlot(ClipFrameAssembly clipAssembly, uint cellId) + { + if (!clipAssembly.CellIdToViewSlices.TryGetValue(cellId, out var slices)) + return false; + foreach (var slice in slices) + if (slice.Slot != 0) + return true; + return false; + } + private void UseIndoorMembershipOnlyRouting() { // Retail's PView portal views decide which cells/objects are eligible,