Port retail's per-frame light collection: the point-light pool is built from ONLY the currently-visible cells' lights, matching CObjCell::add_*_to_global_lights (0x0052b350/0x0052b390) walked over CEnvCell::visible_cell_table (0x0052d410) — not a flat world-space set capped at 128-nearest-camera. - LightSource.CellId (retail insert_light arg6 -> RenderLight +0x6c); tagged at both registration sites from entity.ParentCellId (live weenie fixtures + dat EnvCell statics). - LightManager.BuildPointLightSnapshot(camPos, visibleCells): a light joins the pool iff CellId==0 (viewer/global) or its cell is in the flood. 128 cap kept as a now-non-biting backstop (retail's is 40 static + 7 dynamic, 0x0081ec94/8). - Threaded via RetailPViewDrawContext.RebuildScopedLights, invoked in DrawInside after the flood resolves prepareCells and before the draws (renderers select from the same in-place-rebuilt PointSnapshot; EnvCellRenderer clears its per-cell cache each pass). - [indoor-light] probe (ACDREAM_PROBE_INDOOR_LIGHT=1) dumps the scoped-pool SET COMPOSITION. Un-skips LightManagerTests.PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant. CORRECTION: the handoff called the camera-cap the "confirmed" #176/#177 mechanism. The probe PROVES scoping works (291 Hub fixtures -> pool of 1-9, ~285 through-floor lights dropped/frame, CellIds match the flood), but the user's VISUAL GATE showed BOTH symptoms unchanged. So pool composition is NOT the cause. #176 real cause = an over-bright purple point light (intensity=100, color 0.784,0,0.784 -- from [light-detail]); #177 = a portal-visibility miss (stairs not drawn looking back). Both stay OPEN. This change is retail-faithful and retires the camera-eviction latent bug; kept as such, not as the symptom fix. Register AP-85 corrected; ISSUES #176/#177 re-diagnosed; render digest banner updated. Decomp: insert_light 0x0054d1b0, minimize_object_lighting 0x0054d480, calc_point_light 0x0059c8b0; pseudocode docs/research/2026-07-06-a7-per-cell-lighting-pseudocode.md. Suites green: Core 2595 + 2 skip, App 719 + 2 skip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
4.6 KiB
C#
102 lines
4.6 KiB
C#
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.Core.Lighting;
|
|
|
|
/// <summary>
|
|
/// Converts a <see cref="Setup"/>'s <c>Lights</c> dictionary (dat-level
|
|
/// <see cref="LightInfo"/> records) into runtime <see cref="LightSource"/>
|
|
/// instances the <see cref="LightManager"/> can consume.
|
|
///
|
|
/// <para>
|
|
/// Retail <see cref="LightInfo"/> fields (r13 §1):
|
|
/// <list type="bullet">
|
|
/// <item><description><c>ViewSpaceLocation</c>: local Frame relative to the owning part.</description></item>
|
|
/// <item><description><c>Color</c>: packed ARGB. Alpha is ignored; channels go through <c>/255</c>.</description></item>
|
|
/// <item><description><c>Intensity</c>: multiplies color for final diffuse.</description></item>
|
|
/// <item><description><c>Falloff</c>: world metres — acts as the <see cref="LightSource.Range"/> hard cutoff.</description></item>
|
|
/// <item><description><c>ConeAngle</c>: radians; 0 = point, >0 = spot cone.</description></item>
|
|
/// </list>
|
|
/// </para>
|
|
/// </summary>
|
|
public static class LightInfoLoader
|
|
{
|
|
/// <summary>
|
|
/// Extract all lights from a Setup, positioned in the entity's
|
|
/// world frame (via <paramref name="entityPosition"/> +
|
|
/// <paramref name="entityRotation"/>). The dat's per-light Frame is
|
|
/// treated as a local offset relative to the entity root; acdream
|
|
/// doesn't yet transform through the animated part chain (retail's
|
|
/// hand-held torches), so held lights render at the entity root
|
|
/// until the animation hook layer handles per-part placement.
|
|
/// </summary>
|
|
public static IReadOnlyList<LightSource> Load(
|
|
Setup setup,
|
|
uint ownerId,
|
|
Vector3 entityPosition,
|
|
Quaternion entityRotation,
|
|
bool isDynamic = false,
|
|
uint cellId = 0)
|
|
{
|
|
var results = new List<LightSource>();
|
|
if (setup?.Lights is null || setup.Lights.Count == 0) return results;
|
|
|
|
foreach (var kvp in setup.Lights)
|
|
{
|
|
var info = kvp.Value;
|
|
if (info is null) continue;
|
|
|
|
// Local Frame offset into world space.
|
|
Vector3 localOffset = Vector3.Zero;
|
|
Quaternion localRot = Quaternion.Identity;
|
|
if (info.ViewSpaceLocation is not null)
|
|
{
|
|
localOffset = new Vector3(
|
|
info.ViewSpaceLocation.Origin.X,
|
|
info.ViewSpaceLocation.Origin.Y,
|
|
info.ViewSpaceLocation.Origin.Z);
|
|
localRot = new Quaternion(
|
|
info.ViewSpaceLocation.Orientation.X,
|
|
info.ViewSpaceLocation.Orientation.Y,
|
|
info.ViewSpaceLocation.Orientation.Z,
|
|
info.ViewSpaceLocation.Orientation.W);
|
|
}
|
|
|
|
// Transform local offset into world space via the entity's
|
|
// rotation + translation. No per-part chain yet — held
|
|
// torches track the entity's root for now.
|
|
Vector3 worldPos = entityPosition + Vector3.Transform(localOffset, entityRotation);
|
|
Quaternion worldRot = entityRotation * localRot;
|
|
Vector3 forward = Vector3.Transform(Vector3.UnitY, worldRot);
|
|
|
|
var light = new LightSource
|
|
{
|
|
Kind = info.ConeAngle > 0f ? LightKind.Spot : LightKind.Point,
|
|
WorldPosition = worldPos,
|
|
WorldForward = forward,
|
|
ColorLinear = new Vector3(
|
|
(info.Color?.Red ?? 255) / 255f,
|
|
(info.Color?.Green ?? 255) / 255f,
|
|
(info.Color?.Blue ?? 255) / 255f),
|
|
Intensity = info.Intensity,
|
|
// Range factor by light path (#143). STATIC dat-baked lights use
|
|
// static_light_factor 1.3 (calc_point_light 0x0059c8b0 bake — the path
|
|
// that lights dungeon/house WALLS, #133 A7) and the shader's 1/d³ ramp.
|
|
// DYNAMIC lights (server-object/portal lights registered from the live
|
|
// spawn path with isDynamic:true) use config_hardware_light's rangeAdjust
|
|
// 1.5 (0x00820cc4) and the shader's D3D 1/d attenuation — softer, broader.
|
|
Range = info.Falloff * (isDynamic ? 1.5f : 1.3f),
|
|
ConeAngle = info.ConeAngle,
|
|
OwnerId = ownerId,
|
|
CellId = cellId, // owning cell — scopes the per-frame visible-cell pool (A7 #176/#177)
|
|
IsLit = true,
|
|
IsDynamic = isDynamic,
|
|
};
|
|
results.Add(light);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
}
|