feat(lighting): Phase G.2 LightSource + LightManager (data + selection)
Retail-faithful 8-light cap selection (r13 §12) — the fixed-function D3D pipeline's "hardware lights" constraint carried over to modern GL via UBO-per-draw. Core layer (AcDream.Core/Lighting): - LightSource: Kind (Directional/Point/Spot), WorldPosition, WorldForward, ColorLinear, Intensity, Range (hard cutoff), ConeAngle (spot), OwnerId (entity attachment), IsLit latch. - CellAmbientState: (AmbientColor, SunColor, SunDirection) sourced from R12 sky state for outdoor cells or EnvCell dat for indoor cells. - LightManager: Register/Unregister/UnregisterByOwner/Clear + Tick per frame. Selection matches r13 §12.2 exactly: 1) Skip unlit + directional. 2) Compute DistSq for every registered point/spot. 3) Drop lights outside Range² * 1.1 (10% slack prevents pop). 4) Sort by DistSq ascending; take up to 7 (slot 0 reserved for Sun). 5) Slot 0 = Sun (Directional); slots 1..7 = nearest in-range. Tests (9 new): - Register/Unregister/Idempotent register. - Tick picks top 8 by distance when 12 registered. - Range filter drops far lights (5.0 range, 20m away). - Range slack includes lights at exactly the boundary. - Sun reserved at slot 0 across ticks. - Unlit lights excluded; toggling IsLit brings them back. - UnregisterByOwner removes all owner's lights. - DistSq updated each tick for viewer movement. Build green, 596 tests pass (up from 587). Next: wire LightManager into the shader UBO pass (G.2 second commit) and feed Sun from WorldTimeService.CurrentSunDirection per frame. Ref: r13 §10.2 (D3D attenuation = none inside Range + hard cutoff), §12 (full port plan). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6850d716a2
commit
a28a69af71
3 changed files with 340 additions and 0 deletions
139
tests/AcDream.Core.Tests/Lighting/LightManagerTests.cs
Normal file
139
tests/AcDream.Core.Tests/Lighting/LightManagerTests.cs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Lighting;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Lighting;
|
||||
|
||||
public sealed class LightManagerTests
|
||||
{
|
||||
private static LightSource MakePoint(Vector3 pos, float range, uint ownerId = 0, bool lit = true)
|
||||
=> new LightSource
|
||||
{
|
||||
Kind = LightKind.Point,
|
||||
WorldPosition = pos,
|
||||
Range = range,
|
||||
IsLit = lit,
|
||||
OwnerId = ownerId,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Register_Unregister_TracksList()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
var a = MakePoint(Vector3.Zero, 5f);
|
||||
var b = MakePoint(new Vector3(10, 0, 0), 5f);
|
||||
mgr.Register(a);
|
||||
mgr.Register(b);
|
||||
Assert.Equal(2, mgr.RegisteredCount);
|
||||
|
||||
mgr.Unregister(a);
|
||||
Assert.Equal(1, mgr.RegisteredCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_DuplicateInstance_Idempotent()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
var light = MakePoint(Vector3.Zero, 5f);
|
||||
mgr.Register(light);
|
||||
mgr.Register(light);
|
||||
Assert.Equal(1, mgr.RegisteredCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_SelectsByDistance_Top8()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
// 12 lights at varying distances, all with range 100 so none filter out.
|
||||
for (int i = 0; i < 12; i++)
|
||||
mgr.Register(MakePoint(new Vector3(i, 0, 0), 100f));
|
||||
|
||||
mgr.Tick(viewerWorldPos: Vector3.Zero);
|
||||
|
||||
Assert.Equal(8, mgr.ActiveCount);
|
||||
// Top 8 should be the closest (i=0..7).
|
||||
foreach (var l in mgr.Active)
|
||||
{
|
||||
Assert.NotNull(l);
|
||||
Assert.True(l!.WorldPosition.X <= 7f);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_DropsLightsOutsideRangeWithSlack()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
mgr.Register(MakePoint(new Vector3(20, 0, 0), range: 5f)); // far outside its own range
|
||||
|
||||
mgr.Tick(viewerWorldPos: Vector3.Zero);
|
||||
|
||||
Assert.Equal(0, mgr.ActiveCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_IncludesLightsNearRangeEdge_WithSlack()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
// Light at distance 5.0, range 5.0: distSq=25, rangeSq*1.1^2 = 25*1.21 = 30.25 → included.
|
||||
mgr.Register(MakePoint(new Vector3(5, 0, 0), range: 5f));
|
||||
|
||||
mgr.Tick(viewerWorldPos: Vector3.Zero);
|
||||
Assert.Equal(1, mgr.ActiveCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_SunSlot0_PreservedAcrossTicks()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
var sun = new LightSource { Kind = LightKind.Directional, WorldForward = -Vector3.UnitZ };
|
||||
mgr.Sun = sun;
|
||||
|
||||
mgr.Register(MakePoint(Vector3.Zero, 100f));
|
||||
mgr.Tick(Vector3.Zero);
|
||||
|
||||
Assert.Equal(2, mgr.ActiveCount);
|
||||
Assert.Same(sun, mgr.Active[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_UnlitLight_Excluded()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
var light = MakePoint(Vector3.Zero, 100f, lit: false);
|
||||
mgr.Register(light);
|
||||
|
||||
mgr.Tick(Vector3.Zero);
|
||||
Assert.Equal(0, mgr.ActiveCount);
|
||||
|
||||
// Toggle lit: should now appear.
|
||||
light.IsLit = true;
|
||||
mgr.Tick(Vector3.Zero);
|
||||
Assert.Equal(1, mgr.ActiveCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnregisterByOwner_RemovesAttachedLights()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
mgr.Register(MakePoint(Vector3.Zero, 5f, ownerId: 42));
|
||||
mgr.Register(MakePoint(new Vector3(1, 0, 0), 5f, ownerId: 42));
|
||||
mgr.Register(MakePoint(new Vector3(2, 0, 0), 5f, ownerId: 99));
|
||||
|
||||
mgr.UnregisterByOwner(42);
|
||||
Assert.Equal(1, mgr.RegisteredCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DistSq_UpdatedEachTick()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
var light = MakePoint(new Vector3(3, 0, 4), 10f); // dist 5
|
||||
mgr.Register(light);
|
||||
|
||||
mgr.Tick(Vector3.Zero);
|
||||
Assert.Equal(25f, light.DistSq, 2);
|
||||
|
||||
mgr.Tick(new Vector3(3, 0, 0)); // same x, same y, z diff 4
|
||||
Assert.Equal(16f, light.DistSq, 2);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue