diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index e2b62d9..c95178d 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -36,6 +36,7 @@ public sealed class GameWindow : IDisposable private AcDream.App.Rendering.Wb.EntitySpawnAdapter? _wbEntitySpawnAdapter; private AcDream.App.Rendering.Vfx.EntityScriptActivator? _entityScriptActivator; private AcDream.App.Rendering.Wb.WbDrawDispatcher? _wbDrawDispatcher; + private IndoorCellStencilPipeline? _indoorStencilPipeline; /// Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters /// support. Required at startup — missing bindless throws /// in OnLoad. @@ -1752,6 +1753,15 @@ public sealed class GameWindow : IDisposable _classificationCache); // A.5 T22.5: apply A2C gate from quality preset. _wbDrawDispatcher.AlphaToCoverage = _resolvedQuality.AlphaToCoverage; + + // Phase A8 R3 — indoor visibility culling pipeline. Owns the + // portal_stencil shader pair and a dynamic VBO for per-frame + // portal triangle uploads. Stencil work runs only when the + // camera is inside an EnvCell; outside, the object is dormant. + _indoorStencilPipeline = new IndoorCellStencilPipeline( + _gl, + Path.Combine(shadersDir, "portal_stencil.vert"), + Path.Combine(shadersDir, "portal_stencil.frag")); } // Phase G.1 sky renderer — its own shader (sky.vert / sky.frag) @@ -7119,14 +7129,10 @@ public sealed class GameWindow : IDisposable if (cameraInsideCell) _gl!.Clear(ClearBufferMask.DepthBufferBit); - // L-fix1 (2026-04-28): pass the set of animated-entity ids so - // the renderer keeps remote players / NPCs / monsters - // visible even when their landblock rotates out of the - // frustum. Without this, other characters wink in/out as - // the camera turns. The set is rebuilt per-frame from - // _animatedEntities — it's small (<100 entities typically) - // so HashSet allocation is cheap. Static scenery still - // respects landblock-level cull. + // L-fix1 (2026-04-28): animated-entity id set. Required by both + // the cameraInsideCell branch (to route them to LiveDynamic pass) + // and the outdoor path (where it preserves visibility across + // landblock frustum culling). HashSet? animatedIds = null; if (_animatedEntities.Count > 0) { @@ -7135,11 +7141,75 @@ public sealed class GameWindow : IDisposable animatedIds.Add(k); } - // N.5: WbDrawDispatcher is always non-null (modern path mandatory). - _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, - neverCullLandblockId: playerLb, - visibleCellIds: visibility?.VisibleCellIds, - animatedEntityIds: animatedIds); + if (cameraInsideCell && _indoorStencilPipeline is not null + && visibility?.CameraCell is not null) + { + // Phase A8 R3 — WB RenderInsideOut order. + // + // 1. Terrain has already drawn (color + depth) at line ~7104. + // 2. depth-clear-if-inside has already cleared depth to 1.0 + // (above this branch at line ~7118). The MarkAndPunch + // below is a no-op against that baseline — left in for + // symmetry with WB's reference pipeline and to handle + // the unusual case where depth-clear is later dropped. + // 3. MarkAndPunch — stencil bit 1 at the camera's-own-cell + // exit portals. Step 5 (cross-cell-portal visibility via + // 3-stencil-bit pipeline) is DEFERRED — we mark ONLY the + // camera's own cell's portals, not the BFS-extended + // VisibleCellIds. Trade-off: cross-cell visibility loss + // (rare visually); correctness in the common case (no + // see-through-wall to far-side portal openings). + var cameraCells = new[] { visibility.CameraCell }; + _indoorStencilPipeline.UploadPortalMesh(cameraCells); + + var viewProjection = camera.View * camera.Projection; + _indoorStencilPipeline.MarkAndPunch(viewProjection); + + // 4. IndoorPass — cell mesh + cell statics + building shells + // (R1's IsBuildingShell flag drives the partition). + // Stencil OFF (MarkAndPunch's cleanup restored that). + // Depth test normal; building shells write the wall depth + // that protects the indoor from outdoor visibility. + _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, + neverCullLandblockId: playerLb, + visibleCellIds: visibility.VisibleCellIds, + animatedEntityIds: animatedIds, + set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.IndoorPass); + + // 5. Stencil-gated outdoor pass. + _indoorStencilPipeline.EnableOutdoorPass(); + + // 5a. Re-draw terrain — at portal-silhouette pixels only, + // terrain Z (with the f48c74a -0.01 nudge) wins over the + // punched 1.0 depth. Color writes through window. + _terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb); + + // 5b. Outdoor scenery — same stencil gating. + _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, + neverCullLandblockId: playerLb, + visibleCellIds: visibility.VisibleCellIds, + animatedEntityIds: animatedIds, + set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.OutdoorScenery); + + // 6. Stencil OFF — live dynamic entities draw freely with + // depth test only (no stencil clipping). + _indoorStencilPipeline.DisableStencil(); + + _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, + neverCullLandblockId: playerLb, + visibleCellIds: visibility.VisibleCellIds, + animatedEntityIds: animatedIds, + set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.LiveDynamic); + } + else + { + // Outdoor path — unchanged from pre-A8: single dispatcher call + // walks every entity with default EntitySet.All partition. + _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, + neverCullLandblockId: playerLb, + visibleCellIds: visibility?.VisibleCellIds, + animatedEntityIds: animatedIds); + } // Phase G.1 / E.3: draw all live particles after opaque // scene geometry so alpha blending composites correctly. @@ -10495,6 +10565,7 @@ public sealed class GameWindow : IDisposable _liveSession = null; _audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context _wbDrawDispatcher?.Dispose(); + _indoorStencilPipeline?.Dispose(); _skyRenderer?.Dispose(); // depends on sampler cache; dispose first _samplerCache?.Dispose(); _textureCache?.Dispose();