feat(render): Phase A8.F — RenderInsideOut driven by clipped OutsideView + Job-A/B decouple

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-29 12:26:49 +02:00
parent 08f6a0c1ce
commit 9e2eb909da

View file

@ -11022,64 +11022,24 @@ public sealed class GameWindow : IDisposable
System.Collections.Generic.HashSet<uint>? visibleCellIds) System.Collections.Generic.HashSet<uint>? visibleCellIds)
{ {
var gl = _gl!; var gl = _gl!;
bool didInsideStencil = false;
EmitBuildingsProbe(visibilityCellId: cameraCell.CellId, camBuildings, otherBuildings); EmitBuildingsProbe(visibilityCellId: cameraCell.CellId, camBuildings, otherBuildings);
var visiblePortalCells = new System.Collections.Generic.List<AcDream.App.Rendering.LoadedCell>(); // Phase A8.F: build the recursively-clipped portal frame from the camera cell.
if (visibleCellIds is not null) // OutsideView = exit portals clipped to their portal chain (fixes the cellar flap).
// buildingMembership left null here; Task 8 wires cross-building via CrossBuildingViews.
var portalFrame = AcDream.App.Rendering.PortalVisibilityBuilder.Build(
cameraCell,
camPos,
id => _cellVisibility.TryGetCell(id, out var pc) ? pc : null,
viewProj);
bool didInsideStencil = !portalFrame.OutsideView.IsEmpty;
if (didInsideStencil)
{ {
foreach (uint cellId in visibleCellIds)
{
if (_cellVisibility.TryGetCell(cellId, out var cell) && cell is not null)
visiblePortalCells.Add(cell);
}
}
int insidePortalVertexCount = visiblePortalCells.Count > 0
? _indoorStencilPipeline!.UploadPortalMesh(visiblePortalCells, camPos)
: 0;
// Steps 1+2: stencil bit 1 + far-depth punch at portal-visible exits only.
// WB builds its outside view from the current portal traversal; using every
// exit on the camera building over-punches terrain through indoor openings
// when an unrelated window/door portal overlaps them in screen space.
if (insidePortalVertexCount > 0)
{
didInsideStencil = true;
gl.Enable(EnableCap.StencilTest);
gl.ClearStencil(0);
gl.Clear(ClearBufferMask.StencilBufferBit);
// Step 1: stencil bit 1 at our buildings' portals.
// WB VisibilityManager.cs:86-94
gl.Disable(EnableCap.CullFace);
gl.StencilFunc(StencilFunction.Always, 1, 0xFFu);
gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace);
gl.StencilMask(0x01u);
gl.ColorMask(false, false, false, false);
gl.DepthMask(false);
gl.Enable(EnableCap.DepthTest);
gl.DepthFunc(DepthFunction.Always);
EmitDrawOrderProbe(step: 1, sub: ' '); EmitDrawOrderProbe(step: 1, sub: ' ');
_indoorStencilPipeline!.DrawUploadedPortalMesh( _indoorStencilPipeline!.MarkAndPunchNdc(portalFrame.OutsideView.Polygons);
viewProj, EmitStencilProbe(op: "mark-clipped");
writeFarDepth: false,
enableDepthClamp: true);
EmitStencilProbe(op: "mark-visible");
// Step 2: punch depth at portals.
// WB VisibilityManager.cs:99-104
gl.DepthMask(true);
gl.DepthFunc(DepthFunction.Always);
EmitDrawOrderProbe(step: 2, sub: ' ');
_indoorStencilPipeline!.DrawUploadedPortalMesh(
viewProj,
writeFarDepth: true,
enableDepthClamp: true);
EmitStencilProbe(op: "punch-visible");
} }
// Step 3: render the indoor cells visible from the camera cell // Step 3: render the indoor cells visible from the camera cell
@ -11151,14 +11111,21 @@ public sealed class GameWindow : IDisposable
EmitEnvCellProbe(camBuildings.Count, otherBuildings.Count, currentEnvCellIds.Count); EmitEnvCellProbe(camBuildings.Count, otherBuildings.Count, currentEnvCellIds.Count);
// Step 4: stencil-gated outdoor (terrain + scenery + static objects). // Step 4 (WB VisibilityManager.cs:130-154): stencil STATE gated; exterior DRAWS unconditional.
// WB VisibilityManager.cs:130-154 // WB draws terrain + scenery OUTSIDE the `if`; only the stencil read-only state setup is
// gated on whether an indoor mask was marked. When no exit portal was visible (sealed view,
// e.g. a cellar that reaches no exit), depth written in Step 3 occludes terrain on its own.
if (didInsideStencil) if (didInsideStencil)
{ {
gl.Enable(EnableCap.StencilTest); gl.Enable(EnableCap.StencilTest);
gl.StencilFunc(StencilFunction.Equal, 1, 0x01u); gl.StencilFunc(StencilFunction.Equal, 1, 0x01u);
gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Keep); gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Keep);
gl.StencilMask(0x00u); gl.StencilMask(0x00u);
}
else
{
gl.Disable(EnableCap.StencilTest); // sealed view — depth alone occludes outdoor geometry
}
gl.ColorMask(true, true, true, false); gl.ColorMask(true, true, true, false);
gl.DepthMask(true); gl.DepthMask(true);
gl.Enable(EnableCap.CullFace); gl.Enable(EnableCap.CullFace);
@ -11184,7 +11151,6 @@ public sealed class GameWindow : IDisposable
animatedEntityIds: null, animatedEntityIds: null,
set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.OutdoorScenery); set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.OutdoorScenery);
_a8PerfLastStaticStats = _wbDrawDispatcher.LastDrawStats; _a8PerfLastStaticStats = _wbDrawDispatcher.LastDrawStats;
}
// Step 5: per-other-building 3-bit stencil pipeline. // Step 5: per-other-building 3-bit stencil pipeline.
// WB VisibilityManager.cs:157-232 // WB VisibilityManager.cs:157-232