feat(render): Phase A8 RR6 — IndoorCellStencilPipeline 3-bit + occlusion-query
Extends the dormant single-bit stencil pipeline with WB Step 5 primitives:
MarkBuildingBit2 — mark stencil bit 2 where bit 1 set
PunchDepthAtStencil3 — depth=1.0 at intersection (stencil==3)
EnableOtherBuildingPass — render state for stencil==3 EnvCell pass
ResetBit2 — clear bit 2 between iterations
UploadBuildingPortalMesh — upload a Building.ExitPortalPolygons (vs
cell-based UploadPortalMesh)
Plus occlusion-query helpers:
EnsureOcclusionQueryId — lazy GenQuery
TryReadOcclusionResult — asynchronous read-back (no CPU stall)
BeginOcclusionQuery — BeginQuery wrapper
EndOcclusionQuery — EndQuery wrapper
All GL state sequences mirror WB VisibilityManager.cs:73-239 line-by-line.
Comments reference the corresponding WB line numbers for verification.
Consumed by RR7's Steps 1-4 + RR9's Step 5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3361933ce6
commit
6a7894ac35
1 changed files with 243 additions and 0 deletions
|
|
@ -221,6 +221,249 @@ public sealed unsafe class IndoorCellStencilPipeline : IDisposable
|
|||
_gl.StencilMask(0xFFu); // restore default write mask
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Phase A8 RR6 (2026-05-26): Step 5 — 3-bit stencil mode + occlusion-query
|
||||
// helpers. These methods are called by RR7 (Steps 1-4 wire-in) and RR9
|
||||
// (Step 5 cross-building pass). They assume the VBO was already uploaded
|
||||
// by UploadBuildingPortalMesh for the specific building being processed.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR6: Step 5a — mark stencil bit 2 at portal silhouettes WHERE
|
||||
/// bit 1 is already set. After this call, pixels at the intersection of our
|
||||
/// building's portals (bit 1) and the other building's portals (bit 2) will
|
||||
/// have stencil == 3. Subsequent <see cref="EnableOtherBuildingPass"/> renders
|
||||
/// into those pixels only.
|
||||
///
|
||||
/// <para>GL state on entry: stencil test enabled (left by prior MarkAndPunch
|
||||
/// or EnableOutdoorPass); depth test on. DepthFunc.Lequal set here.</para>
|
||||
///
|
||||
/// <para>Mirrors WB VisibilityManager.cs:186-189.</para>
|
||||
/// </summary>
|
||||
public void MarkBuildingBit2(Matrix4x4 viewProjection, int buildingPortalVertexCount)
|
||||
{
|
||||
// WB:186 StencilFunc.Equal(3, 0x01) — match where bit 1 is set
|
||||
// WB:187 StencilOp(Keep, Keep, Replace)
|
||||
// WB:188 StencilMask 0x02 — only write to bit 2
|
||||
// WB:189 ColorMask off; DepthMask off; DepthFunc.Lequal; Disable CullFace
|
||||
_gl.StencilFunc(StencilFunction.Equal, 3, 0x01u);
|
||||
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace);
|
||||
_gl.StencilMask(0x02u);
|
||||
_gl.ColorMask(false, false, false, false);
|
||||
_gl.DepthMask(false);
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
_gl.DepthFunc(DepthFunction.Lequal);
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
|
||||
_shader.Use();
|
||||
var vp = viewProjection;
|
||||
_gl.UniformMatrix4(_uViewProjectionLoc, 1, false, (float*)&vp);
|
||||
_gl.Uniform1(_uWriteFarDepthLoc, 0); // color/depth writes off anyway
|
||||
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)buildingPortalVertexCount);
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR6: Step 5b — punch depth=1.0 where stencil == 3 (intersection
|
||||
/// of our portal silhouette and the other building's portal silhouette). Clears
|
||||
/// interior-wall depth written during Step 3 so the other building's cells win
|
||||
/// depth when rendered in <see cref="EnableOtherBuildingPass"/>.
|
||||
///
|
||||
/// <para>GL state on entry: stencil enabled; StencilMask 0x02 from MarkBuildingBit2.
|
||||
/// This method sets StencilMask 0x00 (read-only), DepthMask on, DepthFunc.Always.</para>
|
||||
///
|
||||
/// <para>Mirrors WB VisibilityManager.cs:201-205.</para>
|
||||
/// </summary>
|
||||
public void PunchDepthAtStencil3(Matrix4x4 viewProjection, int buildingPortalVertexCount)
|
||||
{
|
||||
// WB:201 StencilFunc.Equal(3, 0x03) — match intersection pixels
|
||||
// WB:202 StencilMask 0x00 — read-only stencil
|
||||
// WB:203 DepthMask on; DepthFunc.Always
|
||||
// WB:204-205 draw portal triangles with uWriteFarDepth=1
|
||||
_gl.StencilFunc(StencilFunction.Equal, 3, 0x03u);
|
||||
_gl.StencilMask(0x00u);
|
||||
_gl.DepthMask(true);
|
||||
_gl.DepthFunc(DepthFunction.Always);
|
||||
|
||||
_shader.Use();
|
||||
var vp = viewProjection;
|
||||
_gl.UniformMatrix4(_uViewProjectionLoc, 1, false, (float*)&vp);
|
||||
_gl.Uniform1(_uWriteFarDepthLoc, 1); // write gl_FragDepth = 1.0
|
||||
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)buildingPortalVertexCount);
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR6: Step 5c — set render state to draw the other-building's
|
||||
/// EnvCells where stencil == 3. Does not issue draw calls; caller renders
|
||||
/// the cells immediately after.
|
||||
///
|
||||
/// <para>GL state on entry: stencil func still Equal(3, 0x03) from
|
||||
/// PunchDepthAtStencil3. This method re-enables color and CullFace, sets
|
||||
/// DepthFunc.Less for the geometry pass.</para>
|
||||
///
|
||||
/// <para>Mirrors WB VisibilityManager.cs:210-212.</para>
|
||||
/// </summary>
|
||||
public void EnableOtherBuildingPass()
|
||||
{
|
||||
// WB:210 ColorMask true; WB:211 DepthFunc.Less; WB:212 Enable CullFace
|
||||
// Stencil func stays Equal(3, 0x03) from PunchDepthAtStencil3.
|
||||
_gl.ColorMask(true, true, true, false);
|
||||
_gl.DepthFunc(DepthFunction.Less);
|
||||
_gl.Enable(EnableCap.CullFace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR6: Step 5d — reset bit 2 to zero so the next other-building
|
||||
/// iteration starts fresh. Re-draws this building's portal triangles with a
|
||||
/// ref value of 1 (bit 2 = 0) to overwrite bit 2 back to 0 in stencil.
|
||||
///
|
||||
/// <para>GL state on entry: color/depth writes may still be on from
|
||||
/// EnableOtherBuildingPass. This method disables them and rewrites bit 2.</para>
|
||||
///
|
||||
/// <para>Mirrors WB VisibilityManager.cs:222-228.</para>
|
||||
/// </summary>
|
||||
public void ResetBit2(Matrix4x4 viewProjection, int buildingPortalVertexCount)
|
||||
{
|
||||
// WB:222 ColorMask off; DepthMask off
|
||||
// WB:223 StencilMask 0x02
|
||||
// WB:224 StencilFunc.Always(1, 0x02) → ref=1 AND mask=0x02 → writes 0 to bit 2
|
||||
// because ref & writeMask = 1 & 0x02 = 0 (bit 2 of 1 is 0)
|
||||
// WB:225-226 StencilOp Replace
|
||||
// WB:227-228 draw portal triangles
|
||||
_gl.ColorMask(false, false, false, false);
|
||||
_gl.DepthMask(false);
|
||||
_gl.StencilMask(0x02u);
|
||||
_gl.StencilFunc(StencilFunction.Always, 1, 0x02u);
|
||||
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace);
|
||||
|
||||
_shader.Use();
|
||||
var vp = viewProjection;
|
||||
_gl.UniformMatrix4(_uViewProjectionLoc, 1, false, (float*)&vp);
|
||||
_gl.Uniform1(_uWriteFarDepthLoc, 0);
|
||||
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)buildingPortalVertexCount);
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Occlusion-query helpers (Phase A8 RR6). Wrap raw GL query calls with
|
||||
// the same asynchronous non-stalling pattern used by WB VisibilityManager
|
||||
// (lines 173-184 and 267-287). All three Building.QueryId/QueryStarted/
|
||||
// WasVisible fields are written by the caller (RR9) around these helpers.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR6: lazily allocate a GL query object handle. The <paramref name="slot"/>
|
||||
/// is written on first call and reused thereafter. Callers pass
|
||||
/// <c>ref building.QueryId</c>. Mirrors WB VisibilityManager.cs:173
|
||||
/// (<c>if (building.QueryId != 0)</c> guard pattern — we create the id before
|
||||
/// that guard is first needed).
|
||||
/// </summary>
|
||||
public uint EnsureOcclusionQueryId(ref uint slot)
|
||||
{
|
||||
if (slot == 0) slot = _gl.GenQuery();
|
||||
return slot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR6: non-blocking read of the previous frame's occlusion query
|
||||
/// result. Returns false immediately if the result is not yet available (no
|
||||
/// CPU stall). Sets <paramref name="anyPassed"/> to true if at least one
|
||||
/// sample passed.
|
||||
///
|
||||
/// <para>Mirrors WB VisibilityManager.cs:174-180 and 272-278.</para>
|
||||
/// </summary>
|
||||
public bool TryReadOcclusionResult(uint queryId, out bool anyPassed)
|
||||
{
|
||||
anyPassed = false;
|
||||
if (queryId == 0) return false;
|
||||
_gl.GetQueryObject(queryId, QueryObjectParameterName.ResultAvailable, out int available);
|
||||
if (available == 0) return false;
|
||||
_gl.GetQueryObject(queryId, QueryObjectParameterName.Result, out int samplesPassed);
|
||||
anyPassed = samplesPassed > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR6: begin a samples-passed occlusion query. Caller is responsible
|
||||
/// for setting <c>building.QueryStarted = true</c> afterward.
|
||||
/// Mirrors WB VisibilityManager.cs:182 and 279.
|
||||
/// </summary>
|
||||
public void BeginOcclusionQuery(uint queryId) =>
|
||||
_gl.BeginQuery(QueryTarget.SamplesPassed, queryId);
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR6: end a samples-passed occlusion query.
|
||||
/// Mirrors WB VisibilityManager.cs:195 and 286.
|
||||
/// </summary>
|
||||
public void EndOcclusionQuery() =>
|
||||
_gl.EndQuery(QueryTarget.SamplesPassed);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Per-building portal mesh upload (Phase A8 RR6 — S3).
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR6: upload a Building's pre-computed world-space exit portal
|
||||
/// polygons as a triangle fan. Mirrors <see cref="UploadPortalMesh"/> but
|
||||
/// operates on <see cref="AcDream.App.Rendering.Wb.Building.ExitPortalPolygons"/>
|
||||
/// instead of per-cell portal lists. Called once per other-building per frame
|
||||
/// (Step 5 loop in RR9) before the MarkBuildingBit2 / PunchDepthAtStencil3 /
|
||||
/// EnableOtherBuildingPass / ResetBit2 sequence.
|
||||
/// </summary>
|
||||
/// <returns>Vertex count uploaded (always a multiple of 3; 0 if no polygons).</returns>
|
||||
public int UploadBuildingPortalMesh(AcDream.App.Rendering.Wb.Building building)
|
||||
{
|
||||
// Pre-count vertices so we allocate exactly.
|
||||
int triVertCount = 0;
|
||||
foreach (var poly in building.ExitPortalPolygons)
|
||||
{
|
||||
if (poly.Length < 3) continue;
|
||||
triVertCount += (poly.Length - 2) * 3;
|
||||
}
|
||||
|
||||
if (triVertCount == 0)
|
||||
{
|
||||
_lastVertexCount = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (triVertCount > _vboCapacityVerts)
|
||||
AllocateVbo(Math.Max(triVertCount * 2, 1024));
|
||||
|
||||
// Triangle-fan triangulation — matches UploadPortalMesh / WB PortalRenderManager.cs:537-543.
|
||||
var verts = new Vector3[triVertCount];
|
||||
int idx = 0;
|
||||
foreach (var poly in building.ExitPortalPolygons)
|
||||
{
|
||||
if (poly.Length < 3) continue;
|
||||
var v0 = poly[0];
|
||||
for (int i = 1; i < poly.Length - 1; i++)
|
||||
{
|
||||
verts[idx++] = v0;
|
||||
verts[idx++] = poly[i];
|
||||
verts[idx++] = poly[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
||||
fixed (Vector3* p = verts)
|
||||
{
|
||||
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0,
|
||||
(nuint)(idx * sizeof(Vector3)), p);
|
||||
}
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
|
||||
_lastVertexCount = idx;
|
||||
return idx;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_shader.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue