Skip to main content

TAA

Constant TAA 

Source
pub const TAA: &str = "// Temporal anti-aliasing resolve: blend the current HDR frame with the\n// reprojected, neighbourhood-clipped history buffer. Single source for every\n// backend; pairs with `fullscreen_vertex` in fullscreen.slang.\n//\n// Resources are declared top-level rather than in a ParameterBlock: this pass\n// is engine-internal (nothing outside the engine binds against its layout), so\n// the declaration order is free to be the one both binding models want. Vulkan\n// gets set 0 bindings 0-2 as combined image samplers plus a 4-byte push\n// constant; Metal gets texture(0..2) + sampler(0..2) and the params at\n// buffer(0).\n\nstruct TaaParams\n{\n    // 0 on the first frame and after a resize - history is then ignored.\n    float history_valid;\n};\n\n[[vk::binding(0, 0)]] Sampler2D<float4> scene_tex;\n[[vk::binding(1, 0)]] Sampler2D<float4> velocity_tex;\n[[vk::binding(2, 0)]] Sampler2D<float4> history_tex;\n\n[[vk::push_constant]]\nConstantBuffer<TaaParams> params;\n\n// History blend weight. 0.9 keeps 90% of the accumulated history each frame -\n// roughly a 10-frame exponential moving average once converged.\nstatic const float TAA_BLEND = 0.9;\n\n// Standard deviations of the neighbourhood the history is allowed to span.\n// 1.0 is the Playdead/Karis default; lower tightens (less ghosting, more\n// flicker), higher loosens.\nstatic const float TAA_VARIANCE_GAMMA = 1.0;\n\n// Finite ceiling every sampled colour is clamped to. Largest finite half - the\n// scene/history targets are RGBA16Float, so an HDR specular that overflowed the\n// target reads back as +Inf, and an uninitialised history texel on the first\n// frame can read back as Inf or NaN. Either one would turn the variance\n// statistics into NaN (Inf-Inf) and, once a NaN lands in the history, it feeds\n// back every frame as a black screen.\nstatic const float TAA_HDR_CLAMP = 65504.0;\n\n{POST_COMMON}\n\n// Scrub a non-finite sample: NaN -> 0, +Inf -> the HDR ceiling, -Inf/negative\n// -> 0. NaN is removed first (NaN != NaN) so the result never depends on how\n// clamp tie-breaks a NaN operand.\nfloat3 taa_sanitize(float3 c)\n{\n    c = select(isnan(c), float3(0.0), c);\n    return clamp(c, float3(0.0), float3(TAA_HDR_CLAMP));\n}\n\n// RGB <-> YCoCg. The neighbourhood clip box is built in YCoCg because its luma\n// axis aligns with perceived error: the box is tighter and better-oriented than\n// an RGB AABB, so a reprojected history ghosts less. The transform is linear,\n// so it is safe on the linear-light HDR values here.\nfloat3 rgb_to_ycocg(float3 c)\n{\n    return float3(\n         0.25 * c.r + 0.5 * c.g + 0.25 * c.b,\n         0.5  * c.r            - 0.5  * c.b,\n        -0.25 * c.r + 0.5 * c.g - 0.25 * c.b);\n}\n\nfloat3 ycocg_to_rgb(float3 c)\n{\n    float t = c.x - c.z;\n    return float3(t + c.y, c.x + c.z, t - c.y);\n}\n\n// Clip the history sample to the neighbourhood box along the line toward the\n// box centre (Karis 2014). Unlike a per-component clamp this preserves the\n// colour\'s direction, so a clipped history shifts hue far less.\nfloat3 clip_to_aabb(float3 bmin, float3 bmax, float3 hist)\n{\n    float3 center = 0.5 * (bmax + bmin);\n    float3 extent = 0.5 * (bmax - bmin) + 1e-5;\n    float3 v = hist - center;\n    float3 a = abs(v) / extent;\n    float ma = max(a.x, max(a.y, a.z));\n    return (ma > 1.0) ? (center + v / ma) : hist;\n}\n\n[shader(\"fragment\")]\nfloat4 taa_fragment_main([[vk::location(0)]] float2 uv : TEXCOORD0) : SV_Target\n{\n    float2 texel = 1.0 / combined_size(scene_tex);\n    float3 cur = taa_sanitize(scene_tex.Sample(uv).rgb);\n\n    // 3x3 neighbourhood statistics in YCoCg. The reprojected history is clipped\n    // to mean +/- gamma*stddev - a variance box, tighter and better-oriented\n    // than a min/max AABB, so disocclusions and sub-pixel misses ghost less.\n    // Every sample is sanitised first so a non-finite HDR texel cannot make the\n    // moments (and therefore the box) NaN.\n    float3 m1 = float3(0.0);\n    float3 m2 = float3(0.0);\n    for (int dy = -1; dy <= 1; ++dy)\n    {\n        for (int dx = -1; dx <= 1; ++dx)\n        {\n            float3 s = taa_sanitize(scene_tex.Sample(uv + float2(dx, dy) * texel).rgb);\n            float3 c = rgb_to_ycocg(s);\n            m1 += c;\n            m2 += c * c;\n        }\n    }\n    float3 mean  = m1 / 9.0;\n    float3 sigma = sqrt(max(m2 / 9.0 - mean * mean, float3(0.0)));\n    float3 bmin  = mean - TAA_VARIANCE_GAMMA * sigma;\n    float3 bmax  = mean + TAA_VARIANCE_GAMMA * sigma;\n\n    // The velocity pre-pass stored each surface\'s screen-space motion as the\n    // offset that maps a current-frame UV onto its previous-frame UV. This\n    // captures camera motion, moving props, and skinned deformation alike.\n    float2 motion  = velocity_tex.Sample(uv).rg;\n    float2 prev_uv = uv + motion;\n    bool on_screen = all(prev_uv >= float2(0.0)) && all(prev_uv <= float2(1.0));\n\n    // Sanitise the history too: on the first frame it is an uninitialised\n    // target, and a NaN read here would survive clip_to_aabb (a NaN fails the\n    // ma > 1 test, so the unclipped NaN is returned) and poison every later\n    // frame through the feedback.\n    float3 hist = rgb_to_ycocg(taa_sanitize(history_tex.Sample(prev_uv).rgb));\n    hist = clip_to_aabb(bmin, bmax, hist);\n\n    // Accumulate only when there is valid, on-screen history; otherwise the\n    // current frame passes straight through (first frame, resize, off-screen).\n    float alpha = (params.history_valid > 0.5 && on_screen) ? TAA_BLEND : 0.0;\n    return float4(lerp(cur, ycocg_to_rgb(hist), alpha), 1.0);\n}\n";
Expand description

taa.slang.