Jimenez Multi-Bounce Ambient Occlusion in GLSL

This article provides an overview and technical breakdown of Jorge Jimenez’s multi-bounce ambient occlusion (AO) approximation, detailing its mathematical formulation and GLSL implementation. Standard single-bounce screen-space ambient occlusion algorithms often yield overly dark and desaturated contact shadows because they neglect secondary light bounces within cavities. Jimenez’s analytical approximation models these interreflections directly from single-bounce visibility and surface albedo, restoring realistic color saturation and energy conservation to ambient lighting with negligible computational overhead.

The Core Problem

Standard ambient occlusion calculates the visibility factor \(V \in [0, 1]\) representing the fraction of the hemisphere unoccluded by geometry:

\[I = I_{\text{ambient}} \cdot \rho \cdot V\]

where \(\rho\) is the surface albedo. This formulation assumes light that hits occluding surfaces is completely absorbed. In reality, light bounces repeatedly within concave geometric features, scattering colored irradiance back onto nearby surfaces. Assuming zero bounces causes cavities to appear excessively dark and grey, completely stripping away the chrominance of the material.

Mathematical Formulation

To solve this without expensive stochastic ray tracing, Jorge Jimenez fitted a cubic polynomial to ground-truth path-traced interreflections inside spherical cavities across varying albedo values.

The multi-bounce factor \(V_{\text{multi}}\) is computed as a function of the single-bounce visibility \(V\) (the scalar ambient occlusion value) and the surface albedo vector \(\rho \in [0, 1]^3\).

1. Polynomial Coefficients

The coefficients \(a\), \(b\), and \(c\) are linear functions of the surface albedo \(\rho\):

\[a = 2.0404 \cdot \rho - 0.3324\]

\[b = -4.7951 \cdot \rho + 0.6417\]

\[c = 2.7552 \cdot \rho + 0.6903\]

Because \(\rho\) is a three-component color vector (RGB), \(a\), \(b\), and \(c\) evaluate to three-component vectors, allowing each color channel to reflect light according to its respective wavelength reflectance.

2. Cubic Evaluation

The multi-bounce visibility factor is evaluated via Horner's method:

\[f(V, \rho) = ((a \cdot V + b) \cdot V + c) \cdot V\]

Expanded, this represents:

\[f(V, \rho) = a V^3 + b V^2 + c V\]

3. Lower Bound Enforcement

To prevent numerical artifacts or cases where dark albedos cause the multi-bounce value to drop below the initial single-bounce occlusion, the final multi-bounce visibility is clamped using a component-wise maximum:

\[V_{\text{multi}} = \max(V, f(V, \rho))\]

The resulting vector \(V_{\text{multi}}\) replaces the scalar \(V\) in your diffuse ambient lighting calculation:

\[I = I_{\text{ambient}} \cdot V_{\text{multi}}\]

Notice that the albedo term is already folded into this multi-bounce approximation when evaluating total ambient irradiance, though some pipelines multiply \(V_{\text{multi}}\) against existing diffuse ambient terms depending on their formulation. In Jimenez's standard formulation, \(V_{\text{multi}}\) acts as an ambient occlusion multiplier that colors and brightens occluded areas.

GLSL Implementation

Below is the optimized GLSL function for modern rendering pipelines:

/**
 * Approximates multi-bounce ambient occlusion.
 * 
 * @param visibility Single-bounce AO factor in the range [0.0, 1.0].
 * @param albedo     Surface diffuse albedo (base color) in linear RGB space.
 * @return           Multi-bounce visibility color modifier (vec3).
 */
vec3 JimenezMultiBounceAO(float visibility, vec3 albedo) {
    vec3 a = 2.0404 * albedo - 0.3324;
    vec3 b = -4.7951 * albedo + 0.6417;
    vec3 c = 2.7552 * albedo + 0.6903;
    
    vec3 multiBounce = ((a * visibility + b) * visibility + c) * visibility;
    
    return max(vec3(visibility), multiBounce);
}

Integration in Shading Passes

To integrate this into a deferred or forward lighting shader:

// Sample or compute single-bounce AO (e.g., from GTAO, SSAO, or an AO map)
float rawAO = texture(aoTexture, texCoord).r;

// Surface diffuse color (must be in linear color space)
vec3 diffuseAlbedo = texture(albedoTexture, texCoord).rgb;

// Evaluate multi-bounce ambient occlusion
vec3 multiBounceAO = JimenezMultiBounceAO(rawAO, diffuseAlbedo);

// Apply to ambient indirect diffuse lighting
vec3 ambientLighting = indirectDiffuseLight * multiBounceAO;

When albedo is near zero (absorbing surfaces), \(V_{\text{multi}}\) smoothly defaults back to the single-bounce scalar \(V\). When albedo is high and saturated (such as bright skin, foliage, or painted surfaces), the higher-order terms preserve irradiance inside geometric crevices while tinting the occluded regions with the natural bounce color of the surface.