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

@ -438,6 +438,41 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
};
}
/// <summary>
/// The fitted frustum slice bounds receivers, not the casters that project
/// onto them. At low celestial elevations a modest tree can be many metres
/// farther along the light ray than its visible ground shadow. Padding by
/// less than the effective receiver reach therefore clips a caster as the
/// camera-relative slice turns, producing partial or disappearing shadows.
/// This expands depth only; cascade XY density, draw count, and caster
/// membership are unchanged.
/// </summary>
internal static float ResolveCasterDepthPaddingMeters(
float configuredPaddingMeters,
float qualityReachMeters,
float residentMaximumReachMeters)
{
if (!float.IsFinite(configuredPaddingMeters)
|| configuredPaddingMeters <= 0f)
{
throw new ArgumentOutOfRangeException(
nameof(configuredPaddingMeters));
}
if (!float.IsFinite(qualityReachMeters) || qualityReachMeters <= 0f)
throw new ArgumentOutOfRangeException(nameof(qualityReachMeters));
if (float.IsNaN(residentMaximumReachMeters)
|| residentMaximumReachMeters <= 0f)
{
throw new ArgumentOutOfRangeException(
nameof(residentMaximumReachMeters));
}
float effectiveReceiverReach = MathF.Min(
qualityReachMeters,
residentMaximumReachMeters);
return MathF.Max(configuredPaddingMeters, effectiveReceiverReach);
}
internal DirectionalSunShadowDiagnostics RenderPrepared(
IGpuFrame frame,
in DirectionalShadowEnvironmentState environment,
@ -473,6 +508,11 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
nameof(worldTransforms));
long started = Stopwatch.GetTimestamp();
float effectiveCasterDepthPaddingMeters =
ResolveCasterDepthPaddingMeters(
casterDepthPaddingMeters,
_quality.MaximumReachMeters,
residentMaximumReachMeters);
var fit = new DirectionalShadowCascadeFitInput(
cameraView,
cameraProjection,
@ -480,7 +520,7 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
_quality,
cameraNearMeters,
PracticalSplitLambda: 0.65f,
casterDepthPaddingMeters,
effectiveCasterDepthPaddingMeters,
residentMaximumReachMeters);
int cascadeCount = DirectionalShadowCascadeFitter.Fit(
fit,

View file

@ -23,6 +23,15 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
private const double FrameEpsilon = 0.000199999995;
private const float MaximumElapsed = 2f;
// The eight retail SetOmega-authored ambient-flyer animations use their
// vector as a per-animation-quantum turn. Their animation data is authored
// at 30 fps; replaying that raw turn once per modern host/render frame
// makes orbit speed scale with monitor refresh (6x at 180 Hz). Keep the
// exact authored result at 30 Hz while making the DAT-scenery projection
// independent of host cadence. Live PhysicsBody owners retain the literal
// CPhysicsObj branch below and are deliberately not changed here.
private const double DatStaticOmegaReferenceHz = 30d;
private sealed class Owner
{
public required WorldEntity Entity;
@ -468,10 +477,17 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
// static has no CPhysicsObj of its own, so the WorldEntity IS
// the frame retail would be rotating — and the render
// projection re-reads entity.Rotation every frame, so the
// parts composed below orbit it.
// parts composed below orbit it. SetOmega's authored vector is
// a 30 Hz per-animation-quantum turn; normalize it at this
// modern host boundary so monitor refresh cannot change the
// flight period.
owner.RootFrameScratch.Origin = owner.Entity.Position;
owner.RootFrameScratch.Orientation = owner.Entity.Rotation;
FrameOps.GRotate(owner.RootFrameScratch, owner.Omega);
float omegaScale = (float)(
ownerElapsed * DatStaticOmegaReferenceHz);
FrameOps.GRotate(
owner.RootFrameScratch,
owner.Omega * omegaScale);
owner.Entity.Rotation = owner.RootFrameScratch.Orientation;
}