fix(lighting): trigger indoor ambient on PLAYER cell, not camera cell

User report: third-person chase camera enters interiors before the
player body does, so the camera-based cameraInsideCell flag was
flipping the scene to indoor lighting prematurely (ambient drops to
0.2 white before the player has actually crossed the doorway).

Retail keys lighting off the PLAYER's cell. CellManager::ChangePosition
@ 0x004559B0 reads CObjCell::seen_outside on the player's current
cell — never on the camera. Match that semantics.

- CellVisibility.IsInsideAnyCell(Vector3): new non-caching brute-force
  scan that's safe to call alongside ComputeVisibility(cameraPos)
  without thrashing the camera cell cache.
- GameWindow render loop: derive playerInsideCell from the player's
  Position when in player mode, otherwise fall back to cameraInsideCell
  (orbit/fly debug camera).
- UpdateSunFromSky now takes playerInsideCell. The sky-render and
  depth-buffer-clear decisions still use cameraInsideCell — those are
  legitimately camera-POV concerns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-19 10:38:48 +02:00
parent a54cd7bef6
commit 1024ba34e0
2 changed files with 34 additions and 3 deletions

View file

@ -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
/// <see cref="AcDream.Core.Lighting.LightingHookSink"/>).
/// 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.
/// </summary>
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.