acdream/docs/research/2026-06-20-indoor-lighting-regime-handoff.md
Erik f7f3e0887b docs(lighting): indoor lighting regime handoff — file #142 (windowed-interior regime) + #143 (portal dynamic light)
Clean handoff for the next M1.5 "indoor world feels right" session, picking up
the two indoor-lighting gaps the user spotted at the #140 visual gate.

#142 (PRIMARY): windowed-building interiors + look-ins read "like outdoors".
Root cause grounded: retail's lighting regime is per-DRAW-STAGE (PView::DrawCells
draws ALL EnvCells in the useSunlightSet(0) interior stage — torch-lit, no sun,
regardless of SeenOutside), while acdream's is a per-FRAME global keyed on the
player's cell (playerInsideCell). So acdream's windowed interiors (SeenOutside)
+ look-ins stay in the outdoor regime. This is the AP-43 residual surfaced.
Fix direction: make sun+ambient per-draw like AP-43's torches (design fork laid
out for a brainstorm). Resolves AP-43.

#143 (SECONDARY): portal swirl casts no light. acdream registers only static
Setup.Lights; the portal is a retail DYNAMIC light (add_dynamic_light ->
minimize_envcell_lighting). Fix: register a dynamic LightSource for portals.

Handoff doc carries the verified retail decomp (useSunlightSet/PView::DrawCells
stages), current acdream line refs, the three gaps, the fix fork, validation
plan, and DO-NOT-RETRY. Neither issue is a regression from #140.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:17:59 +02:00

12 KiB
Raw Blame History

