diff --git a/src/AcDream.App/Rendering/CellVisibility.cs b/src/AcDream.App/Rendering/CellVisibility.cs index f3e0c55..bcca4ec 100644 --- a/src/AcDream.App/Rendering/CellVisibility.cs +++ b/src/AcDream.App/Rendering/CellVisibility.cs @@ -330,6 +330,21 @@ public sealed class CellVisibility local.Z <= cell.LocalBoundsMax.Z + PointInCellEpsilon; } + /// + /// Brute-force scan of every loaded cell to test whether + /// is inside any of them. Does not touch + /// the camera cache (), so this is safe + /// to call alongside in the same frame + /// for a different position (e.g. player position when the camera is + /// in third-person chase mode). + /// + public bool IsInsideAnyCell(Vector3 worldPoint) + { + foreach (var cell in _cellLookup.Values) + if (PointInCell(worldPoint, cell)) return true; + return false; + } + // ------------------------------------------------------------------ // GetVisibleCells (BFS) // ------------------------------------------------------------------ diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 6f87a61..13a660c 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -6848,6 +6848,19 @@ public sealed class GameWindow : IDisposable var visibility = _cellVisibility.ComputeVisibility(camPos); bool cameraInsideCell = visibility?.CameraCell is not null; + // 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; + // Phase C.1: tick retail PhysicsScript particle hooks. Named // retail decomp confirms SkyObject.PesObjectId is copied by // SkyDesc::GetSky but ignored by GameSky, so the sky-PES path is @@ -6861,7 +6874,7 @@ public sealed class GameWindow : IDisposable // the scene-lighting UBO once per frame. Every shader that // consumes binding=1 reads the same data for the rest of the // frame — terrain, static mesh, instanced mesh, sky. - UpdateSunFromSky(kf, cameraInsideCell); + UpdateSunFromSky(kf, playerInsideCell); Lighting.Tick(camPos); var ubo = AcDream.Core.Lighting.SceneLightingUbo.Build( Lighting, in atmo, camPos, (float)WorldTime.DayFraction); @@ -8326,15 +8339,18 @@ public sealed class GameWindow : IDisposable /// Indoor brightness then comes from per-cell point lights /// (Setup.Lights on the cell's static objects, registered through /// ). + /// The trigger is the PLAYER's cell, not the camera's — third-person + /// chase camera enters interiors before the player body does, and + /// retail keys lighting off the player position. /// - private void UpdateSunFromSky(AcDream.Core.World.SkyKeyframe kf, bool cameraInsideCell) + private void UpdateSunFromSky(AcDream.Core.World.SkyKeyframe kf, bool playerInsideCell) { // Sun direction: points FROM the sun TOWARDS the world. Our // shader does dot(N, -forward) so a positive N·L means the // surface faces the sun. var sunToWorld = -AcDream.Core.World.SkyStateProvider.SunDirectionFromKeyframe(kf); - if (cameraInsideCell) + if (playerInsideCell) { // Indoor default — retail's flat 0.2 neutral ambient, sun // zeroed. See xref to retail decomp in the doc comment above.