acdream/docs/research/2026-06-20-indoor-torch-lantern-lighting-investigation.md
Erik 0d8b827721 fix(lighting): indoor interiors now lit — landblock-key bug + viewer light + weenie fixtures
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>
2026-06-20 15:51:55 +02:00

75 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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·(1dist/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)
1. **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.
2. **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.
3. **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.Lights` for light-bearing weenies in
`OnLiveEntitySpawnedLocked`, mirroring `ApplyLoadedTerrainLocked:6742-6758`
(guard `setup.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**.