diff --git a/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs b/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs index f13dda5..4ee53e1 100644 --- a/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs +++ b/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs @@ -264,6 +264,80 @@ public sealed unsafe class IndoorCellStencilPipeline : IDisposable _gl.Disable(EnableCap.StencilTest); } + /// + /// Phase A8.F: mark a pre-projected NDC clip region into stencil bit 1 and far-depth-punch it. + /// Replaces the flat world-space exit-portal path (PortalMeshBuilder.BuildTriangles) with the + /// recursively-clipped region from PortalVisibilityBuilder.OutsideView. Polygons are triangulated + /// (fan) and uploaded as NDC verts (z=0) drawn with an identity view-projection (already clip-space). + /// GL state on exit matches MarkAndPunch: stencil disabled, color+depth on, depth=Less. + /// + public void MarkAndPunchNdc(System.Collections.Generic.IReadOnlyList region) + { + // Triangulate the region (fan per convex polygon) into NDC Vector3 (z=0). + int triVerts = 0; + foreach (var p in region) if (!p.IsEmpty) triVerts += (p.Vertices.Length - 2) * 3; + if (triVerts == 0) { _lastVertexCount = 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); + _lastVertexCount = triVerts; + + // Same GL state machine as MarkAndPunch, but identity VP (verts are already NDC). + var identity = Matrix4x4.Identity; + + _gl.Enable(EnableCap.StencilTest); + _gl.Enable(EnableCap.DepthTest); + _gl.ClearStencil(0); + _gl.Clear(ClearBufferMask.StencilBufferBit); + + // Step 1: mark bit 1. + _gl.ColorMask(false, false, false, false); + _gl.DepthMask(false); + _gl.DepthFunc(DepthFunction.Always); + _gl.Disable(EnableCap.CullFace); + _gl.StencilFunc(StencilFunction.Always, 1, 0xFFu); + _gl.StencilMask(0x01u); + _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); + + // Step 2: far-depth punch where bit 1 is set. + _gl.DepthMask(true); + _gl.DepthFunc(DepthFunction.Always); + _gl.StencilFunc(StencilFunction.Equal, 1, 0x01u); + _gl.StencilMask(0x00u); + _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Keep); + _gl.Uniform1(_uWriteFarDepthLoc, 1); + _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)triVerts); + _gl.BindVertexArray(0); + + // Clean state for the indoor-entities pass (matches MarkAndPunch exit). + _gl.Enable(EnableCap.CullFace); + _gl.ColorMask(true, true, true, true); + _gl.DepthFunc(DepthFunction.Less); + _gl.Disable(EnableCap.StencilTest); + } + /// /// Step 4 of WB's RenderInsideOut: enable stencil read-only with /// ref=1, so subsequent terrain + outdoor entity draws are gated