222 lines
8.9 KiB
C#
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);
|
|
}
|
|
}
|