Temporal Reprojection for SSGI Denoising in GLSL
Screen Space Global Illumination (SSGI) produces physically plausible indirect lighting in real time, but low sample counts yield high-frequency Monte Carlo noise. To resolve this noise without incurring severe performance costs, modern rendering pipelines rely on Temporal Accumulation with Neighborhood Clamping (often integrated into the Spatiotemporal Variance-Guided Filtering, or SVGF, pipeline). This article details the mechanics of temporal reprojection history blending in GLSL, covering velocity-based reprojection, bilateral history validation, variance clipping to eliminate ghosting, and exponential moving average (EMA) blending.
Core Algorithm Overview
The primary algorithm that computes temporal reprojection history blending is the Exponential Moving Average (EMA) Temporal Filter with Color Bounding Box Clamping. Rather than evaluating dozens of rays per pixel each frame, the engine reprojects the previous frame’s accumulated indirect lighting into the current frame using screen-space motion vectors.
The algorithm executes in four primary steps:
- Motion Vector Reprojection: Compute the previous screen-space UV coordinate for the current pixel.
- History Validation: Compare current surface properties (depth and world-space normals) against the reprojected historical properties to detect disocclusions.
- Neighborhood Clamping/Clipping: Calculate the local color distribution in a \(3\times3\) neighborhood around the current pixel to clamp the history sample, preventing ghosting artifacts caused by dynamic lighting changes.
- Exponential Blending: Blend the clamped history with the current noisy frame based on an accumulation factor (\(\alpha\)).
Step-by-Step GLSL Implementation
1. Coordinate Reprojection
The shader uses the pixel's motion vector (typically stored in a G-Buffer target) to sample the previous frame’s indirect lighting buffer:
vec2 velocity = texture(u_VelocityTexture, v_TexCoord).xy;
vec2 historyUV = v_TexCoord - velocity;2. Disocclusion and Geometry Rejection
If an object moves to reveal a background surface (disocclusion), using the historical data will introduce severe trailing artifacts. The shader discards or reduces the historical weight if the depth or normal disparity exceeds a set threshold:
float currentDepth = texture(u_DepthTexture, v_TexCoord).r;
float historyDepth = texture(u_PrevDepthTexture, historyUV).r;
vec3 currentNormal = texture(u_NormalTexture, v_TexCoord).rgb;
vec3 historyNormal = texture(u_PrevNormalTexture, historyUV).rgb;
// Rejection test
bool depthValid = abs(currentDepth - historyDepth) < 0.01;
bool normalValid = dot(currentNormal, historyNormal) > 0.90;
bool historyValid = depthValid && normalValid &&
historyUV.x >= 0.0 && historyUV.x <= 1.0 &&
historyUV.y >= 0.0 && historyUV.y <= 1.0;3. Neighborhood Clamping (Anti-Ghosting)
When indirect lighting changes rapidly, the historical color might remain valid geometrically but invalid radiometrically. Sampling a \(3\times3\) kernel around the current pixel defines an axis-aligned bounding box (AABB) in color space:
vec3 currentRaw = texture(u_CurrentSSGI, v_TexCoord).rgb;
vec3 colorMin = currentRaw;
vec3 colorMax = currentRaw;
// Sample 3x3 neighborhood
for (int x = -1; x <= 1; ++x) {
for (int y = -1; y <= 1; ++y) {
vec2 offset = vec2(x, y) * u_TexelSize;
vec3 neighbor = texture(u_CurrentSSGI, v_TexCoord + offset).rgb;
colorMin = min(colorMin, neighbor);
colorMax = max(colorMax, neighbor);
}
}
vec3 historyColor = texture(u_HistorySSGI, historyUV).rgb;
// Clamp historical color to the local bounding box
historyColor = clamp(historyColor, colorMin, colorMax);(Note: Transforming RGB to YCoCg space before clamping yields significantly better chrominance preservation and tighter bounding bounds).
4. Exponential Moving Average (EMA) Blending
Once validated and clamped, the shader applies linear interpolation between the current noisy sample and the accumulated history:
// Alpha determines responsiveness vs. stability (typically 0.05 to 0.2)
float alpha = historyValid ? 0.1 : 1.0;
// Temporal accumulation computation
vec3 finalDenoisedSSGI = mix(historyColor, currentRaw, alpha);By computing this blend sequentially across frames, the temporal accumulation algorithm reduces variance proportionally to the accumulated frame count, stabilizing the raw SSGI output into a clean, noise-free indirect lighting buffer suitable for subsequent spatial filtering passes.