diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 1c1db412..c96ad05f 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -5630,25 +5630,46 @@ public sealed class GameWindow : IDisposable // as WorldEntity records below — they have real GfxObj MeshRefs that work // fine; EnvCellRenderer.RegisterCell receives an empty staticObjects list. var cellSubMeshes = AcDream.Core.Meshing.CellMesh.Build(envCell, cellStruct, _dats); + + // The cell transforms and the physics + visibility hydration are + // INDEPENDENT of whether the cell has drawable geometry. Retail + // couples neither collision nor portal visibility to a render mesh. + // Keep the small render lift out of physics; retail BSP contact + // planes use the EnvCell origin verbatim. The lift constant is + // shared with every draw-space consumer of portal polygons + // (OutsideView gate, seal/punch fans) — see + // PortalVisibilityBuilder.ShellDrawLiftZ (#130). + var physicsCellOrigin = envCell.Position.Origin + lbOffset; + var cellOrigin = physicsCellOrigin + new System.Numerics.Vector3( + 0f, 0f, AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ); + var cellTransform = + System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * + System.Numerics.Matrix4x4.CreateTranslation(cellOrigin); + var physicsCellTransform = + System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * + System.Numerics.Matrix4x4.CreateTranslation(physicsCellOrigin); + + // G.3a (#133) hydration decouple: BuildLoadedCell + CacheCellStruct + // were previously gated behind `cellSubMeshes.Count > 0`, which + // silently dropped collision (CellTransit.GetCellStruct -> null -> + // fall through floor) and the visibility node for any geometry-less + // collision cell. CacheCellStruct self-gates on a null PhysicsBSP + // (PhysicsDataCache), so this is safe for cells with no physics. + // + // BuildLoadedCell uses the PHYSICS (unlifted) transform. The +0.02 m + // render lift above is a DRAW concern (shell z-fighting vs terrain); + // feeding it into the visibility graph shifted every HORIZONTAL portal + // plane 2 cm up, side-culling decks/landings (#119-residual, + // TowerAscentReplayTests.CapturedTopOfStairs_*). Vertical doorways were + // immune (the lift slides their planes along themselves). + BuildLoadedCell(envCellId, envCell, cellStruct, physicsCellOrigin, physicsCellTransform); + _physicsDataCache.CacheCellStruct(envCellId, envCell, cellStruct, physicsCellTransform); + + // Render registration only when the cell actually has drawable submeshes. if (cellSubMeshes.Count > 0) { _pendingCellMeshes[envCellId] = cellSubMeshes; - // Keep the small render lift out of physics; retail BSP - // contact planes use the EnvCell origin verbatim. The lift - // constant is shared with every draw-space consumer of - // portal polygons (OutsideView gate, seal/punch fans) — - // see PortalVisibilityBuilder.ShellDrawLiftZ (#130). - var physicsCellOrigin = envCell.Position.Origin + lbOffset; - var cellOrigin = physicsCellOrigin + new System.Numerics.Vector3( - 0f, 0f, AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ); - var cellTransform = - System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * - System.Numerics.Matrix4x4.CreateTranslation(cellOrigin); - var physicsCellTransform = - System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * - System.Numerics.Matrix4x4.CreateTranslation(physicsCellOrigin); - // Phase A8: register the cell with EnvCellRenderer for rendering. // staticObjects is empty — cell stabs continue as separate WorldEntity // records via the dispatcher (see lines below for the unchanged stab path). @@ -5661,25 +5682,6 @@ public sealed class GameWindow : IDisposable cellWorldPosition: cellOrigin, cellRotation: envCell.Position.Orientation, staticObjects: System.Array.Empty<(uint, System.Numerics.Vector3, System.Numerics.Quaternion, bool, System.Numerics.Matrix4x4)>()); - - // Step 4: build LoadedCell for portal visibility — with the - // PHYSICS (unlifted) transform. The +0.02 m render lift above - // is a DRAW concern (shell z-fighting vs terrain); feeding it - // into the visibility graph shifted every HORIZONTAL portal - // plane 2 cm up, putting an eye standing on a deck/landing - // 10–20 mm BELOW the lifted plane — outside the side test's - // ±10 mm in-plane window — so the cell behind the portal was - // side-culled: the tower-top staircase vanish + roof flap - // (#119-residual; captured live at eye z=126.803 vs the - // 010A→0107 plane at 126.80, reproduced ONLY with the lift in - // TowerAscentReplayTests.CapturedTopOfStairs_*). Vertical - // doorways were immune (the lift slides their planes along - // themselves), which is why this hit exactly stairs, decks, - // and cellar mouths. - BuildLoadedCell(envCellId, envCell, cellStruct, physicsCellOrigin, physicsCellTransform); - - // Cache CellStruct physics BSP for indoor collision (UNCHANGED). - _physicsDataCache.CacheCellStruct(envCellId, envCell, cellStruct, physicsCellTransform); } } }