The whole "indoor interiors read dark/flat vs retail" saga was ONE root-cause bug: EnvCellRenderer.GetCellLightSet derived the landblock key as `cellId & 0xFFFF0000` (0xXXYY0000), but landblocks are keyed by the streaming id 0xXXYYFFFF. The lookup missed for EVERY cell, so SelectForObject never ran and every EnvCell wall received ZERO point lights — torches, lanterns, the viewer light, all of it. Confirmed by a [cell-light] probe: inBounds=False selected=0 across 1M+ cell draws; after the fix inBounds=True selected=3-4. User-confirmed the interiors now look like retail. Three faithful additions that were blocked by the key bug (and only show now): - Viewer light (LightManager.UpdateViewerLight): retail's SmartBox::set_viewer (0x00452c40) adds a white fill light at the player every frame via add_dynamic_light — the dominant interior fill (no sun indoors). acdream had NO dynamic lights at all. Params from the cdb capture: intensity 2.25, falloff 10, white, offset (0,0,2). Indoor-only via the AP-43 gate. - Weenie fixture lights (OnLiveEntitySpawnedLocked): server-spawned lanterns/ braziers carry Setup.Lights but the dat-static registration never saw CreateObject entities. Register on spawn; unregister on despawn (UnregisterOwner made unconditional). Register row AP-44. - IndoorObjectReceivesTorches now excludes the 0xFFFF landblock marker (it is not an EnvCell) — fixes WbDrawDispatcherIndoorFlagTests.LandblockId_OutdoorFlag0 (a #142 verification miss). Divergence register: AP-44 (weenie light spawn-position, no movement tracking), AP-47 (acdream's 128-light/camera-independent cap keeps interiors always-lit vs retail's 40-nearest-to-player budget that pops in on approach — intentional, user-preferred). Investigation: docs/research/2026-06-20-indoor-torch-lantern-lighting-investigation.md Core 1505 / App 476 green. Visual gate: user-confirmed "looks like retail now." Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.9 KiB
A7 indoor torch/lantern lighting — why acdream interiors read dark (investigation)
Date: 2026-06-20 Mode: report-only investigation (no code changed)
Trigger: after #142 sun-gate (ef5049f) the Agent of Arcanum interior
(0xA9B40171/0172) still reads dark; user: "retail uses torches and lanterns
for lighting indoors."
Retail oracle — the static-light bake (decomp, confirmed)
SetStaticLightingVertexColors (0x0059cfe0), per EnvCell mesh, per vertex:
for each vertex V:
acc = (0,0,0) // from BLACK; no ambient/sun here
for i in 0 .. world_lights.num_static_lights: // ALL lights — NO per-vertex/cell cap
L = sorted_static_lights[i]
if L.type==0: calc_point_light(V, acc, L) // 0x0059c8b0
else: acc += dot(N,dir)*scale*L.color
acc = clamp(acc, 0, 1) // per-channel, AFTER summing all
V.diffuse = encode(acc)
calc_point_light (0x0059c8b0): range = falloff × static_light_factor(1.3);
back-face/range gated; angular = dot_n/dist (<1 m) else dot_n/(dist²·dist);
scale = angular·(1−dist/range)·intensity; per-channel cap min(scale·c, c).
Global cap: max_static_lights = 0x28 = 40 (insert_light 0x0054d1b0 keeps a
distance-sorted, replace-farthest list). So the bake sees up to 40 nearest static
lights — and sums ALL of them per vertex. There is no 8-light cap on the static
wall bake. (The 8-light cap is the D3D hardware path for dynamic objects,
minimize_object_lighting 0x0054d480 — a different path.)
acdream's torch path — three real divergences
| # | Divergence | Site | Effect |
|---|---|---|---|
| Bug A | Weenie fixture lights never registered. RegisterOwnedLight is called ONLY from ApplyLoadedTerrainLocked for dat-baked landblock + EnvCell statics (Setup 0x02xxxxxx, Lights.Count>0). Server CreateObject weenies (OnLiveEntitySpawnedLocked) never register lights. |
GameWindow.cs:6733-6758 (registers); GameWindow.cs:2696/3275 (weenie path — no register) |
If a lantern/torch is a server weenie, its light is entirely absent. |
| Bug B | Per-cell 8-light cap. GetCellLightSet→SelectForObject keeps the 8 nearest-to-cell-center lights (MaxLightsPerObject=8); 9th+ dropped. Retail sums up to 40. |
EnvCellRenderer.cs:1044; LightManager.cs:234-278 |
Dense cells (dungeon corridors, fixture clusters) lose lights. Not binding for the Arcanum (only ~3 in range). |
| Bug C | 128 global camera pre-filter. BuildPointLightSnapshot keeps the 128 nearest-to-camera lights (MaxGlobalLights=128), drops the rest before per-cell selection. Retail has no global per-frame cap on statics. |
LightManager.cs:179,208-211 |
registeredLights=129 ⇒ already at the cap; drops farthest-from-camera. Hurts far lights, not the near interior. |
Bug A·partial: the per-vertex bake is also approximated as one 8-light set per whole cell (uniform across all the cell's vertices), vs retail's true per-vertex bake — a fidelity gap (bright cluster smears across the cell), secondary to the source-count gaps above.
The per-light shader formula (pointContribution) + the min(pointAcc,1.0) torch
clamp match retail's calc_point_light + final saturate — magnitude per light
is faithful. The gap is missing / capped light SOURCES, not per-light math.
Ranked hypotheses (for the Agent of Arcanum specifically)
- H1 — the lanterns are server weenies ⇒ Bug A drops their light (best fit
for the "lanterns" hint). The cell shows only ~3 dat torches; lanterns absent.
Falsify: pick a lantern in-game — guid
0x80…/0x7…= weenie ⇒ confirmed. - H4 — per-cell (not per-vertex) bake / magnitude — if the Arcanum has no weenie fixtures, the 3 dat torches are all there is and the per-cell-uniform bake under-fills vs retail's per-vertex bake.
- H2/H3 (8-cap / 128-cap) — real retail divergences, but not binding for this building (≤3 lights in range, near camera). They matter for dense dungeons.
Fix plan (faithful, per hypothesis)
- H1 (likely): register
Setup.Lightsfor light-bearing weenies inOnLiveEntitySpawnedLocked, mirroringApplyLoadedTerrainLocked:6742-6758(guardsetup.Lights.Count>0); unregister on despawn (UnregisterByOwner). Faithful: retail registers object lights regardless of static/dynamic origin. - H2: raise the per-cell static bake cap from 8 toward retail's 40 (the cap is retail-correct only for the dynamic-object hardware path, not the wall bake).
- H3: make the per-cell selection independent of the 128 camera pre-filter for statics (retail's bake has no global count cap).
- H4: move the cell bake toward per-vertex (longer-horizon A7 LightBake work).
What this is NOT
NOT a #142 sun-gate regression and NOT an ambient bug (ambient matches retail; it's just dim rainy-twilight). It is missing / capped light sources.