fix(G.3 A7): dungeon lighting — select 8 NEAREST lights, not viewer-in-range (#133)
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>
This commit is contained in:
parent
d6fb788c96
commit
a80061b0c2
2 changed files with 23 additions and 8 deletions
|
|
@ -37,7 +37,6 @@ namespace AcDream.Core.Lighting;
|
||||||
public sealed class LightManager
|
public sealed class LightManager
|
||||||
{
|
{
|
||||||
public const int MaxActiveLights = 8; // D3D parity
|
public const int MaxActiveLights = 8; // D3D parity
|
||||||
private const float RangeSlack = 1.1f; // 10% hysteresis around hard cutoff
|
|
||||||
|
|
||||||
private readonly List<LightSource> _all = new();
|
private readonly List<LightSource> _all = new();
|
||||||
private readonly LightSource?[] _active = new LightSource?[MaxActiveLights];
|
private readonly LightSource?[] _active = new LightSource?[MaxActiveLights];
|
||||||
|
|
@ -109,8 +108,16 @@ public sealed class LightManager
|
||||||
Vector3 delta = light.WorldPosition - viewerWorldPos;
|
Vector3 delta = light.WorldPosition - viewerWorldPos;
|
||||||
light.DistSq = delta.LengthSquared();
|
light.DistSq = delta.LengthSquared();
|
||||||
|
|
||||||
float rangeSq = light.Range * light.Range * RangeSlack * RangeSlack;
|
// Retail D3D-style fixed-pipeline lighting picks the 8 NEAREST point
|
||||||
if (light.DistSq > rangeSq) continue;
|
// lights and applies each light's hard range-cutoff PER SURFACE in the
|
||||||
|
// shader (mesh_modern.frag: `if (d < range && range > 1e-3)`). The
|
||||||
|
// previous viewer-range candidacy filter (skip when DistSq > Range²·slack²)
|
||||||
|
// was wrong — it dropped a torch whenever the VIEWER stood outside that
|
||||||
|
// torch's range, so a dungeon room with 2227 registered torches lit only
|
||||||
|
// the ~1 the player was standing inside (activeLights≈1, the rest of the
|
||||||
|
// room at flat 0.2 ambient — the "dungeon lighting off" report). Take the
|
||||||
|
// nearest 8 regardless of viewer range; the shader's per-fragment
|
||||||
|
// `d < range` does the actual hard cutoff.
|
||||||
candidates.Add(light);
|
candidates.Add(light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,21 +60,29 @@ public sealed class LightManagerTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Tick_DropsLightsOutsideRangeWithSlack()
|
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();
|
var mgr = new LightManager();
|
||||||
mgr.Register(MakePoint(new Vector3(20, 0, 0), range: 5f)); // far outside its own range
|
mgr.Register(MakePoint(new Vector3(20, 0, 0), range: 5f)); // viewer outside the torch's range
|
||||||
|
|
||||||
mgr.Tick(viewerWorldPos: Vector3.Zero);
|
mgr.Tick(viewerWorldPos: Vector3.Zero);
|
||||||
|
|
||||||
Assert.Equal(0, mgr.ActiveCount);
|
Assert.Equal(1, mgr.ActiveCount); // selected by distance; the shader culls per-surface
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Tick_IncludesLightsNearRangeEdge_WithSlack()
|
public void Tick_IncludesNearbyLight()
|
||||||
{
|
{
|
||||||
var mgr = new LightManager();
|
var mgr = new LightManager();
|
||||||
// Light at distance 5.0, range 5.0: distSq=25, rangeSq*1.1^2 = 25*1.21 = 30.25 → included.
|
// 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.Register(MakePoint(new Vector3(5, 0, 0), range: 5f));
|
||||||
|
|
||||||
mgr.Tick(viewerWorldPos: Vector3.Zero);
|
mgr.Tick(viewerWorldPos: Vector3.Zero);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue