Fix ambient flyer cadence and rotating shadows

This commit is contained in:
Erik 2026-08-22 17:39:38 +02:00
parent 9cd429dd73
commit 8b7b601b32
5 changed files with 239 additions and 3 deletions

View file

@ -192,6 +192,117 @@ public sealed class DirectionalShadowCascadeFitterTests
Assert.Equal(0, DirectionalShadowCascadeFitter.Fit(in input, cascades));
}
[Theory]
[InlineData(48f, 144f, float.PositiveInfinity, 144f)]
[InlineData(48f, 144f, 96f, 96f)]
[InlineData(160f, 144f, 96f, 160f)]
public void CasterDepthPadding_CoversTheEffectiveResidentReceiverReach(
float configuredPadding,
float qualityReach,
float residentReach,
float expectedPadding)
{
Assert.Equal(
expectedPadding,
DirectionalSunShadowRenderer.ResolveCasterDepthPaddingMeters(
configuredPadding,
qualityReach,
residentReach));
}
[Fact]
public void ReachSizedCasterDepth_KeepsLowSunTreeShadowInsideDuringCameraRotation()
{
DirectionalShadowQuality quality = DirectionalShadowQuality.For(
DirectionalShadowPreset.Medium);
Vector3 light = Vector3.Normalize(new Vector3(0.8f, 0.4f, 0.15f));
Vector3 receiver = new(30f, 0f, 0f);
Vector3 caster = receiver + light * 80f;
int visibleSamples = 0;
bool legacyPaddingClippedCaster = false;
float casterDepthPadding =
DirectionalSunShadowRenderer.ResolveCasterDepthPaddingMeters(
configuredPaddingMeters: 48f,
quality.MaximumReachMeters,
residentMaximumReachMeters: float.PositiveInfinity);
Assert.Equal(quality.MaximumReachMeters, casterDepthPadding);
Span<DirectionalShadowCascade> cascades =
stackalloc DirectionalShadowCascade[4];
Span<DirectionalShadowCascade> legacyCascades =
stackalloc DirectionalShadowCascade[4];
for (int yawDegrees = -50; yawDegrees <= 50; yawDegrees += 5)
{
float yaw = yawDegrees * MathF.PI / 180f;
Vector3 forward = new(MathF.Cos(yaw), MathF.Sin(yaw), 0f);
Matrix4x4 view = Matrix4x4.CreateLookAt(
Vector3.Zero,
forward,
Vector3.UnitZ);
Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(
70f * MathF.PI / 180f,
16f / 9f,
0.1f,
5000f);
Matrix4x4 viewProjection = view * projection;
if (!InsideClip(receiver, viewProjection))
continue;
visibleSamples++;
var input = new DirectionalShadowCascadeFitInput(
view,
projection,
light,
quality,
CasterDepthPaddingMeters: casterDepthPadding);
int count = DirectionalShadowCascadeFitter.Fit(in input, cascades);
DirectionalShadowCascadeFitInput legacyInput = input with
{
CasterDepthPaddingMeters = 48f,
};
int legacyCount = DirectionalShadowCascadeFitter.Fit(
in legacyInput,
legacyCascades);
Assert.Equal(count, legacyCount);
for (int cascadeIndex = 0; cascadeIndex < count; cascadeIndex++)
{
Assert.Equal(
legacyCascades[cascadeIndex].HalfExtentMeters,
cascades[cascadeIndex].HalfExtentMeters);
Assert.Equal(
legacyCascades[cascadeIndex].TexelWorldSize,
cascades[cascadeIndex].TexelWorldSize);
}
DirectionalShadowCascadeBlend selected =
DirectionalShadowReceiverPolicy.SelectCascade(
receiver.Length(),
new Vector4(
cascades[0].SplitFarMeters,
cascades[1].SplitFarMeters,
cascades[2].SplitFarMeters,
0f),
count,
blendWidthMeters: 2f);
legacyPaddingClippedCaster |= !InsideClip(
caster,
legacyCascades[selected.PrimaryCascade].WorldToShadowClip);
Assert.True(
InsideClip(
receiver,
cascades[selected.PrimaryCascade].WorldToShadowClip),
$"receiver left cascade {selected.PrimaryCascade} at yaw {yawDegrees}");
Assert.True(
InsideClip(
caster,
cascades[selected.PrimaryCascade].WorldToShadowClip),
$"caster left cascade {selected.PrimaryCascade} at yaw {yawDegrees}");
}
Assert.True(visibleSamples > 1);
Assert.True(legacyPaddingClippedCaster);
}
private static DirectionalShadowCascadeFitInput CameraInput(
Vector3 position,
DirectionalShadowQuality quality)
@ -219,4 +330,15 @@ public sealed class DirectionalShadowCascadeFitterTests
float y = new Vector3(matrix.M12, matrix.M22, matrix.M32).Length();
return 0.5f * (x + y);
}
private static bool InsideClip(Vector3 point, Matrix4x4 transform)
{
Vector4 clip = Vector4.Transform(new Vector4(point, 1f), transform);
if (!float.IsFinite(clip.W) || MathF.Abs(clip.W) <= 1e-6f)
return false;
Vector3 ndc = new(clip.X / clip.W, clip.Y / clip.W, clip.Z / clip.W);
return MathF.Abs(ndc.X) <= 1f
&& MathF.Abs(ndc.Y) <= 1f
&& ndc.Z is >= 0f and <= 1f;
}
}