Multi-Depth Mip SSGI Evaluation in GLSL

Evaluating Screen-Space Global Illumination (SSGI) across multiple depth mip levels leverages a hierarchical depth buffer (often referred to as Hi-Z) to accelerate ray traversal, reduce self-intersection, and efficiently trace rays across vast screen-space distances. By storing conservative depth bounds in a pyramid of downsampled depth textures, shaders can take large ray steps through coarse mips in empty space and drop to finer mips only when encountering potential geometry intersections. This article details the depth pyramid generation, the hierarchical traversal algorithm, and the GLSL implementation necessary to evaluate multi-depth mip SSGI.

Hierarchical Depth (Hi-Z) Pyramid Setup

To evaluate SSGI across multiple levels of detail, the raw depth buffer must be downsampled into a mip chain. Unlike standard color mipmaps that average texels, depth mipmaps must preserve conservative bounds to prevent rays from passing through geometry undetected:

  • Reversed-Z Buffers (1.0 near, 0.0 far): Use a min operator across the \(2 \times 2\) texels of the parent mip level. The minimum value represents the farthest surface in that cell.
  • Standard Z Buffers (0.0 near, 1.0 far): Use a max operator across the \(2 \times 2\) texels to represent the farthest surface.

This downsampling is executed sequentially via compute shaders or fragment passes using texelFetch to read the previous mip level, storing the result in the next level until reaching a \(1 \times 1\) texture.

Ray Traversal Mechanics Across Mip Levels

The hierarchical traversal operates similarly to a 2D Digital Differential Analyzer (DDA) coupled with depth testing:

  1. Cell Intersection: The ray travels along a vector in screen-space coordinates \((u, v, z)\). At any point, the ray resides within a discrete cell of the current mip level \(k\).
  2. Conservative Depth Test: The shader fetches the depth value from mip level \(k\) corresponding to the ray's current position. If the ray's depth is behind the conservative cell depth, a potential collision is detected.
  3. Refinement (Descent): Upon detecting a potential collision, the algorithm decreases the mip level (\(k = k - 1\)) and subdivides the cell into smaller bounds to test against higher-resolution geometry.
  4. Advance (Ascent): If the ray passes entirely in front of the cell's depth bounds, it cannot intersect any geometry within that cell. The ray is advanced to the boundary of the current cell, and the mip level can be increased (\(k = \min(k + 1, k_{\max})\)) to skip large empty regions in subsequent iterations.
  5. Termination: Traversal stops when a hit is confirmed at mip level 0, when the ray leaves the \([0, 1]\) screen-space bounds, or when the maximum iteration budget is exhausted.

GLSL Implementation

Below is a GLSL routine executing hierarchical depth ray marching. This example assumes standard normalized device coordinates with Reversed-Z depth values:

#version 460 core

layout(binding = 0) uniform sampler2D u_HiZBuffer;
layout(binding = 1) uniform sampler2D u_ColorBuffer;

const int MAX_MIP_LEVEL = 5;
const int MAX_ITERATIONS = 64;

// Marches through the Hi-Z buffer to locate an intersection
bool TraceHiZRay(vec3 rayOrigin, vec3 rayDir, out vec2 hitPixel, out vec3 hitPoint) {
    vec3 currentPos = rayOrigin;
    int currentMip = 0;
    
    // Scale direction to avoid division by zero
    vec3 dPos = rayDir;
    
    for (int i = 0; i < MAX_ITERATIONS; ++i) {
        // Sample conservative depth at current mip
        float cellDepth = textureLod(u_HiZBuffer, currentPos.xy, float(currentMip)).r;
        
        // Reversed-Z: cellDepth represents the minimum (farthest) point in the cell.
        // If ray depth is less than cell depth, the ray is behind the geometry.
        bool isBehind = currentPos.z <= cellDepth;

        if (isBehind) {
            if (currentMip == 0) {
                // Precise intersection found at base level
                // Thickness check to avoid false hits on back-facing surfaces
                float thickness = 0.005;
                if (cellDepth - currentPos.z < thickness) {
                    hitPixel = currentPos.xy;
                    hitPoint = currentPos;
                    return true;
                }
                // Step forward if outside thickness tolerance
                currentPos += dPos * 0.01;
            } else {
                // Drop to higher resolution mip
                currentMip--;
            }
        } else {
            // Find distance to the boundaries of the current cell
            vec2 mipResolution = vec2(textureSize(u_HiZBuffer, currentMip));
            vec2 cellIndex = floor(currentPos.xy * mipResolution);
            
            // Calculate entry into adjacent cell based on ray direction
            vec2 cellBoundary = (cellIndex + max(sign(dPos.xy), 0.0)) / mipResolution;
            vec2 tMax = (cellBoundary - currentPos.xy) / dPos.xy;
            float tAdvance = min(tMax.x, tMax.y);
            
            // Advance ray past the cell boundary with a small epsilon
            currentPos += dPos * (tAdvance + 0.0001);
            
            // Advance to a coarser mip level to accelerate traversal
            currentMip = min(currentMip + 1, MAX_MIP_LEVEL);
        }

        // Boundary check
        if (any(lessThan(currentPos.xy, vec2(0.0))) || any(greaterThan(currentPos.xy, vec2(1.0)))) {
            return false;
        }
    }
    
    return false;
}

Radiance Gathering and Shading

Once an intersection is verified at mip level 0, indirect radiance evaluation proceeds as follows:

  1. Texture Retrieval: The hit screen coordinate (hitPixel) is used to sample the incoming radiance from the previous frame’s resolved HDR color buffer.
  2. BRDF Calculation: Retrieve the world-space normal at the surface origin and compute the cosine factor (\(\mathbf{N} \cdot \mathbf{L}\)) between the hit vector and the hit texel normal.
  3. Screen-Edge Attenuation: Apply a fade factor to smooth out intersections near the screen boundaries where screen-space data becomes missing:
vec2 edgeFactor = smoothstep(0.0, 0.1, hitPixel) * (1.0 - smoothstep(0.9, 1.0, hitPixel));
float visibility = edgeFactor.x * edgeFactor.y;
vec3 indirectIllumination = texture(u_ColorBuffer, hitPixel).rgb * visibility;

This multi-depth mip method avoids fixed-step artifacts and reduces memory bandwidth overhead by evaluating coarse bounds before reading high-frequency depth data.