diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 274d4cf..5c5e893 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -7148,42 +7148,48 @@ public sealed class GameWindow : IDisposable // Step 4: portal visibility — compute BEFORE the UBO upload so // the indoor flag drives the sun's intensity to zero for // dungeons (r13 §13.7). - // Phase U.4c (2026-05-31): root indoor visibility at the PLAYER's cell, not the - // camera EYE. Retail's CellManager::ChangePosition (0x004559B0) tracks curr_cell by - // the player/physics position. The 3rd-person chase EYE drifts out of the player's - // cell (through interior walls into AABB gaps); FindCameraCell then can't place the - // eye and returns the STALE previous cell for its 3 grace frames, from which the - // doorway portal is "behind" the eye → culled → the exit cell + terrain + shells - // flap off. ACDREAM_PROBE_FLAP capture (2026-05-31): every flap frame is - // res=Grace eyeInRoot=n terrain=Skip; every good frame is eyeInRoot=Y. The eye is - // still used for the per-frame PROJECTION (envCellViewProj) — only the cell ROOT + - // portal-side test track the player. This mirrors the playerInsideCell lighting - // decision below, which already roots at the player for exactly this reason. - var visRootPos = (_playerMode && _playerController is not null) - ? _playerController.Position - : camPos; - // UCG W2: use the physics membership answer (DataCache.CellGraph.CurrCell) as the - // BFS root instead of resolving from position via FindCameraCell. Falls back to the - // original ComputeVisibility path when the physics answer isn't usable yet (null - // CurrCell, or its cell id not yet registered with the render CellVisibility system). - // This closes the render/physics disagreement — both now key off the same BSP-based - // resolution — which is the root cause of the "world from below" spawn flicker. - LoadedCell? physicsRoot = null; - if (_physicsEngine.DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell physCell - && _cellVisibility.TryGetCell(physCell.Id, out var registeredCell)) - physicsRoot = registeredCell; - var visibility = _cellVisibility.ComputeVisibilityFromRoot(physicsRoot, visRootPos); + // Phase W single-viewpoint V1 (2026-06-03): the render keys on ONE viewpoint — the + // collided camera ("viewer") — exactly like retail (RenderNormalMode @ 0x453aa0 → + // DrawInside(viewer_cell) pc:92675; InitCell side-test vs viewer.viewpoint pc:432991). + // The viewer cell is the camera-collision sweep's swept cell + // (RetailChaseCamera.ViewerCellId = retail viewer_cell = sphere_path.curr_cell): + // graph-tracked, deterministic, NO AABB / NO grace frames — so the U.4c flap source + // (stale FindCameraCell over grace frames) is gone WITHOUT splitting viewpoints. + // SEPARATELY, lighting / seen_outside key on the PLAYER cell (CurrCell), matching retail + // CellManager::ChangePosition @ 0x4559B0 — the player's cell, not the camera's, decides + // whether the sun dies (sealed interior). retail player->cell (physics/lighting) vs + // SmartBox->viewer_cell (render); the old per-render player-root + eye-projection split is gone. + + // ── Lighting root: the PLAYER cell (CurrCell). ── + LoadedCell? playerRoot = null; + if (_physicsEngine.DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell playerCellObj + && _cellVisibility.TryGetCell(playerCellObj.Id, out var playerRegCell)) + playerRoot = playerRegCell; + bool playerSeenOutside = playerRoot?.SeenOutside ?? true; + + // ── Render root: the VIEWER (collided camera) cell + eye. ── + // Default (player mode + retail chase cam): the sweep's viewer cell. Fallback for the + // non-default legacy/debug camera paths: the player's registered cell (or none). + uint viewerCellId = + (_playerMode && _retailChaseCamera is not null + && AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera) + ? _retailChaseCamera.ViewerCellId + : (playerRoot?.CellId ?? 0u); + var viewerEyePos = camPos; // the collided eye drives the side-test AND the projection + LoadedCell? viewerRoot = null; + if ((viewerCellId & 0xFFFFu) >= 0x0100u + && _cellVisibility.TryGetCell(viewerCellId, out var viewerRegCell)) + viewerRoot = viewerRegCell; + var visibility = _cellVisibility.ComputeVisibilityFromRoot(viewerRoot, viewerEyePos); bool cameraInsideCell = visibility?.CameraCell is not null; - // Stage 3 (2026-06-02): extract seen_outside from the PVS root cell. - // Retail CellManager::ChangePosition @ 0x004559B0 (pseudo_c:94649): - // "if (seen_outside || keep_lscape_loaded) keep landscape + terrain - // else LScape::release_all (dungeon)" - // Outdoor root (physicsRoot==null) → always seen_outside=true. - // Building interior with exit portal → seen_outside=true (sky/terrain kept live; - // clipped to doorway in Stage 4). + // Stage 3 (2026-06-02): the RENDER's seen_outside (gates terrain/sky through the + // doorway) comes from the VIEWER root cell. Retail CellManager::ChangePosition + // @ 0x004559B0 (pseudo_c:94649): keep landscape+terrain iff seen_outside else release. + // Outdoor viewer (viewerRoot==null) → always seen_outside=true. + // Building interior with exit portal → seen_outside=true (terrain clipped to the door). // Pure dungeon (no exit portal reachable) → seen_outside=false (sky suppressed). - bool rootSeenOutside = physicsRoot?.SeenOutside ?? true; + bool rootSeenOutside = viewerRoot?.SeenOutside ?? true; // Phase U.4 (2026-05-30): the [vis] probe moved DOWN to the unified // gated-draw block (after envCellViewProj exists) where it can report @@ -7198,8 +7204,9 @@ public sealed class GameWindow : IDisposable // independent AABB containment scan. playerInsideCell = true (kill sunlight) only // when the player is inside a SEALED interior (seen_outside=false = dungeon). // Building interiors with seen_outside=true keep the sun (sky visible through door). - // When not in player mode (orbit/fly debug camera) we fall back to cameraInsideCell. - bool playerInsideCell = cameraInsideCell && !rootSeenOutside; + // V1 (2026-06-03): keyed on the PLAYER cell (playerRoot/playerSeenOutside), independent + // of the camera's viewer cell — retail kills the sun off the player's cell, not the eye. + bool playerInsideCell = playerRoot is not null && !playerSeenOutside; // Phase C.1: tick retail PhysicsScript particle hooks. Named // retail decomp confirms SkyObject.PesObjectId is copied by @@ -7320,12 +7327,13 @@ public sealed class GameWindow : IDisposable HashSet? envCellShellFilter = null; // drawable visible cells (cellIdToSlot keys) if (clipRoot is not null) { - // Phase U.4c: side test + distance ordering use the PLAYER position (visRootPos, - // stable inside the cell); projection uses the eye's envCellViewProj (the screen - // view). See the visRootPos rationale at the ComputeVisibility call above. + // Phase W single-viewpoint V1 (2026-06-03): the portal side test + distance ordering + // use the VIEWER eye (the collided camera) — same viewpoint as the projection + // (envCellViewProj) and the render root (clipRoot = the viewer cell). ONE viewpoint, + // retail InitCell side-test vs viewer.viewpoint (pc:432991). No more player/eye split. pvFrame = PortalVisibilityBuilder.Build( clipRoot, - visRootPos, + viewerEyePos, id => _cellVisibility.TryGetCell(id, out var c) ? c : null, envCellViewProj); @@ -7369,8 +7377,10 @@ public sealed class GameWindow : IDisposable { var flapPlayer = _playerController?.Position ?? camPos; bool eyeInRoot = CellVisibility.PointInCell(camPos, clipRoot); + uint flapPlayerCell = _physicsEngine.DataCache?.CellGraph.CurrCell?.Id ?? 0u; Console.WriteLine( - $"[flap-cam] root=0x{clipRoot.CellId:X8} res={_cellVisibility.LastCameraCellResolution} " + + $"[flap-cam] root=0x{clipRoot.CellId:X8} viewerCell=0x{viewerCellId:X8} playerCell=0x{flapPlayerCell:X8} " + + $"res={_cellVisibility.LastCameraCellResolution} " + $"eyeInRoot={(eyeInRoot ? "Y" : "n")} eye=({camPos.X:F2},{camPos.Y:F2},{camPos.Z:F2}) " + $"player=({flapPlayer.X:F2},{flapPlayer.Y:F2},{flapPlayer.Z:F2}) " + $"terrain={clipAssembly.TerrainMode} outVisible={clipAssembly.OutdoorVisible}");