df3240c17f
Phase 3 -- YCoCg neighbourhood variance clamp: the reprojected history is forced into the colour box (mean +/- gamma*sigma) of the current frame's 3x3 neighbourhood in YCoCg, crushing the doubling/smearing the reconstructed motion field leaves while preserving the marble-steady smoothing at rest. RYUJINX_TAA_CLAMP tunes the box half-width (default 1.0). Phase 4 -- reduced-resolution optical flow: the colour is bilinear-downsampled to 1/N (default half) and Lucas-Kanade + the 3x3 median run there (~N^2 less work, the dominant cost), then the field is bilinear-upscaled and scaled by N in the blend. The per-frame full-res previous-color copy is gone. RYUJINX_TAA_MV_DOWNSCALE sets the divisor (1/2/4, default 2). New TaaDownsample shader. Validated: 60 FPS at x3 with the Phase 3 quality preserved. Velocity-drop hysteresis -- removes the single-frame flash on a hard stop: a per-pixel smoothed velocity (carried in the history alpha) rises instantly with motion but decays over several frames, and both the clamp width and blend weight are continuous functions of it, so the static 'marble' reasserts gradually instead of snapping. Decay 0.85 for large distant surfaces (waterfalls). All gated on RYUJINX_TAA=1; default render path unchanged when off.
134 lines
5.8 KiB
GLSL
134 lines
5.8 KiB
GLSL
// Temporal Anti-Aliasing (clean-room).
|
|
//
|
|
// Phase 4 + velocity-drop hysteresis. On top of motion reprojection (reduced-res field) and the YCoCg
|
|
// neighbourhood variance clamp, a per-pixel SMOOTHED velocity (carried in the history's alpha channel)
|
|
// removes the single-frame flash seen when the camera stops dead: the smoothed velocity rises instantly
|
|
// with motion but decays over ~3-4 frames, and BOTH the clamp width and the blend weight are continuous
|
|
// functions of it. So when you stop, the clamp loosens and the history eases back to its "marble" weight
|
|
// gradually instead of snapping in one frame.
|
|
//
|
|
// vSmooth = max(|mv|, vSmooth_prev * VEL_DECAY) // instant rise, slow fall
|
|
// motion = smoothstep(VEL_LOW, VEL_HIGH, vSmooth) // 0 static .. 1 moving
|
|
// gamma = mix(clampGamma*STATIC_CLAMP, clampGamma, motion)
|
|
// wBlend = mix(blend, blend*MOVING_BLEND, motion)
|
|
//
|
|
// prevPos = loc + motionSign * mv // motionSign = +1 (calibrated)
|
|
|
|
#version 430 core
|
|
|
|
layout (local_size_x = 16, local_size_y = 16) in;
|
|
|
|
layout (rgba8, binding = 0, set = 3) uniform image2D imgOutput;
|
|
layout (rgba16f, binding = 1, set = 3) uniform image2D imgHistoryWrite;
|
|
layout (binding = 1, set = 2) uniform sampler2D Source; // current color
|
|
layout (binding = 3, set = 2) uniform sampler2D HistoryRead; // previous result (rgb) + smoothed velocity (a)
|
|
layout (binding = 5, set = 2) uniform sampler2D Motion; // rg16f motion field, motion-res pixels
|
|
|
|
layout (binding = 2) uniform params {
|
|
float width;
|
|
float height;
|
|
float blend; // history weight in [0,1]; current weight is (1 - blend)
|
|
float hasHistory; // >0.5 once the read-side history holds a valid previous frame
|
|
float motionSign; // +1 (calibrated) maps a pixel back to its previous position (prevPos = loc + mv)
|
|
float hasMotion; // >0.5 once a previous frame exists to estimate motion from
|
|
float clampGamma; // variance-box half-width in std-devs (smaller = tighter = less ghost, more flicker)
|
|
float mvScale; // full-res / motion-res ratio: upscales the low-res motion field to full-res pixels
|
|
};
|
|
|
|
// Velocity-drop hysteresis constants (full-res pixel units). Hardcoded tuning -- continuity, not the exact
|
|
// values, is what removes the flash.
|
|
const float VEL_DECAY = 0.85; // per-frame decay of the smoothed velocity when motion stops (gentle: longer
|
|
// ramp, ~5-6 frames of easing -- lets a huge distant surface like a waterfall
|
|
// re-align with no visible break)
|
|
const float VEL_LOW = 0.10; // below this smoothed velocity the pixel is treated as fully static
|
|
const float VEL_HIGH = 1.00; // above this it is treated as fully moving
|
|
const float STATIC_CLAMP = 2.00; // clamp box is this much looser when fully static (history trusted = marble)
|
|
const float MOVING_BLEND = 0.92; // history weight is scaled by this when fully moving (slightly more reactive)
|
|
const float VEL_MAX = 64.0; // clamp the stored velocity so it can never run away
|
|
|
|
vec3 RGBToYCoCg(vec3 c)
|
|
{
|
|
return vec3(
|
|
0.25 * c.r + 0.5 * c.g + 0.25 * c.b,
|
|
0.5 * c.r - 0.5 * c.b,
|
|
-0.25 * c.r + 0.5 * c.g - 0.25 * c.b);
|
|
}
|
|
|
|
vec3 YCoCgToRGB(vec3 c)
|
|
{
|
|
float y = c.x;
|
|
float co = c.y;
|
|
float cg = c.z;
|
|
|
|
return vec3(y + co - cg, y + cg, y - co - cg);
|
|
}
|
|
|
|
void main()
|
|
{
|
|
ivec2 loc = ivec2(gl_GlobalInvocationID.xy);
|
|
|
|
int iw = int(width);
|
|
int ih = int(height);
|
|
|
|
if (loc.x >= iw || loc.y >= ih)
|
|
{
|
|
return;
|
|
}
|
|
|
|
vec4 current = texelFetch(Source, loc, 0);
|
|
vec3 currentY = RGBToYCoCg(current.rgb);
|
|
|
|
// Build the current 3x3 neighbourhood's YCoCg mean and variance for the clamp box.
|
|
vec3 m1 = vec3(0.0);
|
|
vec3 m2 = vec3(0.0);
|
|
|
|
for (int dy = -1; dy <= 1; ++dy)
|
|
{
|
|
for (int dx = -1; dx <= 1; ++dx)
|
|
{
|
|
ivec2 p = clamp(loc + ivec2(dx, dy), ivec2(0), ivec2(iw - 1, ih - 1));
|
|
vec3 c = RGBToYCoCg(texelFetch(Source, p, 0).rgb);
|
|
m1 += c;
|
|
m2 += c * c;
|
|
}
|
|
}
|
|
|
|
vec3 mean = m1 / 9.0;
|
|
vec3 sigma = sqrt(max(vec3(0.0), m2 / 9.0 - mean * mean));
|
|
|
|
// Reproject and read the history (rgb) plus the previous smoothed velocity (alpha). Motion is estimated
|
|
// at reduced resolution: bilinear-sample it and scale to full-res pixels.
|
|
vec2 locUv = (vec2(loc) + 0.5) / vec2(width, height);
|
|
vec2 mv = hasMotion > 0.5 ? texture(Motion, locUv).xy * mvScale : vec2(0.0);
|
|
vec2 prevPos = vec2(loc) + 0.5 + motionSign * mv;
|
|
vec2 uv = prevPos / vec2(width, height);
|
|
|
|
bool onScreen = all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));
|
|
bool valid = hasHistory > 0.5 && onScreen;
|
|
|
|
vec4 histSample = texture(HistoryRead, uv);
|
|
float prevVSmooth = valid ? histSample.a : 0.0;
|
|
|
|
// Smoothed velocity: jumps up the instant the pixel moves, eases down over several frames when it stops.
|
|
float vRaw = length(mv);
|
|
float vSmooth = min(max(vRaw, prevVSmooth * VEL_DECAY), VEL_MAX);
|
|
float motion = smoothstep(VEL_LOW, VEL_HIGH, vSmooth);
|
|
|
|
// Continuous transition guards: clamp loosens and blend rises back to the static "marble" values as the
|
|
// smoothed velocity decays, so the stop is amortised instead of snapping in a single frame.
|
|
float gamma = mix(clampGamma * STATIC_CLAMP, clampGamma, motion);
|
|
float wBlend = mix(blend, blend * MOVING_BLEND, motion);
|
|
|
|
vec3 boxMin = mean - gamma * sigma;
|
|
vec3 boxMax = mean + gamma * sigma;
|
|
vec3 historyY = clamp(RGBToYCoCg(histSample.rgb), boxMin, boxMax);
|
|
|
|
float w = valid ? wBlend : 0.0;
|
|
vec3 resultY = mix(currentY, historyY, w);
|
|
vec3 resultRGB = YCoCgToRGB(resultY);
|
|
|
|
// Output carries the real alpha; the history alpha carries the smoothed velocity for next frame.
|
|
imageStore(imgOutput, loc, vec4(resultRGB, current.a));
|
|
imageStore(imgHistoryWrite, loc, vec4(resultRGB, vSmooth));
|
|
}
|