diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs index 085c371..ad3e4e1 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs @@ -81,16 +81,6 @@ public sealed unsafe class EnvCellRenderer : IDisposable private static uint _currentVao; private static CullMode? _currentCullMode; - // Phase A8 A/B diagnostic (2026-05-28 visual-gate-#2 follow-up): - // ACDREAM_A8_DISABLE_CULL=1 forces every batch's effective CullMode to - // None (no face culling). Used to isolate whether the missing-floor - // symptom is caused by polygon-winding+CullMode interaction or by - // something else (lighting, depth, alpha). If the floor appears with - // this set, cull/winding is the bug. If not, look elsewhere. Static - // because it's read once at startup; no need to re-query per draw. - private static readonly bool _forceCullModeNone = - string.Equals(Environment.GetEnvironmentVariable("ACDREAM_A8_DISABLE_CULL"), "1", StringComparison.Ordinal); - public bool NeedsPrepare { get; private set; } = true; public bool IsDisposed { get; private set; } @@ -825,6 +815,22 @@ public sealed unsafe class EnvCellRenderer : IDisposable _gl.BindVertexArray(0); _currentVao = 0; + // Phase A8 fix (2026-05-28 visual-gate-#3 follow-up): explicitly + // restore cull-back at exit. The Landblock→None override above + // can leave cull DISABLED if the last batch's CullMode was + // Landblock — which would leak into the subsequent dispatcher + // draws (IndoorPass building shells, then LiveDynamic chars + + // NPCs + doors), making them all render see-through (no + // back-face cull). The see-through-head symptom in the + // ACDREAM_A8_DISABLE_CULL=1 A/B test was caused exactly by + // this state leak. Re-enabling cull here restores the + // dispatcher's expected default and updates our static cache + // so the next Render call's first SetCullMode comparison is + // correct. + _gl.Enable(EnableCap.CullFace); + _gl.CullFace(TriangleFace.Back); + _currentCullMode = CullMode.CounterClockwise; + // Update frame stats for probe emission at the call site. _lastFrameStats.CellsRendered = filter?.Count ?? snapshot.BatchedByCell.Count; _lastFrameStats.TrianglesDrawn = 0; @@ -997,10 +1003,24 @@ public sealed unsafe class EnvCellRenderer : IDisposable foreach (var group in batchesByCullMode) { var cullMode = (CullMode)(group.Key % 4); - // A/B diagnostic: ACDREAM_A8_DISABLE_CULL=1 overrides every batch - // to no-culling. Reveals whether floor-missing is a cull/winding - // bug or something else. - if (_forceCullModeNone) cullMode = CullMode.None; + // Phase A8 fix (2026-05-28 visual-gate-#3 evidence): override + // CullMode.Landblock to None for cell-mesh batches. WB sets + // glFrontFace(CW) globally (GameScene.cs:843) so its CullMode + // mapping (Landblock→Back) culls the correct side; we set + // glFrontFace(CCW) in WbDrawDispatcher (line 1056) so the + // mapping would cull the OPPOSITE side, hiding cell floors. + // Cell-mesh polys with CullMode.Landblock represent the floor + + // walls + ceiling of a single room — they face different + // directions but share one CullMode value, so a single cull + // setting can't be correct for all of them. The retail-faithful + // approach is double-sided rendering for cell polys (cull off), + // matching what the cull-disable A/B diagnostic empirically + // confirmed (floor visible with cull off in visual-gate-#3). + // CullMode.Landblock is only ever assigned in this codebase by + // PrepareCellStructMeshData (cell polys) — terrain has its own + // renderer that doesn't go through this code path — so this + // override is scoped exactly right. + if (cullMode == CullMode.Landblock) cullMode = CullMode.None; if (_currentCullMode != cullMode) { SetCullMode(cullMode);