SSDO Diffuse Bounce Light Accumulation in GLSL
Screen-Space Directional Occlusion (SSDO) improves upon traditional ambient occlusion by accounting for the directionality of incoming light and approximating single-bounce indirect diffuse transport. In GLSL, SSDO accumulates diffuse bounce light by treating occluding geometry within a local screen-space hemisphere as secondary light emitters. Rather than simply darkening an occluded fragment, the shader samples neighboring surface elements, determines their direct illumination from a light pre-pass, and projects that radiance back onto the receiving surface using an analytical diffuse form factor.
The Mechanism of SSDO Bounce Light
Traditional Screen-Space Ambient Occlusion (SSAO) computes a scalar visibility factor by checking if sample points within a hemisphere lie inside geometry. SSDO extends this concept: when a sample point is found to be occluded by depth-buffer geometry, that occluding point is considered visible from the current receiver fragment.
If direct lighting has already been calculated and stored in a color buffer (or G-buffer emissive/lighting pass), the occluder's stored color represents the outgoing radiance toward the receiver. The shader accumulates this bounce light by evaluating the transfer between two oriented surface patches (the occluder and the receiver).
Required Input Buffers
To calculate bounce light in a GLSL fragment shader, the pass requires:
- View-Space Position / Linear Depth Buffer: To reconstruct 3D sample coordinates.
- View-Space Normal Buffer: Normals for both the receiver fragment and the occluding sample fragments.
- Direct Radiance / Albedo Buffer: The lit scene color containing direct diffuse light from the previous rendering pass.
- Kernel Samples: A set of randomized 3D hemisphere vectors scaled to a defined sampling radius.
GLSL Implementation
The accumulation process occurs within a screen-space sampling loop. The receiver fragment generates sample positions along its hemisphere, projects them into texture coordinates, and compares the projected depth with the actual depth in the buffer.
#version 450 core
in vec2 vTexCoord;
out vec4 FragColor;
uniform sampler2D uPositionMap; // View-space positions
uniform sampler2D uNormalMap; // View-space normals
uniform sampler2D uDirectLight; // Pre-pass lit color
uniform sampler2D uNoiseTex; // Random rotation vectors
uniform vec3 uKernel[32];
uniform int uKernelSize = 32;
uniform float uRadius = 0.5;
uniform mat4 uProjection;
void main()
{
vec3 receiverPos = texture(uPositionMap, vTexCoord).xyz;
vec3 receiverNormal = normalize(texture(uNormalMap, vTexCoord).xyz);
// Randomize hemisphere rotation
vec2 noiseScale = textureSize(uPositionMap, 0) / textureSize(uNoiseTex, 0);
vec3 randomVec = normalize(texture(uNoiseTex, vTexCoord * noiseScale).xyz);
vec3 tangent = normalize(randomVec - receiverNormal * dot(randomVec, receiverNormal));
vec3 bitangent = cross(receiverNormal, tangent);
mat3 TBN = mat3(tangent, bitangent, receiverNormal);
vec3 indirectDiffuse = vec3(0.0);
for (int i = 0; i < uKernelSize; ++i)
{
// Reorient kernel vector to receiver's hemisphere
vec3 samplePos = receiverPos + (TBN * uKernel[i]) * uRadius;
// Project sample to screen space
vec4 offset = uProjection * vec4(samplePos, 1.0);
offset.xy /= offset.w;
offset.xy = offset.xy * 0.5 + 0.5;
// Read occluder geometry
vec3 occluderPos = texture(uPositionMap, offset.xy).xyz;
vec3 occluderNormal = normalize(texture(uNormalMap, offset.xy).xyz);
vec3 occluderRadiance = texture(uDirectLight, offset.xy).rgb;
// Vector from receiver to occluder
vec3 dir = occluderPos - receiverPos;
float distSq = dot(dir, dir);
vec3 L = normalize(dir);
// Check if sample lies within geometry (occlusion condition)
// and enforce a valid range threshold to avoid bleeding across distant geometry
if (occluderPos.z >= samplePos.z + 0.01 && sqrt(distSq) < uRadius)
{
// Lambertian transfer: dot receiver normal with L,
// and dot occluder normal with -L (facing each other)
float NdotL = max(dot(receiverNormal, L), 0.0);
float NoccDotMinusL = max(dot(occluderNormal, -L), 0.0);
// Geometric form factor with distance attenuation
float attenuation = 1.0 / (1.0 + distSq);
float formFactor = NdotL * NoccDotMinusL * attenuation;
indirectDiffuse += occluderRadiance * formFactor;
}
}
// Average the accumulated bounce light
indirectDiffuse /= float(uKernelSize);
FragColor = vec4(indirectDiffuse, 1.0);
}Mathematical Weighting and Edge Handling
The integration depends on three mathematical terms evaluated inside the loop:
- Receiver Geometric Term (
NdotL): Ensures bounce light only illuminates surfaces facing the occluder. - Emitter Geometric Term
(
NoccDotMinusL): Restricts bounce light so that only surfaces facing the receiver emit light toward it. This prevents the backsides of walls or objects from falsely bleeding color forward. - Distance Falloff: An attenuation factor (such as inverse-square or inverse-linear falloff) dampens energy transfer over longer distances, preventing harsh halo effects.
Because this calculation occurs purely in screen space, bounce light cannot be accumulated from geometry outside the field of view or obscured by closer foreground objects. Consequently, SSDO diffuse bounce implementations typically combine this screen-space accumulation with a low-frequency ambient probe or spherical harmonics fallback to maintain lighting consistency when camera angles obscure local light emitters.