diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index f7b503b..f68e8c3 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -11070,13 +11070,37 @@ public sealed class GameWindow : IDisposable gl.Disable(EnableCap.Blend); _envCellRenderer!.Render(AcDream.App.Rendering.Wb.WbRenderPass.Opaque, currentEnvCellIds); - // Transparency pass. + // Phase A8.F (#2): translucent cell geometry clipped to each cell's portal-chain + // region (stencil BIT 2). Opaque cells already clip correctly via depth (left + // untouched above — Q4 fidelity-vs-perf decision). Bit 1 (OutsideView mask) is + // preserved: every bit-2 op below uses StencilMask 0x02 and never clears the + // whole stencil. The camera cell's region is full-screen, so it (and any cell + // without a non-empty region) renders unclipped. gl.Enable(EnableCap.Blend); gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); gl.DepthMask(false); - _envCellRenderer!.Render(AcDream.App.Rendering.Wb.WbRenderPass.Transparent, currentEnvCellIds); + foreach (var cellId in currentEnvCellIds) + { + var oneCell = new System.Collections.Generic.HashSet { cellId }; + if (cellId != cameraCell.CellId + && portalFrame.CellViews.TryGetValue(cellId, out var cv) && !cv.IsEmpty) + { + _indoorStencilPipeline!.MarkRegionBit2(cv.Polygons); + _indoorStencilPipeline.EnableBit2CellPass(); + _meshShader!.Use(); + _envCellRenderer!.Render(AcDream.App.Rendering.Wb.WbRenderPass.Transparent, oneCell); + gl.Disable(EnableCap.StencilTest); + _indoorStencilPipeline.ResetRegionBit2(cv.Polygons); // clear bit 2 for next cell; bit 1 intact + } + else + { + _meshShader!.Use(); + _envCellRenderer!.Render(AcDream.App.Rendering.Wb.WbRenderPass.Transparent, oneCell); + } + } gl.DepthMask(true); gl.Disable(EnableCap.Blend); + gl.Disable(EnableCap.StencilTest); // ensure clean for Step 4 (which re-enables for bit-1 terrain gate) } // FIX 2026-05-28 (post-third-visual-gate): render IndoorPass entities. diff --git a/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs b/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs index a35b8c7..a887c87 100644 --- a/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs +++ b/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs @@ -347,6 +347,77 @@ public sealed unsafe class IndoorCellStencilPipeline : IDisposable _gl.Disable(EnableCap.StencilTest); } + /// + /// Phase A8.F (#2): mark stencil BIT 2 (only) wherever the NDC region covers, preserving bit 1. + /// Color/depth writes off. Enables the stencil test and leaves it enabled. Used to clip a single + /// cell's translucent geometry to its portal-chain screen region without disturbing the bit-1 + /// OutsideView mask. Pair with EnableBit2CellPass (render) then ResetRegionBit2 (clear). + /// + public void MarkRegionBit2(System.Collections.Generic.IReadOnlyList region) + => DrawRegionBit2(region, setBit: true); + + /// Phase A8.F (#2): clear stencil BIT 2 (only) wherever the NDC region covers, preserving + /// bit 1. Call after rendering a cell so the next cell starts with bit 2 == 0 in this region. + public void ResetRegionBit2(System.Collections.Generic.IReadOnlyList region) + => DrawRegionBit2(region, setBit: false); + + /// Phase A8.F (#2): set render state to draw geometry only where bit 2 is set (read-only + /// stencil). Call between MarkRegionBit2 and the cell's draw. + public void EnableBit2CellPass() + { + _gl.Enable(EnableCap.StencilTest); + _gl.StencilFunc(StencilFunction.Equal, 0x02, 0x02u); // bit 2 set + _gl.StencilMask(0x00u); // read-only — do not touch bit 1 or bit 2 + _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Keep); + } + + // Triangulate the NDC region (fan, z=0) and draw it writing only bit 2 (set or clear). + // StencilMask 0x02 guarantees bit 1 is never modified. Color/depth writes off. + private void DrawRegionBit2(System.Collections.Generic.IReadOnlyList region, bool setBit) + { + int triVerts = 0; + foreach (var p in region) if (!p.IsEmpty) triVerts += (p.Vertices.Length - 2) * 3; + if (triVerts == 0) return; + + var verts = new Vector3[triVerts]; + int idx = 0; + foreach (var p in region) + { + if (p.IsEmpty) continue; + var v0 = new Vector3(p.Vertices[0], 0f); + for (int i = 1; i < p.Vertices.Length - 1; i++) + { + verts[idx++] = v0; + verts[idx++] = new Vector3(p.Vertices[i], 0f); + verts[idx++] = new Vector3(p.Vertices[i + 1], 0f); + } + } + + if (triVerts > _vboCapacityVerts) AllocateVbo(Math.Max(triVerts * 2, 1024)); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); + fixed (Vector3* p = verts) + _gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)(triVerts * sizeof(Vector3)), p); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); + + var identity = Matrix4x4.Identity; + _gl.Enable(EnableCap.StencilTest); + _gl.ColorMask(false, false, false, false); + _gl.DepthMask(false); + _gl.DepthFunc(DepthFunction.Always); + _gl.Disable(EnableCap.CullFace); + _gl.StencilMask(0x02u); // ONLY bit 2 — bit 1 preserved + _gl.StencilFunc(StencilFunction.Always, setBit ? 0x02 : 0x00, 0xFFu); + _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace); + _shader.Use(); + _gl.UniformMatrix4(_uViewProjectionLoc, 1, false, (float*)&identity); + _gl.Uniform1(_uWriteFarDepthLoc, 0); + _gl.BindVertexArray(_vao); + _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)triVerts); + _gl.BindVertexArray(0); + _gl.Enable(EnableCap.CullFace); + _gl.ColorMask(true, true, true, true); + } + /// /// Step 4 of WB's RenderInsideOut: enable stencil read-only with /// ref=1, so subsequent terrain + outdoor entity draws are gated