using System.Collections.Generic; using System.Numerics; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; namespace AcDream.Core.Lighting; /// /// Converts a 's Lights dictionary (dat-level /// records) into runtime /// instances the can consume. /// /// /// Retail fields (r13 §1): /// /// ViewSpaceLocation: local Frame relative to the owning part. /// Color: packed ARGB. Alpha is ignored; channels go through /255. /// Intensity: multiplies color for final diffuse. /// Falloff: world metres — acts as the hard cutoff. /// ConeAngle: radians; 0 = point, >0 = spot cone. /// /// /// public static class LightInfoLoader { /// /// Extract all lights from a Setup, positioned in the entity's /// world frame (via + /// ). 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. /// public static IReadOnlyList Load( Setup setup, uint ownerId, Vector3 entityPosition, Quaternion entityRotation, bool isDynamic = false, uint cellId = 0) { var results = new List(); 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; } }