The active-light selection dropped any point light whose range didn't reach the VIEWER (DistSq > Range^2*slack -> skip). Retail's D3D-style fixed pipeline picks the 8 NEAREST lights and applies the hard range cutoff PER SURFACE in the shader (mesh_modern.frag: if (d < range)). The viewer-range candidacy filter suppressed a torch whenever the player stood outside its range, so a dungeon room with 2227 registered torches lit only the ~1 the player was standing in (activeLights ~= 1, rest of the room at flat 0.2 ambient = the "lighting off" report). Drop the filter; take the nearest 8 regardless of viewer range. Removed the now-unused RangeSlack const; updated the two tests that codified the old filter. Core lighting suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
147 lines
4.5 KiB
C#
147 lines
4.5 KiB
C#
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_SelectsByDistance_RegardlessOfViewerRange()
|
|
{
|
|
// Retail D3D-style: candidacy is distance-only (the nearest 8). A torch
|
|
// lights its OWN surfaces — the shader applies the hard `d < range` cutoff
|
|
// PER FRAGMENT (mesh_modern.frag) — so a torch the VIEWER is standing
|
|
// outside the range of is still selected; it lights the wall it sits on.
|
|
// Replaces the old viewer-range candidacy filter that suppressed it, which
|
|
// left dungeon rooms (2227 registered torches) at activeLights≈1 / flat 0.2
|
|
// ambient — the "dungeon lighting off" report (#133 A7).
|
|
var mgr = new LightManager();
|
|
mgr.Register(MakePoint(new Vector3(20, 0, 0), range: 5f)); // viewer outside the torch's range
|
|
|
|
mgr.Tick(viewerWorldPos: Vector3.Zero);
|
|
|
|
Assert.Equal(1, mgr.ActiveCount); // selected by distance; the shader culls per-surface
|
|
}
|
|
|
|
[Fact]
|
|
public void Tick_IncludesNearbyLight()
|
|
{
|
|
var mgr = new LightManager();
|
|
// A nearby point light is selected (distance-only candidacy; the shader
|
|
// applies the per-fragment range cutoff).
|
|
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);
|
|
}
|
|
}
|