Screen-Space Reflection Edge Fade in GLSL
Screen-space reflections (SSR) produce harsh visual cutoffs when reflection rays traverse beyond the visible viewport borders. Implementing boundary vignette attenuation solves this issue by computing an attenuation factor based on the distance between the projected sample UVs and the screen boundaries, smoothly fading the reflection color to zero as it approaches the viewport perimeter.
The Mathematical Formulas
There are two primary mathematical approaches used in GLSL to calculate screen-space boundary attenuation:
1. Distance-to-Edge with Smoothstep (Separable)
This approach computes the linear distance to the nearest edge on
each axis independently, applies a Hermite interpolation
(smoothstep) across an edge threshold, and multiplies the
axes together.
Given UV coordinates \(\mathbf{uv} \in [0, 1]^2\) and a fade margin \(m \in (0, 0.5]\):
\[\mathbf{d} = \min(\mathbf{uv}, 1.0 - \mathbf{uv})\] \[\mathbf{fade} = \text{smoothstep}(0.0, m, \mathbf{d})\] \[\text{attenuation} = \mathbf{fade}_x \cdot \mathbf{fade}_y\]
2. Normalized Device Coordinate (NDC) Chebyshev Metric
Alternatively, you can transform the UVs into normalized coordinates centered at the origin \([-1, 1]\), compute the maximum absolute deviation (Chebyshev distance), and fade out toward the outer edge.
Given UV coordinates \(\mathbf{uv} \in [0, 1]^2\) and a fade start threshold \(t \in [0, 1)\):
\[\mathbf{ndc} = |\mathbf{uv} \cdot 2.0 - 1.0|\] \[d_{\max} = \max(\mathbf{ndc}_x, \mathbf{ndc}_y)\] \[\text{attenuation} = 1.0 - \text{smoothstep}(t, 1.0, d_{\max})\]
GLSL Implementations
Method 1: Separable Edge Attenuation
This function calculates smooth fading individually on both the X and Y axes, producing rounded corner falloffs.
/**
* Calculates vignette attenuation for screen edges.
* @param uv Normalized screen-space texture coordinates in [0, 1].
* @param edgeMargin The thickness of the fade zone (typically 0.05 to 0.2).
* @return Attenuation factor in the range [0.0, 1.0].
*/
float calculateSSREdgeFade(vec2 uv, float edgeMargin) {
vec2 edgeDist = min(uv, vec2(1.0) - uv);
vec2 fade = smoothstep(vec2(0.0), vec2(edgeMargin), edgeDist);
return fade.x * fade.y;
}Method 2: Center-Offset Maximum Bound Fade
This method measures the distance from the center of the viewport to the nearest outer edge using normalized device coordinates.
/**
* Calculates vignette attenuation based on maximum offset from the screen center.
* @param uv Normalized screen-space texture coordinates in [0, 1].
* @param fadeStart Normalized distance from center where fade begins [0.0, 1.0].
* @return Attenuation factor in the range [0.0, 1.0].
*/
float calculateSSREdgeFadeChebyshev(vec2 uv, float fadeStart) {
vec2 ndc = abs(uv * 2.0 - 1.0);
float maxDist = max(ndc.x, ndc.y);
return clamp(1.0 - smoothstep(fadeStart, 1.0, maxDist), 0.0, 1.0);
}Applying the Vignette to Reflections
In an SSR shader pipeline, the attenuation factor scales the incoming reflected radiance before compositing with the scene color:
vec2 hitUV = rayMarchResult.xy;
bool hit = rayMarchResult.z > 0.0;
if (hit) {
vec3 reflectionColor = texture(sceneColorTexture, hitUV).rgb;
// Compute boundary fade
float edgeFactor = calculateSSREdgeFade(hitUV, 0.1);
// Combine with other attenuation factors (e.g., ray distance, roughness)
float finalAlpha = edgeFactor * hitConfidence;
outColor = mix(baseColor, reflectionColor, finalAlpha);
}This ensures that any reflection ray intersecting scene geometry near the edges of the frame diminishes smoothly without popping or displaying clipping artifacts.