From 352086042e6b2c155320f8fdfb3493e4a5eec021 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 15:37:00 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20Stage=203=20T3.2=20=E2=80=94=20?= =?UTF-8?q?seen=5Foutside=20terrain/sky=20gate=20per=20CellManager::Change?= =?UTF-8?q?Position?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port retail CellManager::ChangePosition @ 0x004559B0 (pseudo_c:94649) landscape policy. Three changes in GameWindow.OnRender: 1. Extract rootSeenOutside = physicsRoot?.SeenOutside ?? true after ComputeVisibilityFromRoot (outdoor null root → always seen_outside=true). 2. Replace IsInsideAnyCell AABB scan with seen_outside-derived predicate: playerInsideCell = cameraInsideCell && !rootSeenOutside. Semantics: sun zeroed only in sealed interior (dungeon); building interiors with seen_outside keep the sun (sky visible through door). 3. renderSky = !cameraInsideCell || rootSeenOutside (Stage 3 gate, interim: sky draws full-screen in building interiors until Stage 4 clips to doorway). 4. Weather gate updated to follow renderSky (seen_outside policy). Retail anchors: CellManager::ChangePosition 0x004559B0 (landscape/sun policy), SmartBox::RenderNormalMode 0x00453aa0 (sky gate per seen_outside). NOTE: Interim regression — sky renders full-screen indoors for seen_outside cells until Stage 4 wires OutsideView clip. Expected per EXECUTION POLICY. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 45 ++++++++++++++++--------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 6431682..c9d92ca 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -7166,6 +7166,16 @@ public sealed class GameWindow : IDisposable var visibility = _cellVisibility.ComputeVisibilityFromRoot(physicsRoot, visRootPos); 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). + // Pure dungeon (no exit portal reachable) → seen_outside=false (sky suppressed). + bool rootSeenOutside = physicsRoot?.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 // the real PortalVisibilityFrame — OutsideView polygon/plane counts and @@ -7173,18 +7183,14 @@ public sealed class GameWindow : IDisposable // of the old camera-state-only spike. See the U.4 ClipFrame assembly // below (gated on ACDREAM_PROBE_VIS=1, cell-change-throttled). - // Lighting decisions (sun zeroed, indoor ambient applied) must - // track the PLAYER's cell, not the camera's. In third-person - // chase mode the camera enters interiors before the player body - // does, so a camera-based trigger flips the scene to indoor - // lighting prematurely. Retail's CellManager::ChangePosition - // @ 0x004559B0 reads CObjCell::seen_outside on the player's - // current cell — that's the semantics we want here. When the - // player isn't in player mode (orbit / fly debug camera) we - // fall back to the camera trigger. - bool playerInsideCell = (_playerMode && _playerController is not null) - ? _cellVisibility.IsInsideAnyCell(_playerController.Position) - : cameraInsideCell; + // Stage 3 (2026-06-02): replace the IsInsideAnyCell AABB scan with the + // seen_outside-derived predicate. Retail CellManager::ChangePosition (0x004559B0) + // gates sun/lighting off seen_outside on the player's current cell, NOT off an + // 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; // Phase C.1: tick retail PhysicsScript particle hooks. Named // retail decomp confirms SkyObject.PesObjectId is copied by @@ -7264,7 +7270,14 @@ public sealed class GameWindow : IDisposable // cylinder 0x01004C42/0x01004C44) need to overlay terrain and // entities to look volumetric — see the post-scene RenderWeather // call further below. - bool renderSky = !cameraInsideCell; + // Stage 3 (2026-06-02): sky gate uses seen_outside per retail RenderNormalMode:92649. + // Outdoor root (cameraInsideCell=false): always render sky. + // Building interior (cameraInsideCell=true, rootSeenOutside=true): render sky — + // it draws full-screen here until Stage 4 clips it to the doorway via OutsideView. + // Sealed dungeon (cameraInsideCell=true, rootSeenOutside=false): no sky. + // NOTE: interim regression until Stage 4 — sky draws full-screen in building interiors. + // This is expected per the EXECUTION POLICY; do NOT add a workaround gate. + bool renderSky = !cameraInsideCell || rootSeenOutside; if (renderSky) { _skyRenderer?.RenderSky(camera, camPos, (float)WorldTime.DayFraction, @@ -7502,8 +7515,10 @@ public sealed class GameWindow : IDisposable // instead of being painted over by them. This is the second // half of retail's LScape::draw split — GameSky::Draw(1) // fires after the DrawBlock loop. Same indoor gate as the - // sky pass: weather is suppressed inside cells. - if (!cameraInsideCell) + // sky pass: weather follows renderSky (seen_outside policy, + // Stage 3: suppressed in sealed dungeons, visible in building + // interiors through exit portals, always visible outdoors). + if (renderSky) { _skyRenderer?.RenderWeather(camera, camPos, (float)WorldTime.DayFraction, _activeDayGroup, kf, environOverrideActive);