Indoor lighting regime — HANDOFF (#142 windowed-interior regime, #143 portal dynamic light)

Date: 2026-06-20 Base: main @ 31d7ffd (A7 #140 + all D.5 work; pushed to both remotes) Milestone: M1.5 "Indoor world feels right" Start with: #142 (issue #1). Predecessor: docs/research/2026-06-19-lighting-a7-fixD-round2-torch-reach-CHECKPOINT.md (RESOLVED banner — the #140 outdoor fix). Companion: claude-memory/reference_retail_ambient_values.md.

Where we are

#140 (outdoor building over-bright near torches) is SHIPPED + user-confirmed + merged + pushed. Real cause: retail lights outdoor objects with SUN + ambient only, never torches (the useSunlight gate); fix = gate per-object torch selection on the object being indoor (IndoorObjectReceivesTorches, WbDrawDispatcher.cs). Register row AP-43.

At the #140 visual gate the user spotted two INDOOR-lighting gaps (the opposite problem — interiors too DARK / "like outdoors"). Both are this handoff. Neither is a regression from #140 — that fix only subtracts torch light from outdoor objects.

The unifying insight (read this first)

acdream's lighting REGIME (sun on/off + which ambient) is a per-FRAME global keyed on whether the PLAYER is in a sealed cell. Retail's is per-DRAW-STAGE: the outdoor stage runs with the sun on, the interior-cell stage runs with the sun off + torches on. #140 fixed the torch half of this mismatch per-object (AP-43). #142 is the SUN + AMBIENT half — i.e. the AP-43 residual, now surfaced as a visible bug. Finishing #142 lets us delete/narrow AP-43.


#142 (issue #1) — windowed-building interiors read "like outdoors" [PRIMARY]

Symptom (user, 2026-06-19, at the #140 gate)

"Agent of Arcanum house — in retail it is much brighter indoors; when looking into the house it is lit, same light when you walk in. In acdream it is NOT lit — looking in and when inside it feels the same like it is outdoors."

The meeting hall (a more sealed interior) looked OK — the user only flagged its portal (#143), not its walls. That contrast is the key clue (see "the three gaps").

Retail mechanism (VERIFIED — read verbatim this session)

PView::DrawCells (0x005a4840) draws a frame in two ordered stages:

  1. Outside stage: useSunlightSet(1) (0x005a485a) → LScape::draw → outdoor terrain/buildings/ objects, sun on, torches skipped (the #140 mechanism).
  2. Interior stage: useSunlightSet(0) (0x005a49f3) → restore_all_lighting → loop over every EnvCell in cell_draw_listDrawEnvCell (0x0059f170): walls baked (SetStaticLightingVertexColors 0x0059cfe0), objects torch-lit (minimize_object_lighting 0x0054d480, enabled because useSunlight==0 per DrawMeshInternal 0x0059f398), NO sun.
  3. useSunlightSet(1) (0x005a4b5d) restores outdoor mode at the very end.

useSunlightSet(arg) (0x0054d450): sets useSunlight=arg; arg==1 enables the SUN as the active hardware light, arg==0 enables none (sun off).

KEY FACT: cell_draw_list holds ALL visible EnvCells — windowed (SeenOutside) and sealed. Retail draws every interior in the useSunlight==0 stage. The regime is per-stage, never per- building / per-SeenOutside. So retail torch-lights every building interior, including windowed ones and look-ins viewed from outside.

acdream current state (per-FRAME global) — current line refs (@31d7ffd)

  • GameWindow.cs:8061 playerSeenOutside = playerRoot?.SeenOutside ?? true — the PLAYER cell's flag.
  • GameWindow.cs:8107 playerInsideCell = playerRoot is not null && !playerSeenOutside.
  • GameWindow.cs:8122 UpdateSunFromSky(kf, playerInsideCell) → (:10786) sets the global sun + ambient: inside → sun Intensity=0 + flat (0.2,0.2,0.2) ambient; outside → keyframe sun + outdoor ambient.
  • That ambient is uploaded ONCE per frame to the SceneLighting UBO (CurrentAmbient.AmbientColor, :8171) and read by BOTH mode-0 (objects) and mode-1 (EnvCell shells) in mesh_modern.vert.
  • Torches are ALREADY per-cell (AP-43: IndoorObjectReceivesTorches WbDrawDispatcher.cs:2076, used at :2057; plus EnvCellRenderer SelectForObject) — independent of playerInsideCell. So the torch half is fine; only the SUN + AMBIENT are still per-frame-global.

The three gaps (all one root: per-frame-global vs per-stage)

  1. Player OUTSIDE, looking INTO any building (look-in): playerSeenOutside=true → outdoor regime → the look-in interior gets sun + outdoor ambient. Retail draws look-in cells in the useSunlight=0 stage (torch-lit). → "when looking in, not lit."
  2. Player INSIDE a WINDOWED building (SeenOutside=true cells, e.g. Agent of Arcanum): playerInsideCell=false → outdoor regime → interior gets sun + outdoor ambient. Retail: useSunlight=0, torch-lit. → "when inside, feels like outdoors."
  3. Player INSIDE a SEALED building / dungeon (SeenOutside=false): playerInsideCell=true → indoor regime → MATCHES retail. ✓ (the meeting hall + dungeons — why they looked right.)

Cheap validation FIRST (before any code)

  • Confirm the windowed-vs-sealed split is the discriminator. Verify the Agent of Arcanum is a WINDOWED building (its EnvCells' SeenOutside=true) and the meeting hall is sealed. Dat flag: EnvCellFlags.SeenOutside (hydrated to ObjCell.SeenOutside; see EnvCell.cs / PhysicsDataCache.cs). We did NOT pin the Agent of Arcanum's landblock this session — either have the user point at it in game ([B.4b] pick line names clicked objects), or extend HoltburgTorchFalloffProbeTests to dump SeenOutside per EnvCell across the Holtburg landblocks and find the windowed buildings.
  • ACDREAM_PROBE_LIGHT=1 ([light] line logs insideCell / ambient / sun) while standing inside the Agent of Arcanum vs the meeting hall — confirms each gets the regime predicted above.

Fix direction (BRAINSTORM this — it is a design fork, not a mechanical port)

Make the SUN + AMBIENT per-draw-context, mirroring AP-43's per-object torch decision. The renderer is batched bindless-MDI, so a per-stage global won't work across mixed batches — per-object is the natural fit (exact same reasoning that put AP-43 per-object; see the #140 explanation). An object/cell is "indoor" iff its ParentCellId is an EnvCell (reuse IndoorObjectReceivesTorches). Then:

  • Indoor draws (mode-1 EnvCell shells; mode-0 objects with EnvCell ParentCellId): SKIP the sun + use the indoor ambient (flat (0.2,0.2,0.2) / retail indoor). (mode-1 already skips the sun; it just needs the indoor ambient. mode-0 indoor objects currently ADD the sun — gate it off.)
  • Outdoor draws: sun + outdoor ambient (as today).

Open design questions for the brainstorm:

  • The shader needs BOTH ambients (indoor + outdoor) + a per-instance "indoor" selector. Options: (a) add an indoorAmbient to the SceneLighting UBO + a per-instance indoor bit (a tiny SSBO like the light-set, or pack into an existing per-instance field); (b) add a third uLightingMode (e.g. 2 = indoor object: no sun, indoor ambient, torches); (c) compute both and select.
  • UpdateSunFromSky must stop branching on playerInsideCell and instead provide BOTH regimes every frame (outdoor sun + outdoor ambient AND the indoor flat ambient), so the shader picks per object.
  • Verify retail's indoor ambient (the restore_all_lighting path + the per-EnvCell ambient): is it the flat (0.2,0.2,0.2) we use, or the cell's own authored ambient? Cross-check before locking it.

This work RESOLVES the AP-43 residual (regime becomes per-draw → no doorway/look-in mismatch). Update/delete AP-43 in the same commit.

Files

  • GameWindow.cs: :8061/:8107 (playerInsideCell), :8122 + :10786 UpdateSunFromSky (the regime source), :8171 (ambient → UBO).
  • src/AcDream.App/Rendering/Shaders/mesh_modern.vert: accumulateLights (sun loop under if (uLightingMode==0) ~:193; ambient uCellAmbient.xyz ~:188). The sun gate + ambient selection live here.
  • WbDrawDispatcher.cs: IndoorObjectReceivesTorches (:2076) — the indoor predicate to reuse; ComputeEntityLightSet (:2057).
  • EnvCellRenderer.cs: mode-1 draws (uLightingMode=1) — need the indoor ambient.
  • LightManager / the SceneLighting UBO layout (GlobalLightPacker is the binding-4 helper) — where a second ambient + the indoor selector would go.

#143 (issue #2) — portal swirl doesn't light the room [SECONDARY]

Symptom

Inside the meeting hall, retail's portal swirl visibly tints/lights the room; acdream's portal lights nothing.

Retail mechanism

The portal swirl is a DYNAMIC light. add_dynamic_light (0x0054d420) → insert_light (0x0054d1b0) → world_lights.dynamic_lights. minimize_envcell_lighting (0x0054c170) enables the cell's DYNAMIC subset (class 2) as hardware lights → tints the EnvCell walls; minimize_object_lighting (0x0054d480) enables dynamics for objects in the cell too. Captured params (predecessor cdb, tools/cdb/a7-fixd-*.cdb): the Holtburg portal dynamic light = intensity=100, falloff=6, color=(0.784, 0, 0.784) (magenta/purple).

acdream gap

acdream registers ONLY static Setup.Lights (GameWindow.cs ~:6404 RegisterOwnedLight). It registers no dynamic lights — the portal entity casts no light. (GpuWorldState.cs:101 even mentions "unregistering dynamic lights" but none are ever registered.)

Fix approach

Register a dynamic LightSource for portal-swirl entities at their world position with the retail params (or read the portal model's own dat Setup.Lights if it carries one — check the portal GfxObj/ Setup first). It then flows through the existing point-light path (LightManager.PointSnapshotSelectForObject → shader), lighting nearby EnvCell walls + indoor objects. It is a POINT light, lives INSIDE a cell → it must light via the indoor path (the EnvCell bake SelectForObject already picks any registered point light near a cell, so registering it may "just work" once it has a LightSource). Find where portal swirls spawn in acdream (the particle/portal emitter spawn path) and attach the light there; unregister on despawn (UnregisterByOwner). Keep it OUT of the AP-43 outdoor-object gate (it's indoor). Decomp anchors: add_dynamic_light 0x0054d420, minimize_envcell_lighting 0x0054c170, insert_light 0x0054d1b0.


Decomp anchors (quick reference)

useSunlightSet 0x0054d450 · useSunlight gate DrawMeshInternal 0x0059f398 · PView::DrawCells 0x005a4840 (useSunlightSet(1) 0x005a485a / useSunlightSet(0) 0x005a49f3 / useSunlightSet(1) 0x005a4b5d) · DrawEnvCell 0x0059f170 · SetStaticLightingVertexColors 0x0059cfe0 · calc_point_light 0x0059c8b0 (range = falloff × static_light_factor 1.3 @ 0x00820e24) · minimize_object_lighting 0x0054d480 · minimize_envcell_lighting 0x0054c170 · add_dynamic_light 0x0054d420 · insert_light 0x0054d1b0 · config_hardware_light 0x0059ad30 (rangeAdjust 1.5 @ 0x00820cc4 — the dynamic/object hardware path).

DO-NOT-RETRY / gotchas

  • The OUTDOOR torch gate (#140 / AP-43) is correct + user-confirmed — don't touch it.
  • Don't shorten Falloff × 1.3 — acdream reads the dat falloffs faithfully (the reach is correct).
  • The regime is a per-FRAME global; the fix is to make sun+ambient per-DRAW (per-object/cell), mirroring AP-43's torch decision — NOT to split into separate render passes (fights the batched MDI; the per-object route is why AP-43 exists).
  • Line numbers above are @31d7ffd and WILL drift — re-grep playerInsideCell / UpdateSunFromSky / IndoorObjectReceivesTorches before editing.

Verification (the acceptance gate)

Visual side-by-side vs retail at the Agent of Arcanum (looking IN from outside + walking IN) and the meeting-hall portal. Expected after #142: interiors are torch-lit/warm both looking-in and inside; windowed buildings no longer "feel like outdoors." After #143: the portal swirl tints the room.

Pointers

  • Register: AP-43 (docs/architecture/retail-divergence-register.md) — the residual this work resolves.
  • claude-memory/reference_retail_ambient_values.md — cdb values incl. the portal dynamic-light capture + the indoor/outdoor ambient numbers.
  • claude-memory/project_render_pipeline_digest.md — per-cell light + look-in (#124) + flap context.
  • #140 CHECKPOINT (above) — the full outdoor-torch story + the verified useSunlight decomp.