feat(render): Phase A8.F — wire-in #2 per-cell translucent clip on stencil bit 2

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-29 12:45:56 +02:00
parent d581f4c549
commit 1c02a01298
2 changed files with 97 additions and 2 deletions

View file

@ -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<uint> { 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.

View file

@ -347,6 +347,77 @@ public sealed unsafe class IndoorCellStencilPipeline : IDisposable
_gl.Disable(EnableCap.StencilTest);
}
/// <summary>
/// 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).
/// </summary>
public void MarkRegionBit2(System.Collections.Generic.IReadOnlyList<ViewPolygon> region)
=> DrawRegionBit2(region, setBit: true);
/// <summary>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.</summary>
public void ResetRegionBit2(System.Collections.Generic.IReadOnlyList<ViewPolygon> region)
=> DrawRegionBit2(region, setBit: false);
/// <summary>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.</summary>
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<ViewPolygon> 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);
}
/// <summary>
/// Step 4 of WB's RenderInsideOut: enable stencil read-only with
/// ref=1, so subsequent terrain + outdoor entity draws are gated