Fix ambient flyer cadence and rotating shadows
This commit is contained in:
parent
9cd429dd73
commit
8b7b601b32
5 changed files with 239 additions and 3 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -316,6 +316,17 @@ public sealed class DirectionalShadowGpuTests
|
|||
Assert.Equal(expectedPasses, device.OfKind<GpuRecordedTimerScope>().Count());
|
||||
Assert.Equal(expectedPasses, device.OfKind<GpuRecordedUniformBind>()
|
||||
.Count(call => call.Binding == GpuBindingModel.UniformDirectionalShadow));
|
||||
GpuRecordedUniformBind shadowUniformBind = Assert.Single(
|
||||
device.OfKind<GpuRecordedUniformBind>()
|
||||
.DistinctBy(call => (call.BufferName, call.OffsetBytes, call.SizeBytes)),
|
||||
call => call.Binding == GpuBindingModel.UniformDirectionalShadow);
|
||||
DirectionalShadowUniforms shadowUniforms = MemoryMarshal.Read<DirectionalShadowUniforms>(
|
||||
device.RingBytes.Slice(
|
||||
checked((int)shadowUniformBind.OffsetBytes),
|
||||
checked((int)shadowUniformBind.SizeBytes)));
|
||||
Assert.Equal(
|
||||
DirectionalShadowQuality.For(preset).MaximumReachMeters,
|
||||
shadowUniforms.BiasMeters.W);
|
||||
Assert.Equal(expectedDraws, device.OfKind<GpuRecordedMultiDrawIndirect>().Count());
|
||||
Assert.Equal(2, device.OfKind<GpuRecordedRingAllocation>().Count());
|
||||
GpuRecordedRingAllocation transformAllocation = Assert.Single(
|
||||
|
|
|
|||
|
|
@ -842,6 +842,53 @@ public sealed class RetailStaticAnimatingObjectSchedulerTests
|
|||
Assert.NotEqual(afterFirst, entity.Rotation);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(30)]
|
||||
[InlineData(60)]
|
||||
[InlineData(180)]
|
||||
public void SetOmegaHook_DatSceneryOrbitIsIndependentOfHostFrameRate(
|
||||
int hostFramesPerSecond)
|
||||
{
|
||||
const float authoredYawPerQuantum = -0.027f;
|
||||
var loader = new Loader();
|
||||
loader.Add(
|
||||
AnimationId,
|
||||
OmegaAnimation(new Vector3(0f, 0f, authoredYawPerQuantum)));
|
||||
var scheduler = new RetailStaticAnimatingObjectScheduler(
|
||||
loader,
|
||||
(_, sequencer) => sequencer.ConsumePendingHooks(),
|
||||
(_, _, _) => { });
|
||||
WorldEntity entity = MakeEntity();
|
||||
scheduler.Register(entity, new ScriptActivationInfo(
|
||||
ScriptId: 0,
|
||||
PartTransforms: entity.IndexedPartTransforms,
|
||||
PartAvailability: entity.IndexedPartAvailable,
|
||||
Setup: MakeSetup(),
|
||||
DefaultAnimationId: AnimationId,
|
||||
UsesStaticAnimationWorkset: true));
|
||||
|
||||
// Bank SetOmega at process_hooks; retail first applies it on the next
|
||||
// static-animation pass. Do not include this installation frame in the
|
||||
// measured one-second orbit interval.
|
||||
scheduler.Tick(1f / 30f);
|
||||
scheduler.ProcessHooks();
|
||||
|
||||
float hostDelta = 1f / hostFramesPerSecond;
|
||||
for (int frame = 0; frame < hostFramesPerSecond; frame++)
|
||||
{
|
||||
scheduler.Tick(hostDelta);
|
||||
scheduler.ProcessHooks();
|
||||
}
|
||||
|
||||
Vector3 actualForward = Vector3.Transform(Vector3.UnitX, entity.Rotation);
|
||||
float expectedYaw = authoredYawPerQuantum * 30f;
|
||||
var expectedForward = new Vector3(
|
||||
MathF.Cos(expectedYaw),
|
||||
MathF.Sin(expectedYaw),
|
||||
0f);
|
||||
Assert.InRange(Vector3.Distance(actualForward, expectedForward), 0f, 0.0001f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithoutASetOmegaHookTheRootNeverTurns()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue