FSR2: reduce doubling via tighter clamp, MV dead zone, luminance stability

- Motion shader: zero out sub-0.01px motion to eliminate float precision
  noise in reprojection (distant geometry with large world coords)
- Accumulate: tighten neighborhood clamp gamma 3.0→1.5 to catch slightly
  misaligned history causing ghost doubles
- Reduce max jitter-aware blend 30%→20% for less visible oscillation
- Add luminance instability dampening: reduce blend when current frame
  disagrees with history to prevent shimmer on small/distant features
This commit is contained in:
Kelsi 2026-03-08 14:34:58 -07:00
parent e8bbb17196
commit f74dcc37e0
4 changed files with 33 additions and 4 deletions

View file

@ -133,7 +133,10 @@ void main() {
vec3 variance = max(m2 / 9.0 - mean * mean, vec3(0.0)); vec3 variance = max(m2 / 9.0 - mean * mean, vec3(0.0));
vec3 stddev = sqrt(variance); vec3 stddev = sqrt(variance);
float gamma = 3.0; // Tighter clamp (gamma 1.5) catches slightly misaligned history that
// causes doubling. With jitter-aware blending providing stability,
// the clamp can be tight without causing jitter-chasing.
float gamma = 1.5;
vec3 boxMin = mean - gamma * stddev; vec3 boxMin = mean - gamma * stddev;
vec3 boxMax = mean + gamma * stddev; vec3 boxMax = mean + gamma * stddev;
@ -143,9 +146,28 @@ void main() {
float clampDist = length(historyYCoCg - clampedHistory); float clampDist = length(historyYCoCg - clampedHistory);
// Uniform 5% blend: ~45 frames for 90% convergence. // Jitter-aware sample weighting: compute how close the current frame's
// Simpler than edge-aware; the anti-ringing bicubic handles edge stability. // jittered sample fell to this output pixel. Close samples are high quality
float blendFactor = 0.05; // (blend aggressively for fast convergence), distant samples are low quality
// (blend minimally to avoid visible jitter).
vec2 jitterPx = pc.jitterOffset.xy * 0.5 * pc.internalSize.xy;
vec2 internalPos = outUV * pc.internalSize.xy;
vec2 subPixelOffset = fract(internalPos) - 0.5;
vec2 sampleDelta = subPixelOffset - jitterPx;
float dist2 = dot(sampleDelta, sampleDelta);
float sampleQuality = exp(-dist2 * 3.0);
float baseBlend = mix(0.02, 0.20, sampleQuality);
// Luminance instability: when current frame differs significantly from
// history, it may be aliased/flickering content. Reduce blend to prevent
// oscillation, especially for small distant features.
float lumCurrent = dot(currentColor, vec3(0.299, 0.587, 0.114));
float lumHistory = dot(historyColor, vec3(0.299, 0.587, 0.114));
float lumDelta = abs(lumCurrent - lumHistory) / max(max(lumCurrent, lumHistory), 0.01);
float stability = 1.0 - clamp(lumDelta * 3.0, 0.0, 0.7);
baseBlend *= stability;
float blendFactor = baseBlend;
// Disocclusion: large clamp distance → rapidly replace stale history // Disocclusion: large clamp distance → rapidly replace stale history
blendFactor = mix(blendFactor, 0.60, clamp(clampDist * 5.0, 0.0, 1.0)); blendFactor = mix(blendFactor, 0.60, clamp(clampDist * 5.0, 0.0, 1.0));

View file

@ -41,5 +41,12 @@ void main() {
// For a static scene (identity reprojMatrix), this is exactly zero. // For a static scene (identity reprojMatrix), this is exactly zero.
vec2 motion = prevUV - currentUnjitteredUV; vec2 motion = prevUV - currentUnjitteredUV;
// Dead zone: sub-pixel motion below 0.01 pixels is floating-point noise
// from reprojMatrix precision (especially for distant geometry where
// large world coords amplify VP inverse errors). Zero it to prevent
// phantom doubling artifacts.
float motionPx = length(motion * pc.resolution.xy);
motion *= step(0.01, motionPx);
imageStore(motionVectors, pixelCoord, vec4(motion, 0.0, 0.0)); imageStore(motionVectors, pixelCoord, vec4(motion, 0.0, 0.0));
} }

Binary file not shown.