From a54cd7bef6e04cbb102143092aaf790cce08836d Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 19 May 2026 10:14:25 +0200 Subject: [PATCH] fix(lighting): match retail indoor ambient (0.20 neutral, not 0.10/0.09/0.08 warm) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/research/deepdives/r13-dynamic-lighting.md | 2 +- src/AcDream.App/Rendering/GameWindow.cs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/research/deepdives/r13-dynamic-lighting.md b/docs/research/deepdives/r13-dynamic-lighting.md index 91067d1..b1321a5 100644 --- a/docs/research/deepdives/r13-dynamic-lighting.md +++ b/docs/research/deepdives/r13-dynamic-lighting.md @@ -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` diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index ff777f8..6f87a61 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -8319,7 +8319,13 @@ public sealed class GameWindow : IDisposable /// Derive the current sun (directional light, slot 0 of the UBO) /// from the interpolated , /// 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 CellManager::ChangePosition @ 0x004559B0, + /// which calls SmartBox::SetWorldAmbientLight(0.2f, 0xFFFFFFFF) + /// when the player's CObjCell::seen_outside flag is 0. + /// Indoor brightness then comes from per-cell point lights + /// (Setup.Lights on the cell's static objects, registered through + /// ). /// 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); }