fix(lighting): match retail indoor ambient (0.20 neutral, not 0.10/0.09/0.08 warm)

Indoor cells rendered "almost black" because the hardcoded ambient at
GameWindow.cs:8342-8345 was an early-2026 guess (0.10, 0.09, 0.08 — half
retail brightness, warm-tinted) rather than the retail value. The named
retail decomp (acclient.pdb, Sept 2013 EoR build) shows
CellManager::ChangePosition @ 0x004559B0 calls
SmartBox::SetWorldAmbientLight(0.2f, 0xFFFFFFFF) whenever the player's
CObjCell::seen_outside flag is 0 — a flat 0.20 white floor, not a
dungeon-tone warm color.

Investigation also confirmed:
- EnvCell.dat does NOT carry inline lights — CEnvCell::UnPack reads
  numVisibleCells where Binary Ninja's heuristic decomp inferred
  "num_lights". Retail's CObjCell.light_list is populated at runtime via
  add_light() calls from neighbouring cell light registrations + per-cell
  static-object Setup.Lights, NOT from the dat byte stream.
- Setup.Lights from indoor static objects (entity.SourceGfxObjOrSetupId
  prefix 0x02xxxxxx) DO flow through LightInfoLoader.Load (line 5765)
  and reach LightManager via LightingHookSink. The wire is intact; the
  per-frame Tick + UBO upload chain (line 6865-6867) is intact.
- Retail's particle system does NOT emit lights from particles themselves.
  The light comes from the owning Setup's LightInfo records.

Pre-existing failures in DispatcherToMovementIntegrationTests, BSPStepUpTests,
and MotionInterpreterTests are on the branch already and unrelated to this
change (verified by stashing + retesting).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-19 10:14:25 +02:00
parent 67e64c79cf
commit a54cd7bef6
2 changed files with 11 additions and 4 deletions

View file

@ -46,7 +46,7 @@ public partial class LightInfo : IDatObjType {
- **Practical consequence.** For indoor cells, retail sets directional sun to zero (the cell is windowless) and relies on the baked vertex colours for the ambient "floor". Any `LightInfo` inside the cell is additive.
- **No cell has a separate ambient RGB field.** The only global ambient is `SkyTimeOfDay.AmbColor` / `AmbBright`, which is only applied outdoors.
- **acdream action.** We need a `CellAmbientState` that holds the current `AmbColor * AmbBright` (outdoors, driven by sky dat) or a fixed dark RGB like `(0.10, 0.09, 0.08)` (indoors, approximating the dungeon "deep" tone) — then add active lights on top. See §12 for the C# class.
- **acdream action.** We need a `CellAmbientState` that holds the current `AmbColor * AmbBright` (outdoors, driven by sky dat) or **a flat `(0.20, 0.20, 0.20)` neutral** (indoors) — then add active lights on top. The indoor constant is taken **directly from retail**: `CellManager::ChangePosition` (0x004559B0) calls `SmartBox::SetWorldAmbientLight(0.2f, 0xFFFFFFFF)` whenever `CObjCell::seen_outside == 0`. The early-2026 guess at `(0.10, 0.09, 0.08)` was eyeballed; the retail value is both brighter and neutral. See §12 for the C# class.
## 4. Torch lights and `WeenieType.LightSource`

View file

@ -8319,7 +8319,13 @@ public sealed class GameWindow : IDisposable
/// Derive the current sun (directional light, slot 0 of the UBO)
/// from the interpolated <see cref="AcDream.Core.World.SkyKeyframe"/>,
/// plus the cell ambient. Indoor cells force the sun intensity to
/// zero (r13 §13.7) and substitute a fixed dungeon-tone ambient.
/// zero and substitute a flat 0.2 white ambient — exact retail
/// behavior per <c>CellManager::ChangePosition</c> @ 0x004559B0,
/// which calls <c>SmartBox::SetWorldAmbientLight(0.2f, 0xFFFFFFFF)</c>
/// when the player's <c>CObjCell::seen_outside</c> flag is 0.
/// 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"/>).
/// </summary>
private void UpdateSunFromSky(AcDream.Core.World.SkyKeyframe kf, bool cameraInsideCell)
{
@ -8330,7 +8336,8 @@ public sealed class GameWindow : IDisposable
if (cameraInsideCell)
{
// Dungeon default per r13 §3 — warm-dark ambient, no sun.
// Indoor default — retail's flat 0.2 neutral ambient, sun
// zeroed. See xref to retail decomp in the doc comment above.
Lighting.Sun = new AcDream.Core.Lighting.LightSource
{
Kind = AcDream.Core.Lighting.LightKind.Directional,
@ -8340,7 +8347,7 @@ public sealed class GameWindow : IDisposable
Range = 1f,
};
Lighting.CurrentAmbient = new AcDream.Core.Lighting.CellAmbientState(
AmbientColor: new System.Numerics.Vector3(0.10f, 0.09f, 0.08f),
AmbientColor: new System.Numerics.Vector3(0.20f, 0.20f, 0.20f),
SunColor: System.Numerics.Vector3.Zero,
SunDirection: sunToWorld);
}