using System; using System.Numerics; using AcDream.Core.Lighting; using Xunit; namespace AcDream.Core.Tests.Lighting; /// /// Conformance tests for the per-vertex static-light burn-in /// (), ported from retail calc_point_light /// (0x0059c8b0). Golden values are hand-derived from the decompiled equation: /// wrap = (1/1.5)·(N·D + 0.5·dist); norm = distsq>1 ? distsq·dist : dist; /// scale = (1 − dist/Range)·intensity·(wrap/norm); contrib = min(scale·color, color). /// public sealed class LightBakeTests { private static LightSource Torch(Vector3 pos, float intensity = 100f, float range = 10f) => new LightSource { Kind = LightKind.Point, WorldPosition = pos, ColorLinear = Vector3.One, Intensity = intensity, Range = range, IsLit = true, }; [Fact] public void NearTorch_FacingIt_SaturatesToColor() { // Vertex at origin facing up (+Z); torch 2 m above. // dist=2, distsq=4, wrap=(1/1.5)(2+1)=2, norm=4·2=8, // scale=(1-0.2)·100·(2/8)=20 → min(20·1,1)=1 per channel. var c = LightBake.PointContribution( Vector3.Zero, new Vector3(0, 0, 1), Torch(new Vector3(0, 0, 2))); Assert.Equal(1f, c.X, 4); Assert.Equal(1f, c.Y, 4); Assert.Equal(1f, c.Z, 4); } [Fact] public void FarTorch_FallsOffSmoothly() { // Torch 8 m above (still within Range 10). scale=(1-0.8)·100·(8/512)=0.3125. var c = LightBake.PointContribution( Vector3.Zero, new Vector3(0, 0, 1), Torch(new Vector3(0, 0, 8))); Assert.Equal(0.3125f, c.X, 4); Assert.Equal(0.3125f, c.Y, 4); Assert.Equal(0.3125f, c.Z, 4); } [Fact] public void OutOfRange_ContributesNothing() { // Torch 11 m above, Range 10 → dist >= falloff_eff, skipped. var c = LightBake.PointContribution( Vector3.Zero, new Vector3(0, 0, 1), Torch(new Vector3(0, 0, 11))); Assert.Equal(Vector3.Zero, c); } [Fact] public void FacingAway_BeyondWrap_ContributesNothing() { // Normal points away (−Z) from a torch above: N·D=−2, wrap=(1/1.5)(−2+1)<0. var c = LightBake.PointContribution( Vector3.Zero, new Vector3(0, 0, -1), Torch(new Vector3(0, 0, 2))); Assert.Equal(Vector3.Zero, c); } [Fact] public void HalfLambertWrap_LightsSurfaceAngledPast90Degrees() { // Normal at ~100° from the light direction still gets light (Lambert would not). // Light straight above (+Z 2 m); normal tilted to (sin100°, 0, cos100°). double t = 100.0 * Math.PI / 180.0; var n = new Vector3((float)Math.Sin(t), 0, (float)Math.Cos(t)); // cos100° < 0 var c = LightBake.PointContribution(Vector3.Zero, n, Torch(new Vector3(0, 0, 2))); Assert.True(c.X > 0f, "half-Lambert wrap should light a surface angled past 90°"); } [Fact] public void ComputeVertexColor_SumsLightsAndClampsToOne() { // Two saturating torches → sum clamps to 1, never overflows. var lights = new[] { Torch(new Vector3(0, 0, 2)), Torch(new Vector3(0, 0, 2)), }; var c = LightBake.ComputeVertexColor(Vector3.Zero, new Vector3(0, 0, 1), lights); Assert.Equal(1f, c.X, 4); Assert.Equal(1f, c.Y, 4); Assert.Equal(1f, c.Z, 4); } [Fact] public void ComputeVertexColor_SkipsDirectionalAndUnlit() { var lights = new[] { new LightSource { Kind = LightKind.Directional, WorldPosition = new Vector3(0,0,2), ColorLinear = Vector3.One, Intensity = 100f, Range = 10f, IsLit = true }, new LightSource { Kind = LightKind.Point, WorldPosition = new Vector3(0,0,2), ColorLinear = Vector3.One, Intensity = 100f, Range = 10f, IsLit = false }, }; var c = LightBake.ComputeVertexColor(Vector3.Zero, new Vector3(0, 0, 1), lights); Assert.Equal(Vector3.Zero, c); } }