acdream/src/AcDream.Core/Lighting/LightSource.cs
Erik d8984e877f fix #176: light pool tracked the camera via flood scoping - collect residents, anchor at player
The seam-floor purple flicker was NOT a draw z-fight. The in-engine
[seam-*] probe (ACDREAM_PROBE_SEAMDRAW - built because RenderDoc cannot
capture this pipeline: it hides GL_ARB_bindless_texture and the
mandatory-modern startup gate throws; AMD GPU rules out Nsight) killed
every double-draw suspect: ONE shell instance per seam cell at the
lifted z, no floor-coincident entity (portal entities sit at z=-12.05),
zero portal depth fans in the sealed Hub. What it caught instead: the
corridor floor's applied light set flipping wholesale with the flood.

Root cause: c500912b scoped BuildPointLightSnapshot by the per-frame
portal flood, on the research doc's gloss of CEnvCell::visible_cell_table
as "the portal-flood visible set". The named decomp refutes the gloss:
add_visible_cell (0x0052de40) DBObj-LOADS absent cells and inserts them;
a cell activation adds itself + its whole dat visible-cell list
(0x0052e228/0x0052e24a); entries leave only via the flush machinery.
It is the RESIDENT-cell registry - gaze can never remove a cell.
add_dynamic_lights (0x0052d410) walks the WHOLE table per frame
(caller 0x00452d30), and insert_light (0x0054d1b0) caps the pool by
distance to Render::player_pos (0x0054d1dd). Retail's pool is a function
of player position only. Ours followed the camera: turning changed the
flood (probe: 8..41 cells across one turn), the six intensity-100
under-room portal purples entered/left the pool, and the wedge blinked.

Fix: BuildPointLightSnapshot(playerWorldPos) collects ALL registered
(=resident) lit lights; over cap keeps dynamics FIRST (retail's separate
7-slot dynamic pool never competes with statics) then nearest-the-player;
the RebuildScopedLights callback is deleted. Live-verified with the probe:
full-circle turn, flood churning 8..41, the floor set held the same 8
identities on every post-spawn frame. The purple wedge SHAPE stays - it
is cdb-proven retail-faithful.

Residual deviation (AP-85 rewritten): single 128 pool vs retail's
7-dynamic/40-static degrade-scaled dual pools - the Hub now shows
7 purples + viewer where retail's cdb showed 4 + viewer + fixture slots;
if the gate reads the wedge as too purple, the A7 dual-pool cap is the
faithful trim.

Pins: PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant
(rewritten to the corrected model),
PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics,
PointSnapshot_OverCap_KeepsNearestThePlayer,
PointSnapshot_ResidentCollection_CellTagDoesNotFilter.
Suites: Core 2599+2skip / App 726+2skip / UI 425 / Net 385.

The [seam-*] probes stay until the visual gate passes, then strip.
Correction banner added to 2026-07-06-a7-per-cell-lighting-pseudocode.md;
outcome banner on the z-fight handoff; ISSUES #176 updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:12:31 +02:00

71 lines
3 KiB
C#
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.

using System;
using System.Numerics;
namespace AcDream.Core.Lighting;
/// <summary>
/// Retail light-source kinds (r13 §12.1).
/// </summary>
public enum LightKind
{
Directional = 0, // sun, moon — no position, infinite range
Point = 1, // torch, fireplace, spell aura
Spot = 2, // cone-shaped (rare in AC, used for a few specific lamps)
}
/// <summary>
/// Per-frame light record. Used by <see cref="LightManager"/> and fed to
/// the shader UBO on every draw call.
///
/// <para>
/// Retail semantics (r13 §10.2):
/// <list type="bullet">
/// <item><description>
/// Hard cutoff at <see cref="Range"/> — no smoothstep, no distance
/// attenuation inside the range. "Crisp bubble of illumination."
/// </description></item>
/// <item><description>
/// Max 8 active lights (<see cref="LightManager.MaxActiveLights"/>),
/// ranked by distance-to-viewer. Slot 0 is reserved for the sun.
/// </description></item>
/// <item><description>
/// <see cref="IsLit"/> latches the <c>SetLightHook</c> value so
/// animations can toggle a light on/off (torch being lit, light
/// crystal activating).
/// </description></item>
/// </list>
/// </para>
/// </summary>
public sealed class LightSource
{
public LightKind Kind;
public Vector3 WorldPosition;
public Vector3 WorldForward; // for Spot/Directional
public Vector3 ColorLinear = Vector3.One; // R,G,B in [0,1], pre-brightness
public float Intensity = 1f;
public float Range = 10f; // metres, hard cutoff
public float ConeAngle = 0f; // radians, Spot only
public uint OwnerId; // attached entity id; 0 = world-global
public uint CellId; // owning cell id (0xLLLLNNNN); 0 = cell-less/global (viewer fill, sun).
// Retail carries this on the RenderLight (insert_light arg6, +0x6c) for the
// cross-cell block-offset distance math. #176 correction (2026-07-06): it is
// NOT a pool filter — retail collects from ALL resident cells
// (CEnvCell::visible_cell_table = the loaded-cell registry, not the flood);
// acdream keeps the tag for probes ([indoor-light]/[seam-*]) + future parity.
public bool IsLit = true; // SetLightHook latch
public bool IsDynamic; // #143: true = D3D hardware path (1/d att, range×1.5);
// false = static dat-baked bake (1/d³, range×1.3)
// Cached each frame by LightManager.
public float DistSq;
}
/// <summary>
/// Per-cell ambient + sun state. For outdoor cells this comes from the
/// R12 sky state; for indoor cells the EnvCell dat carries a per-cell
/// ambient override (r13 §3).
/// </summary>
public readonly record struct CellAmbientState(
Vector3 AmbientColor,
Vector3 SunColor,
Vector3 SunDirection);