acdream/tests/AcDream.App.Tests/Rendering/DirectionalShadowCascadeFitterTests.cs

222 lines
8.9 KiB
C#

using System.Numerics;
using AcDream.App.Rendering;
namespace AcDream.App.Tests.Rendering;
public sealed class DirectionalShadowCascadeFitterTests
{
[Theory]
[InlineData(DirectionalShadowPreset.Low, 2, 72f)]
[InlineData(DirectionalShadowPreset.Medium, 3, 144f)]
[InlineData(DirectionalShadowPreset.High, 4, 240f)]
internal void Fit_UsesPracticalIncreasingSplitsAndExactPresetReach(
DirectionalShadowPreset preset,
int expectedCount,
float expectedReach)
{
DirectionalShadowQuality quality = DirectionalShadowQuality.For(preset);
DirectionalShadowCascadeFitInput input = CameraInput(
Vector3.Zero,
quality);
Span<DirectionalShadowCascade> cascades =
stackalloc DirectionalShadowCascade[4];
int count = DirectionalShadowCascadeFitter.Fit(in input, cascades);
Assert.Equal(expectedCount, count);
float previous = input.CameraNearMeters;
for (int i = 0; i < count; i++)
{
Assert.Equal(previous, cascades[i].SplitNearMeters);
Assert.True(cascades[i].SplitFarMeters > previous);
Assert.True(cascades[i].TexelWorldSize > 0f);
Assert.True(float.IsFinite(cascades[i].WorldToShadowClip.M11));
previous = cascades[i].SplitFarMeters;
}
Assert.Equal(expectedReach, cascades[count - 1].SplitFarMeters, 3);
}
[Fact]
public void TexelStabilization_SubTexelCameraTranslationKeepsSnappedCenter()
{
DirectionalShadowQuality quality = DirectionalShadowQuality.For(
DirectionalShadowPreset.Medium);
DirectionalShadowCascadeFitInput firstInput = CameraInput(
new Vector3(100f, 200f, 30f),
quality);
Span<DirectionalShadowCascade> first =
stackalloc DirectionalShadowCascade[4];
DirectionalShadowCascadeFitter.Fit(in firstInput, first);
// Translation along the light-space X axis by less than half a map
// texel must not move the stabilized projection centre.
Vector3 light = Vector3.Normalize(firstInput.SurfaceToLightDirection);
Vector3 lightX = Vector3.Normalize(Vector3.Cross(
DirectionalShadowCascadeFitter.StableLightUp(light),
light));
Vector3 movement = lightX * (first[0].TexelWorldSize * 0.2f);
DirectionalShadowCascadeFitInput secondInput = CameraInput(
new Vector3(100f, 200f, 30f) + movement,
quality);
Span<DirectionalShadowCascade> second =
stackalloc DirectionalShadowCascade[4];
DirectionalShadowCascadeFitter.Fit(in secondInput, second);
Assert.Equal(
first[0].StabilizedLightSpaceCenter.X,
second[0].StabilizedLightSpaceCenter.X);
Assert.Equal(
first[0].StabilizedLightSpaceCenter.Y,
second[0].StabilizedLightSpaceCenter.Y);
Assert.Equal(first[0].HalfExtentMeters, second[0].HalfExtentMeters);
}
[Fact]
public void StableLightUp_DoesNotRotateAtTheFormerHighLightThreshold()
{
Vector3 below = Vector3.Normalize(new Vector3(0.3125f, 0.02f, 0.9498f));
Vector3 above = Vector3.Normalize(new Vector3(0.3110f, 0.02f, 0.9503f));
Vector3 belowUp = DirectionalShadowCascadeFitter.StableLightUp(below);
Vector3 aboveUp = DirectionalShadowCascadeFitter.StableLightUp(above);
Assert.InRange(MathF.Abs(Vector3.Dot(below, belowUp)), 0f, 1e-5f);
Assert.InRange(MathF.Abs(Vector3.Dot(above, aboveUp)), 0f, 1e-5f);
Assert.True(Vector3.Dot(belowUp, aboveUp) > 0.999f);
}
[Fact]
public void StableLightUp_TrueZenithIsFiniteAndOrthogonal()
{
Vector3 up = DirectionalShadowCascadeFitter.StableLightUp(Vector3.UnitZ);
Assert.True(float.IsFinite(up.X) && float.IsFinite(up.Y) && float.IsFinite(up.Z));
Assert.Equal(1f, up.Length(), 5);
Assert.InRange(MathF.Abs(Vector3.Dot(Vector3.UnitZ, up)), 0f, 1e-5f);
}
[Fact]
public void StableLightUp_RemainsContinuousThroughCelestialZenith()
{
Vector3 beforeZenith = Vector3.Normalize(new Vector3(0.001f, 0.002f, 1f));
Vector3 zenith = Vector3.UnitZ;
Vector3 afterZenith = Vector3.Normalize(new Vector3(-0.001f, -0.002f, 1f));
Vector3 beforeUp = DirectionalShadowCascadeFitter.StableLightUp(beforeZenith);
Vector3 zenithUp = DirectionalShadowCascadeFitter.StableLightUp(zenith);
Vector3 afterUp = DirectionalShadowCascadeFitter.StableLightUp(afterZenith);
Assert.True(Vector3.Dot(beforeUp, zenithUp) > 0.99999f);
Assert.True(Vector3.Dot(zenithUp, afterUp) > 0.99999f);
Assert.InRange(MathF.Abs(Vector3.Dot(beforeZenith, beforeUp)), 0f, 1e-5f);
Assert.InRange(MathF.Abs(Vector3.Dot(afterZenith, afterUp)), 0f, 1e-5f);
}
[Fact]
public void ClipDensityRatio_MatchesCascadeTexelFootprintRatio()
{
DirectionalShadowCascadeFitInput input = CameraInput(
new Vector3(40f, -15f, 8f),
DirectionalShadowQuality.For(DirectionalShadowPreset.High));
Span<DirectionalShadowCascade> cascades =
stackalloc DirectionalShadowCascade[4];
int count = DirectionalShadowCascadeFitter.Fit(in input, cascades);
float nearDensity = ClipXyDensity(cascades[0].WorldToShadowClip);
float farDensity = ClipXyDensity(cascades[count - 1].WorldToShadowClip);
float shaderScale = farDensity / nearDensity;
float expectedScale = cascades[0].TexelWorldSize
/ cascades[count - 1].TexelWorldSize;
Assert.Equal(expectedScale, shaderScale, 4);
Assert.InRange(shaderScale, 0f, 0.999f);
}
[Fact]
public void Fit_DoesNotAllocateOrInvokeSceneVisibility()
{
DirectionalShadowCascadeFitInput input = CameraInput(
Vector3.Zero,
DirectionalShadowQuality.For(DirectionalShadowPreset.High));
Span<DirectionalShadowCascade> cascades =
stackalloc DirectionalShadowCascade[4];
// Cross the tiered-JIT promotion threshold before taking the thread's
// allocation counter; measuring immediately after one call makes the
// runtime's compilation bookkeeping look like renderer allocation.
for (int i = 0; i < 128; i++)
DirectionalShadowCascadeFitter.Fit(in input, cascades);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 100; i++)
DirectionalShadowCascadeFitter.Fit(in input, cascades);
long after = GC.GetAllocatedBytesForCurrentThread();
Assert.Equal(0, after - before);
}
[Fact]
public void ResidentWindowClampsOnlyTheFinalCascadeReach()
{
DirectionalShadowQuality quality = DirectionalShadowQuality.For(
DirectionalShadowPreset.High);
DirectionalShadowCascadeFitInput input = CameraInput(
Vector3.Zero,
quality) with
{
ResidentMaximumReachMeters = 96f,
};
Span<DirectionalShadowCascade> cascades =
stackalloc DirectionalShadowCascade[4];
int count = DirectionalShadowCascadeFitter.Fit(in input, cascades);
Assert.Equal(quality.CascadeCount, count);
Assert.Equal(96f, cascades[count - 1].SplitFarMeters, 3);
Assert.All(
cascades[..count].ToArray(),
cascade => Assert.InRange(cascade.SplitFarMeters, 0f, 96f));
}
[Fact]
public void UnavailableResidentWindowDisablesFittingWithoutAllocating()
{
DirectionalShadowCascadeFitInput input = CameraInput(
Vector3.Zero,
DirectionalShadowQuality.For(DirectionalShadowPreset.High)) with
{
ResidentMaximumReachMeters = 0f,
};
Span<DirectionalShadowCascade> cascades =
stackalloc DirectionalShadowCascade[4];
Assert.Equal(0, DirectionalShadowCascadeFitter.Fit(in input, cascades));
}
private static DirectionalShadowCascadeFitInput CameraInput(
Vector3 position,
DirectionalShadowQuality quality)
{
Vector3 target = position + Vector3.Normalize(new Vector3(1f, 2f, -0.2f));
Matrix4x4 view = Matrix4x4.CreateLookAt(position, target, Vector3.UnitZ);
Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(
70f * MathF.PI / 180f,
16f / 9f,
0.1f,
5000f);
return new DirectionalShadowCascadeFitInput(
view,
projection,
Vector3.Normalize(new Vector3(0.4f, 0.7f, 0.55f)),
quality);
}
private static float ClipXyDensity(Matrix4x4 matrix)
{
// System.Numerics row-vector storage is read as the transposed
// column-major matrix in GLSL. These are the same two clip gradients
// evaluated by acdreamShadowBiasScale.
float x = new Vector3(matrix.M11, matrix.M21, matrix.M31).Length();
float y = new Vector3(matrix.M12, matrix.M22, matrix.M32).Length();
return 0.5f * (x + y);
}
}