refactor(render): Phase U.1 — delete two-pipe inside-out machinery
Remove IndoorCellStencilPipeline + portal_stencil shaders, RenderInsideOutAcdream, RenderOutsideInAcdream, the A8-perf instrumentation, the cameraInsideBuilding / ACDREAM_A8_INDOOR_BRANCH branch, and the dead EntitySet partition values. Collapse the render branch to the default Draw(All) path (U.4a replaces it with the gated unified pass). Keep all audited EnvCellRenderer / BuildingLoader / CellVisibility / camera-collision fixes. Also deleted with the partition: the two test-only walk helpers (WbDrawDispatcher.WalkEntitiesForTest / WalkEntitiesForTestByCellIds) and their test files (WbDrawDispatcherEntitySetTests, WbDrawDispatcherCellIdsOverloadTests), which existed solely to exercise the removed IndoorPass/OutdoorScenery/ BuildingShells/LiveDynamic partition. EntityMatchesSet / IsShellScopedSet collapse to the All-path constants; the set: parameter is retained as a seam for the unified pass. Note: the depth-clear-if-inside default-path workaround was removed per the U.1 task list — any current indoor-wall degradation persists until a later Phase U task lands the unified pass (expected, not a regression introduced here). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0f7b395be1
commit
3fc77be5de
8 changed files with 37 additions and 2612 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,792 +0,0 @@
|
|||
// IndoorCellStencilPipeline.cs — Phase A8 indoor-cell visibility culling.
|
||||
//
|
||||
// Ports WorldBuilder's RenderInsideOut stencil mechanism for acdream's
|
||||
// modern GL pipeline. Closes issue #78 (outdoor stabs visible through
|
||||
// indoor walls) and the cellar-stairs artifact (outdoor terrain visible
|
||||
// inside cottage cellars).
|
||||
//
|
||||
// Algorithm (per WB VisibilityManager.cs:73-239):
|
||||
// Step 1: stencil ref=1 marked everywhere portal polygons cover
|
||||
// (color/depth writes off, depth=Always, CullFace off).
|
||||
// Step 2: gl_FragDepth=1.0 punched into stencil=1 regions
|
||||
// (color off, depth on, depth=Always).
|
||||
// Step 3: (caller) draw indoor entities with stencil OFF.
|
||||
// Step 4: (caller) draw terrain + outdoor entities with
|
||||
// glStencilFunc(Equal, 1, 0x01), stencil read-only.
|
||||
//
|
||||
// Retail equivalent: PView::DrawCells at
|
||||
// docs/research/named-retail/acclient_2013_pseudo_c.txt:432709 uses
|
||||
// screen-space polygon-clip scissor instead of stencil. Same observable
|
||||
// behavior; the stencil approach matches modern GL pipeline conventions.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Pure-math triangle-fan generation from a list of cells with
|
||||
/// <see cref="LoadedCell.PortalPolygons"/>. Extracted as a static class so
|
||||
/// the vertex-list construction is unit-testable without a GL context.
|
||||
/// </summary>
|
||||
public static class PortalMeshBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a flat triangle-fan vertex array in world space from every
|
||||
/// exit portal (<see cref="CellPortalInfo.OtherCellId"/> == 0xFFFF) on
|
||||
/// every cell in <paramref name="cells"/>. Inner portals are skipped
|
||||
/// — they don't open to outdoors, so stencil-marking them would let
|
||||
/// outdoor geometry bleed into adjacent rooms (incorrect).
|
||||
/// </summary>
|
||||
public static Vector3[] BuildTriangles(
|
||||
IReadOnlyCollection<LoadedCell> cells,
|
||||
Vector3? cameraWorldPosition = null)
|
||||
{
|
||||
// Pre-count to size the output exactly.
|
||||
int triCount = 0;
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
for (int p = 0; p < cell.Portals.Count; p++)
|
||||
{
|
||||
if (cell.Portals[p].OtherCellId != 0xFFFF) continue;
|
||||
if (!ExitPortalPassesCameraSide(cell, p, cameraWorldPosition)) continue;
|
||||
if (p >= cell.PortalPolygons.Count) continue;
|
||||
var poly = cell.PortalPolygons[p];
|
||||
if (poly.Length < 3) continue;
|
||||
triCount += (poly.Length - 2) * 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (triCount == 0) return Array.Empty<Vector3>();
|
||||
|
||||
var output = new Vector3[triCount];
|
||||
int outIdx = 0;
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
var xform = cell.WorldTransform;
|
||||
for (int p = 0; p < cell.Portals.Count; p++)
|
||||
{
|
||||
if (cell.Portals[p].OtherCellId != 0xFFFF) continue;
|
||||
if (!ExitPortalPassesCameraSide(cell, p, cameraWorldPosition)) continue;
|
||||
if (p >= cell.PortalPolygons.Count) continue;
|
||||
var poly = cell.PortalPolygons[p];
|
||||
if (poly.Length < 3) continue;
|
||||
|
||||
// Triangle-fan from vertex 0.
|
||||
var v0 = Vector3.Transform(poly[0], xform);
|
||||
for (int i = 1; i < poly.Length - 1; i++)
|
||||
{
|
||||
output[outIdx++] = v0;
|
||||
output[outIdx++] = Vector3.Transform(poly[i], xform);
|
||||
output[outIdx++] = Vector3.Transform(poly[i + 1], xform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static bool ExitPortalPassesCameraSide(
|
||||
LoadedCell cell,
|
||||
int portalIndex,
|
||||
Vector3? cameraWorldPosition)
|
||||
{
|
||||
if (cameraWorldPosition is not Vector3 camera)
|
||||
return true;
|
||||
if (portalIndex >= cell.ClipPlanes.Count)
|
||||
return true;
|
||||
|
||||
var plane = cell.ClipPlanes[portalIndex];
|
||||
if (plane.Normal.LengthSquared() < 1e-8f)
|
||||
return true;
|
||||
|
||||
var localCamera = Vector3.Transform(camera, cell.InverseWorldTransform);
|
||||
float dot = Vector3.Dot(plane.Normal, localCamera) + plane.D;
|
||||
|
||||
return plane.InsideSide == 0
|
||||
? dot >= -0.01f
|
||||
: dot <= 0.01f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GL pipeline owner. Holds the portal stencil shader and a dynamic
|
||||
/// VBO/VAO for per-frame portal triangle uploads. Public methods are
|
||||
/// called in the per-frame render path when the camera is inside an
|
||||
/// EnvCell. When outside, this object is dormant — no GL work.
|
||||
/// </summary>
|
||||
public sealed unsafe class IndoorCellStencilPipeline : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly Shader _shader;
|
||||
private readonly uint _vao;
|
||||
private readonly uint _vbo;
|
||||
private int _vboCapacityVerts;
|
||||
private int _lastVertexCount;
|
||||
private readonly int _uViewProjectionLoc;
|
||||
private readonly int _uWriteFarDepthLoc;
|
||||
|
||||
public IndoorCellStencilPipeline(GL gl, string vertPath, string fragPath)
|
||||
{
|
||||
_gl = gl;
|
||||
_shader = new Shader(gl, vertPath, fragPath);
|
||||
_uViewProjectionLoc = _gl.GetUniformLocation(_shader.Program, "uViewProjection");
|
||||
_uWriteFarDepthLoc = _gl.GetUniformLocation(_shader.Program, "uWriteFarDepth");
|
||||
|
||||
_vao = _gl.GenVertexArray();
|
||||
_vbo = _gl.GenBuffer();
|
||||
AllocateVbo(1024);
|
||||
ConfigureVao();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the per-frame portal triangle array from <paramref name="cells"/>
|
||||
/// and uploads it to <see cref="_vbo"/>. Returns the vertex count
|
||||
/// (0 means no exit portals — caller should skip stencil setup entirely).
|
||||
/// </summary>
|
||||
public int UploadPortalMesh(
|
||||
IReadOnlyCollection<LoadedCell> cells,
|
||||
Vector3? cameraWorldPosition = null)
|
||||
{
|
||||
var verts = PortalMeshBuilder.BuildTriangles(cells, cameraWorldPosition);
|
||||
_lastVertexCount = verts.Length;
|
||||
if (_lastVertexCount == 0) return 0;
|
||||
|
||||
if (_lastVertexCount > _vboCapacityVerts)
|
||||
{
|
||||
_vboCapacityVerts = Math.Max(_lastVertexCount * 2, 1024);
|
||||
AllocateVbo(_vboCapacityVerts);
|
||||
}
|
||||
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
||||
fixed (Vector3* p = verts)
|
||||
{
|
||||
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0,
|
||||
(nuint)(_lastVertexCount * sizeof(Vector3)), p);
|
||||
}
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
return _lastVertexCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the portal mesh most recently uploaded by <see cref="UploadPortalMesh"/>.
|
||||
/// The caller owns stencil/depth/color/cull state, matching
|
||||
/// <see cref="RenderBuildingStencilMask"/>.
|
||||
/// </summary>
|
||||
public void DrawUploadedPortalMesh(
|
||||
Matrix4x4 viewProjection,
|
||||
bool writeFarDepth,
|
||||
bool enableDepthClamp = true)
|
||||
{
|
||||
if (_lastVertexCount == 0)
|
||||
{
|
||||
LastStencilVertexCount = 0;
|
||||
LastStencilWasFarPunch = writeFarDepth;
|
||||
LastStencilBuildingId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableDepthClamp)
|
||||
_gl.Enable(EnableCap.DepthClamp);
|
||||
|
||||
_shader.Use();
|
||||
var vp = viewProjection;
|
||||
_gl.UniformMatrix4(_uViewProjectionLoc, 1, false, (float*)&vp);
|
||||
_gl.Uniform1(_uWriteFarDepthLoc, writeFarDepth ? 1 : 0);
|
||||
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)_lastVertexCount);
|
||||
_gl.BindVertexArray(0);
|
||||
|
||||
if (enableDepthClamp)
|
||||
_gl.Disable(EnableCap.DepthClamp);
|
||||
|
||||
LastStencilVertexCount = _lastVertexCount;
|
||||
LastStencilWasFarPunch = writeFarDepth;
|
||||
LastStencilBuildingId = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Steps 1+2 of WB's RenderInsideOut: mark stencil ref=1 wherever
|
||||
/// portal polygons cover, then write gl_FragDepth=1.0 into those
|
||||
/// regions. Leaves the GL state set up for the caller to invoke
|
||||
/// <see cref="EnableOutdoorPass"/>.
|
||||
/// </summary>
|
||||
public void MarkAndPunch(Matrix4x4 viewProjection)
|
||||
{
|
||||
if (_lastVertexCount == 0) return;
|
||||
|
||||
_gl.Enable(EnableCap.StencilTest);
|
||||
_gl.Enable(EnableCap.DepthTest); // idempotent if already on; makes MarkAndPunch self-contained
|
||||
_gl.ClearStencil(0);
|
||||
_gl.Clear(ClearBufferMask.StencilBufferBit);
|
||||
|
||||
// Step 1: stencil mark.
|
||||
_gl.ColorMask(false, false, false, false);
|
||||
_gl.DepthMask(false);
|
||||
_gl.DepthFunc(DepthFunction.Always);
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
_gl.Enable(EnableCap.DepthClamp); // portal polys can be at any Z
|
||||
_gl.StencilFunc(StencilFunction.Always, 1, 0xFFu);
|
||||
_gl.StencilMask(0x01u);
|
||||
_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)_lastVertexCount);
|
||||
|
||||
// Step 2: far-depth punch.
|
||||
_gl.ColorMask(false, false, false, false);
|
||||
_gl.DepthMask(true);
|
||||
_gl.DepthFunc(DepthFunction.Always);
|
||||
_gl.StencilFunc(StencilFunction.Equal, 1, 0x01u);
|
||||
_gl.StencilMask(0x00u); // read-only
|
||||
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Keep);
|
||||
|
||||
_gl.Uniform1(_uWriteFarDepthLoc, 1);
|
||||
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)_lastVertexCount);
|
||||
|
||||
_gl.BindVertexArray(0);
|
||||
_gl.Disable(EnableCap.DepthClamp);
|
||||
_gl.Enable(EnableCap.CullFace);
|
||||
|
||||
// Leave a clean state for the indoor-entities pass: stencil
|
||||
// disabled, color+depth on, depth=Less, no cull change.
|
||||
_gl.ColorMask(true, true, true, true);
|
||||
_gl.DepthFunc(DepthFunction.Less);
|
||||
_gl.Disable(EnableCap.StencilTest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void MarkAndPunchNdc(System.Collections.Generic.IReadOnlyList<ViewPolygon> 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; LastStencilVertexCount = 0; LastStencilBuildingId = 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;
|
||||
|
||||
// Phase A8.F: expose the clipped-mask vert count to the [stencil] probe so the Task 9
|
||||
// visual-gate evidence reflects the recursively-clipped OutsideView (building id 0 = N/A).
|
||||
LastStencilVertexCount = triVerts;
|
||||
LastStencilWasFarPunch = true;
|
||||
LastStencilBuildingId = 0;
|
||||
|
||||
// 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.
|
||||
// No DepthClamp here (unlike MarkAndPunch): MarkAndPunch draws world-space portal
|
||||
// polygons that can fall outside [near,far]; these verts are already NDC with z=0,
|
||||
// always within the depth range, so there is nothing to clamp.
|
||||
_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);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
// Make the per-cell transparent render state explicit (don't inherit DrawRegionBit2's
|
||||
// DepthFunc.Always): depth-test Less, depth-write off (translucent). Self-describing so
|
||||
// the clip pass can't silently regress if a sibling helper's exit state changes.
|
||||
_gl.DepthFunc(DepthFunction.Less);
|
||||
_gl.DepthMask(false);
|
||||
}
|
||||
|
||||
// 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, false); // match the indoor pass convention (alpha-write off)
|
||||
// Restore the loop's depth-test: do NOT leak DepthFunc.Always into the cell render, the
|
||||
// unclipped else-branch, the IndoorPass shells, or Step 4. (Opus review C1/C2.)
|
||||
_gl.DepthFunc(DepthFunction.Less);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Step 4 of WB's RenderInsideOut: enable stencil read-only with
|
||||
/// ref=1, so subsequent terrain + outdoor entity draws are gated
|
||||
/// to portal silhouette regions only.
|
||||
/// </summary>
|
||||
public void EnableOutdoorPass()
|
||||
{
|
||||
if (_lastVertexCount == 0) return;
|
||||
_gl.Enable(EnableCap.StencilTest);
|
||||
_gl.StencilFunc(StencilFunction.Equal, 1, 0x01u);
|
||||
_gl.StencilMask(0x00u);
|
||||
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Keep);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores stencil-off state. Call after all outdoor passes complete
|
||||
/// so subsequent rendering (particles, sky, UI) is unaffected.
|
||||
/// </summary>
|
||||
public void DisableStencil()
|
||||
{
|
||||
_gl.Disable(EnableCap.StencilTest);
|
||||
_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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 (2026-05-28): low-level building-portal stencil draw. Mirrors WB
|
||||
/// <c>PortalRenderManager.RenderBuildingStencilMask</c> at
|
||||
/// <c>references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs:471-484</c>.
|
||||
///
|
||||
/// <para>Uploads the building's exit-portal mesh to our shared VBO and draws
|
||||
/// it with the portal_stencil shader. <strong>Does NOT set or restore any
|
||||
/// surrounding GL state</strong> — caller is responsible (stencil func,
|
||||
/// depth mask, color mask, cull face, etc.) per WB
|
||||
/// <c>VisibilityManager.RenderInsideOut</c> Steps 1/2/5a/5b/5d
|
||||
/// expectations.</para>
|
||||
///
|
||||
/// <para>Note: enables/disables <c>GL_DEPTH_CLAMP</c> around the draw
|
||||
/// because portal polygons can extend beyond the camera's near/far range.
|
||||
/// This is symmetric — no state leakage.</para>
|
||||
/// </summary>
|
||||
/// <param name="building">The building whose exit portal polygons to draw.</param>
|
||||
/// <param name="viewProjection">Camera view-projection matrix.</param>
|
||||
/// <param name="writeFarDepth">When true, the fragment shader writes
|
||||
/// <c>gl_FragDepth = 1.0</c> (WB Step 2 / Step 5b "punch" semantic).
|
||||
/// When false, default depth is written (WB Step 1 / Step 5a "mark"
|
||||
/// semantic).</param>
|
||||
public void RenderBuildingStencilMask(AcDream.App.Rendering.Wb.Building building, Matrix4x4 viewProjection, bool writeFarDepth)
|
||||
{
|
||||
int vertexCount = UploadBuildingPortalMesh(building);
|
||||
if (vertexCount == 0)
|
||||
{
|
||||
LastStencilVertexCount = 0;
|
||||
LastStencilWasFarPunch = writeFarDepth;
|
||||
LastStencilBuildingId = building.BuildingId;
|
||||
return;
|
||||
}
|
||||
|
||||
_gl.Enable(EnableCap.DepthClamp);
|
||||
|
||||
_shader.Use();
|
||||
var vp = viewProjection;
|
||||
_gl.UniformMatrix4(_uViewProjectionLoc, 1, false, (float*)&vp);
|
||||
_gl.Uniform1(_uWriteFarDepthLoc, writeFarDepth ? 1 : 0);
|
||||
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)vertexCount);
|
||||
_gl.BindVertexArray(0);
|
||||
|
||||
_gl.Disable(EnableCap.DepthClamp);
|
||||
|
||||
LastStencilVertexCount = vertexCount;
|
||||
LastStencilWasFarPunch = writeFarDepth;
|
||||
LastStencilBuildingId = building.BuildingId;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Probe data (Phase A8 — read by the [stencil] probe emitter in GameWindow).
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Phase A8 RR9: vertex count of the most recent
|
||||
/// <see cref="RenderBuildingStencilMask"/> draw. 0 if the building had no portals.</summary>
|
||||
public int LastStencilVertexCount { get; private set; }
|
||||
|
||||
/// <summary>Phase A8 RR9: true iff the most recent
|
||||
/// <see cref="RenderBuildingStencilMask"/> draw was a far-depth punch (Step 2).</summary>
|
||||
public bool LastStencilWasFarPunch { get; private set; }
|
||||
|
||||
/// <summary>Phase A8 RR9: building id of the most recent
|
||||
/// <see cref="RenderBuildingStencilMask"/> draw.</summary>
|
||||
public uint LastStencilBuildingId { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_shader.Dispose();
|
||||
_gl.DeleteVertexArray(_vao);
|
||||
_gl.DeleteBuffer(_vbo);
|
||||
}
|
||||
|
||||
// Safe to call mid-session after ConfigureVao — the VAO bakes the
|
||||
// VBO association at VertexAttribPointer time, so reallocating the
|
||||
// VBO with new size does NOT require re-running ConfigureVao.
|
||||
private void AllocateVbo(int capacityVerts)
|
||||
{
|
||||
_vboCapacityVerts = capacityVerts;
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
||||
_gl.BufferData(BufferTargetARB.ArrayBuffer,
|
||||
(nuint)(capacityVerts * sizeof(Vector3)),
|
||||
null, BufferUsageARB.DynamicDraw);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
}
|
||||
|
||||
private void ConfigureVao()
|
||||
{
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false,
|
||||
(uint)sizeof(Vector3), (void*)0);
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
#version 430 core
|
||||
//
|
||||
// Phase A8 — portal stencil mark + far-depth punch.
|
||||
//
|
||||
// uWriteFarDepth = 0 → pass through gl_FragCoord.z (used for the
|
||||
// stencil mark pass; depth mask is off anyway).
|
||||
// uWriteFarDepth != 0 → write gl_FragDepth = 1.0 (the far-depth punch
|
||||
// pass; depth mask is on, color is off).
|
||||
//
|
||||
// Matches WorldBuilder's PortalStencil.frag at
|
||||
// references/WorldBuilder/Chorizite.OpenGLSDLBackend/Shaders/PortalStencil.frag
|
||||
|
||||
uniform int uWriteFarDepth;
|
||||
|
||||
void main()
|
||||
{
|
||||
if (uWriteFarDepth != 0)
|
||||
{
|
||||
gl_FragDepth = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
gl_FragDepth = gl_FragCoord.z;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
#version 430 core
|
||||
//
|
||||
// Phase A8 - portal stencil mark + far-depth punch.
|
||||
//
|
||||
// Position is in WORLD space (pipeline transforms cell-local portal
|
||||
// polygon vertices through cell.WorldTransform on the CPU before
|
||||
// uploading to the VBO). Output is clip space via uViewProjection.
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
|
||||
uniform mat4 uViewProjection;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 pos = uViewProjection * vec4(aPosition, 1.0);
|
||||
|
||||
// Match WorldBuilder's PortalStencil.vert: keep portal polygons stable
|
||||
// when the chase camera straddles an exit portal plane. Without this,
|
||||
// near-zero clip W can explode the screen-space portal mask and let the
|
||||
// Step 4 terrain pass punch into indoor floor/wall pixels for a frame.
|
||||
if (abs(pos.w) < 0.001)
|
||||
pos.w = pos.w < 0.0 ? -0.001 : 0.001;
|
||||
|
||||
gl_Position = pos;
|
||||
}
|
||||
|
|
@ -62,42 +62,21 @@ namespace AcDream.App.Rendering.Wb;
|
|||
public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Phase A8 — which subset of entities to walk in a single Draw call.
|
||||
/// Used to split the indoor-cell visibility pipeline into three passes
|
||||
/// when the camera is inside an EnvCell.
|
||||
/// Which subset of entities to walk in a single Draw call.
|
||||
///
|
||||
/// Taxonomy reference: docs/research/2026-05-26-a8-entity-taxonomy.md.
|
||||
/// Phase U.1 (2026-05-30): the indoor/outdoor two-pipe split (IndoorPass /
|
||||
/// OutdoorScenery / BuildingShells / LiveDynamic) was deleted along with the
|
||||
/// inside-out render machinery. <see cref="All"/> is the sole remaining
|
||||
/// member; the unified retail-faithful pass (Phase U) draws every entity in
|
||||
/// one path. The <c>set:</c> parameter is retained on the Draw overloads so
|
||||
/// the unified pass can re-introduce partitioning later without re-threading
|
||||
/// the call sites.
|
||||
/// </summary>
|
||||
public enum EntitySet
|
||||
{
|
||||
/// <summary>Pre-A8 behavior: every entity walked, gated only by
|
||||
/// the existing <c>ParentCellId ∈ visibleCellIds</c> filter.
|
||||
/// Used when the camera is OUTSIDE any EnvCell.</summary>
|
||||
/// <summary>Every entity walked, gated only by the existing
|
||||
/// <c>ParentCellId ∈ visibleCellIds</c> filter.</summary>
|
||||
All,
|
||||
|
||||
/// <summary>Cell mesh + cell statics (<see cref="WorldEntity.ParentCellId"/>
|
||||
/// non-null) PLUS building shell stabs (<see cref="WorldEntity.IsBuildingShell"/>
|
||||
/// true) whose <see cref="WorldEntity.BuildingShellAnchorCellId"/>
|
||||
/// belongs to the active building cell set. Live-dynamic
|
||||
/// (<c>ServerGuid != 0</c>) is excluded; it flows through
|
||||
/// <see cref="LiveDynamic"/>.</summary>
|
||||
IndoorPass,
|
||||
|
||||
/// <summary>Outdoor/top-level stabs (<c>ParentCellId == null</c>),
|
||||
/// including building shells. Drawn stencil-gated to portal
|
||||
/// silhouettes when the camera is inside. Live-dynamic excluded.</summary>
|
||||
OutdoorScenery,
|
||||
|
||||
/// <summary>Top-level building shell stabs only, optionally scoped by
|
||||
/// <see cref="WorldEntity.BuildingShellAnchorCellId"/>. Used for
|
||||
/// portal depth repair without walking the full outdoor scenery set.</summary>
|
||||
BuildingShells,
|
||||
|
||||
/// <summary>Server-spawned dynamic entities (<c>ServerGuid != 0</c>):
|
||||
/// player, NPCs, monsters, dropped items, animated and idle doors.
|
||||
/// Drawn last with stencil disabled so they're depth-tested against
|
||||
/// everything else but not stencil-clipped.</summary>
|
||||
LiveDynamic,
|
||||
}
|
||||
|
||||
private readonly GL _gl;
|
||||
|
|
@ -576,13 +555,6 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
probeState,
|
||||
set);
|
||||
|
||||
if (set == EntitySet.IndoorPass && RenderingDiagnostics.ProbeVisibilityEnabled)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[indoor-shells] anchorPass={walkResult.BuildingShellAnchorPass} " +
|
||||
$"anchorReject={walkResult.BuildingShellAnchorReject} walked={walkResult.EntitiesWalked}");
|
||||
}
|
||||
|
||||
// Tier 1 cache (#53) flush-tracking locals. _walkScratch holds one tuple
|
||||
// per (entity, MeshRefIndex) and is in entity-order, so all MeshRefs of
|
||||
// a given entity are contiguous. We accumulate ALL of an entity's
|
||||
|
|
@ -1507,82 +1479,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 — entity-taxonomy-aware membership test for the three-way
|
||||
/// EntitySet partition. See <see cref="EntitySet"/> for the doctrine.
|
||||
/// Entity-set membership test. Phase U.1 (2026-05-30): with the
|
||||
/// two-pipe partition deleted, the sole <see cref="EntitySet.All"/>
|
||||
/// member matches every entity. Retained as a seam for the unified
|
||||
/// pass to re-introduce partitioning.
|
||||
/// </summary>
|
||||
private static bool EntityMatchesSet(WorldEntity entity, EntitySet set)
|
||||
{
|
||||
if (set == EntitySet.All) return true;
|
||||
|
||||
bool isLiveDynamic = entity.ServerGuid != 0;
|
||||
if (set == EntitySet.LiveDynamic) return isLiveDynamic;
|
||||
if (isLiveDynamic) return false; // IndoorPass/OutdoorScenery exclude live-dynamic
|
||||
|
||||
bool isIndoor = entity.ParentCellId.HasValue || entity.IsBuildingShell;
|
||||
if (set == EntitySet.IndoorPass) return isIndoor;
|
||||
if (set == EntitySet.OutdoorScenery) return !entity.ParentCellId.HasValue;
|
||||
if (set == EntitySet.BuildingShells) return entity.IsBuildingShell;
|
||||
|
||||
throw new InvalidOperationException($"Unhandled EntitySet value: {set}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 test helper: runs the EntitySet partition + visibleCellIds
|
||||
/// gate against an in-memory entity list, returning the IDs that
|
||||
/// survive both filters. Exists so the partition logic is unit-testable
|
||||
/// without requiring a GL context or landblock-entries machinery.
|
||||
/// </summary>
|
||||
public static List<uint> WalkEntitiesForTest(
|
||||
IReadOnlyList<AcDream.Core.World.WorldEntity> entities,
|
||||
HashSet<uint>? visibleCellIds,
|
||||
EntitySet set)
|
||||
{
|
||||
var output = new List<uint>();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (!EntityMatchesSet(entity, set)) continue;
|
||||
if (entity.MeshRefs.Count == 0) continue;
|
||||
|
||||
if (!EntityPassesVisibleCellGate(entity, visibleCellIds, set)) continue;
|
||||
|
||||
output.Add(entity.Id);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR5 (2026-05-26): pure-data walk for the explicit cellIds
|
||||
/// overload. Used by RR7's IndoorPass to render only the camera-buildings'
|
||||
/// cells (instead of the visibility-derived set).
|
||||
///
|
||||
/// <para>Indoor entities (ParentCellId set) gated by membership in
|
||||
/// <paramref name="cellIds"/>. Building shells are gated by
|
||||
/// BuildingShellAnchorCellId membership in the same cell set. Outdoor
|
||||
/// scenery is excluded by the EntitySet partition (no cell-list gate
|
||||
/// needed — EntityMatchesSet handles it).</para>
|
||||
/// </summary>
|
||||
public static List<uint> WalkEntitiesForTestByCellIds(
|
||||
IEnumerable<AcDream.Core.World.WorldEntity> entities,
|
||||
IReadOnlyCollection<uint> cellIds,
|
||||
EntitySet set)
|
||||
{
|
||||
var result = new List<uint>();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (!EntityMatchesSet(entity, set)) continue;
|
||||
if (entity.MeshRefs.Count == 0) continue;
|
||||
if (entity.ParentCellId.HasValue && !cellIds.Contains(entity.ParentCellId.Value))
|
||||
continue;
|
||||
if (IsShellScopedSet(set) && entity.IsBuildingShell)
|
||||
{
|
||||
if (entity.BuildingShellAnchorCellId is not uint anchorCellId ||
|
||||
!cellIds.Contains(anchorCellId))
|
||||
continue;
|
||||
}
|
||||
result.Add(entity.Id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static bool EntityMatchesSet(WorldEntity entity, EntitySet set) => true;
|
||||
|
||||
private static bool EntityPassesVisibleCellGate(
|
||||
WorldEntity entity,
|
||||
|
|
@ -1604,8 +1506,9 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool IsShellScopedSet(EntitySet set) =>
|
||||
set == EntitySet.IndoorPass || set == EntitySet.BuildingShells;
|
||||
// Phase U.1 (2026-05-30): the shell-scoped sets (IndoorPass / BuildingShells)
|
||||
// were deleted with the two-pipe machinery. EntitySet.All is never shell-scoped.
|
||||
private static bool IsShellScopedSet(EntitySet set) => false;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue