From 9e2eb909da8c2c9ca9dff9f177b635ebe6b374b0 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 29 May 2026 12:26:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20Phase=20A8.F=20=E2=80=94=20Rend?= =?UTF-8?q?erInsideOut=20driven=20by=20clipped=20OutsideView=20+=20Job-A/B?= =?UTF-8?q?=20decouple?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/AcDream.App/Rendering/GameWindow.cs | 126 +++++++++--------------- 1 file changed, 46 insertions(+), 80 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 492ec36..f7b503b 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -11022,64 +11022,24 @@ public sealed class GameWindow : IDisposable System.Collections.Generic.HashSet? visibleCellIds) { var gl = _gl!; - bool didInsideStencil = false; EmitBuildingsProbe(visibilityCellId: cameraCell.CellId, camBuildings, otherBuildings); - var visiblePortalCells = new System.Collections.Generic.List(); - if (visibleCellIds is not null) + // Phase A8.F: build the recursively-clipped portal frame from the camera cell. + // OutsideView = exit portals clipped to their portal chain (fixes the cellar flap). + // buildingMembership left null here; Task 8 wires cross-building via CrossBuildingViews. + var portalFrame = AcDream.App.Rendering.PortalVisibilityBuilder.Build( + cameraCell, + camPos, + id => _cellVisibility.TryGetCell(id, out var pc) ? pc : null, + viewProj); + + bool didInsideStencil = !portalFrame.OutsideView.IsEmpty; + if (didInsideStencil) { - foreach (uint cellId in visibleCellIds) - { - if (_cellVisibility.TryGetCell(cellId, out var cell) && cell is not null) - visiblePortalCells.Add(cell); - } - } - - int insidePortalVertexCount = visiblePortalCells.Count > 0 - ? _indoorStencilPipeline!.UploadPortalMesh(visiblePortalCells, camPos) - : 0; - - // Steps 1+2: stencil bit 1 + far-depth punch at portal-visible exits only. - // WB builds its outside view from the current portal traversal; using every - // exit on the camera building over-punches terrain through indoor openings - // when an unrelated window/door portal overlaps them in screen space. - if (insidePortalVertexCount > 0) - { - didInsideStencil = true; - gl.Enable(EnableCap.StencilTest); - gl.ClearStencil(0); - gl.Clear(ClearBufferMask.StencilBufferBit); - - // Step 1: stencil bit 1 at our buildings' portals. - // WB VisibilityManager.cs:86-94 - gl.Disable(EnableCap.CullFace); - gl.StencilFunc(StencilFunction.Always, 1, 0xFFu); - gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace); - gl.StencilMask(0x01u); - gl.ColorMask(false, false, false, false); - gl.DepthMask(false); - gl.Enable(EnableCap.DepthTest); - gl.DepthFunc(DepthFunction.Always); - EmitDrawOrderProbe(step: 1, sub: ' '); - _indoorStencilPipeline!.DrawUploadedPortalMesh( - viewProj, - writeFarDepth: false, - enableDepthClamp: true); - EmitStencilProbe(op: "mark-visible"); - - // Step 2: punch depth at portals. - // WB VisibilityManager.cs:99-104 - gl.DepthMask(true); - gl.DepthFunc(DepthFunction.Always); - - EmitDrawOrderProbe(step: 2, sub: ' '); - _indoorStencilPipeline!.DrawUploadedPortalMesh( - viewProj, - writeFarDepth: true, - enableDepthClamp: true); - EmitStencilProbe(op: "punch-visible"); + _indoorStencilPipeline!.MarkAndPunchNdc(portalFrame.OutsideView.Polygons); + EmitStencilProbe(op: "mark-clipped"); } // Step 3: render the indoor cells visible from the camera cell @@ -11151,40 +11111,46 @@ public sealed class GameWindow : IDisposable EmitEnvCellProbe(camBuildings.Count, otherBuildings.Count, currentEnvCellIds.Count); - // Step 4: stencil-gated outdoor (terrain + scenery + static objects). - // WB VisibilityManager.cs:130-154 + // Step 4 (WB VisibilityManager.cs:130-154): stencil STATE gated; exterior DRAWS unconditional. + // WB draws terrain + scenery OUTSIDE the `if`; only the stencil read-only state setup is + // gated on whether an indoor mask was marked. When no exit portal was visible (sealed view, + // e.g. a cellar that reaches no exit), depth written in Step 3 occludes terrain on its own. if (didInsideStencil) { gl.Enable(EnableCap.StencilTest); gl.StencilFunc(StencilFunction.Equal, 1, 0x01u); gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Keep); gl.StencilMask(0x00u); - gl.ColorMask(true, true, true, false); - gl.DepthMask(true); - gl.Enable(EnableCap.CullFace); - gl.DepthFunc(DepthFunction.Less); - - EmitDrawOrderProbe(step: 4, sub: ' '); - // Terrain (WB line 143). - // acdream's retail/ACME terrain mesh is CCW from the visible top side - // (see terrain_modern.vert's LandblockMesh order comment), while WB's - // editor terrain uses the opposite vertex order under its global CW - // convention. Step 4 enables culling before terrain, so temporarily - // use terrain's own front-face convention or ground disappears through - // indoor portal silhouettes. - gl.FrontFace(FrontFaceDirection.Ccw); - _terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb); - gl.FrontFace(FrontFaceDirection.CW); - - _meshShader!.Use(); - // Scenery + static objects via dispatcher (WB lines 148-154). - _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntriesWithoutAnimatedIndex, frustum, - neverCullLandblockId: playerLb, - visibleCellIds: visibleCellIds, // OK - outdoor cells outside the building - animatedEntityIds: null, - set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.OutdoorScenery); - _a8PerfLastStaticStats = _wbDrawDispatcher.LastDrawStats; } + else + { + gl.Disable(EnableCap.StencilTest); // sealed view — depth alone occludes outdoor geometry + } + gl.ColorMask(true, true, true, false); + gl.DepthMask(true); + gl.Enable(EnableCap.CullFace); + gl.DepthFunc(DepthFunction.Less); + + EmitDrawOrderProbe(step: 4, sub: ' '); + // Terrain (WB line 143). + // acdream's retail/ACME terrain mesh is CCW from the visible top side + // (see terrain_modern.vert's LandblockMesh order comment), while WB's + // editor terrain uses the opposite vertex order under its global CW + // convention. Step 4 enables culling before terrain, so temporarily + // use terrain's own front-face convention or ground disappears through + // indoor portal silhouettes. + gl.FrontFace(FrontFaceDirection.Ccw); + _terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb); + gl.FrontFace(FrontFaceDirection.CW); + + _meshShader!.Use(); + // Scenery + static objects via dispatcher (WB lines 148-154). + _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntriesWithoutAnimatedIndex, frustum, + neverCullLandblockId: playerLb, + visibleCellIds: visibleCellIds, // OK - outdoor cells outside the building + animatedEntityIds: null, + set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.OutdoorScenery); + _a8PerfLastStaticStats = _wbDrawDispatcher.LastDrawStats; // Step 5: per-other-building 3-bit stencil pipeline. // WB VisibilityManager.cs:157-232