Computer Graphics · Shading Models
The Split-Sum Approximation for Specular Image-Based Lighting
How a prefiltered environment map plus a two-channel BRDF lookup table replaces a per-shading-point integral, what assumptions the factorization hides, and where the grazing-angle error appears.
Specular reflection from an environment map is an integral per shading point:
$$L_o(\omega_o) = \int_{\Omega} f_r(\omega_i, \omega_o), L_i(\omega_i), (\omega_i \cdot n), d\omega_i$$
Doing that integral at every pixel is possible but expensive, because the environment is a texture and the integral has thousands of directions with non-negligible weight for a smooth lobe. The split-sum approximation reduces it to a texture fetch from a prefiltered mip chain multiplied by a value read from a small two-dimensional table. The cost collapses to one or two samples, and the price is a set of assumptions about the relationship between the surface normal and the view direction that do not hold in general.
Why the integral resists table lookup
An environment-lit specular surface depends on five things: the incoming radiance distribution, the roughness, the surface normal, the view direction, and the Fresnel parameters. Precomputing the whole thing would require a table indexed by all five, which is not feasible in any useful resolution.
The natural set of assumptions to reduce the dimensionality is to fix the view direction relative to the normal, then precompute everything else. If the view direction is assumed equal to the normal, the integral depends only on the normal, the roughness, and the material’s Fresnel parameters, while the environment radiance depends only on the normal direction. That is one three-dimensional table per environment, plus a two-dimensional table for the Fresnel-dependent factor.
The approximation is that the specular lobe is evaluated using the reflection direction and roughness, with no dependence on how grazing the view is relative to the normal. Under that assumption, the prefiltered environment map stores the average radiance visible through a lobe of a given width, and the BRDF table stores the fraction of that radiance the material reflects.
But dropping the view dependence from the prefilter would leave the result wrong for all non-normal viewing, so the remaining dependence is recovered by the second factor, which is computed exactly.
The factorization
Write the Fresnel term in Schlick form, F = F₀ + (1 − F₀)(1 − cos θ_h)⁵, which is linear in F₀. The integral is then linear in F₀ as well, so it splits into two integrals with the same non-Fresnel part:
$$L_o \approx F_0 \underbrace{\int f_{\text{non-F}} , L_i \cos\theta_i , d\omega_i}{\text{scale}} + \underbrace{\int f{\text{non-F}} (1 - \cos\theta_h)^5 , L_i \cos\theta_i , d\omega_i}_{\text{bias}}$$
Each of these is a product of an environment-dependent part and a material-dependent part only if the two can be separated, which they cannot exactly. The approximation that makes it work is to replace the coupled integral by a product of two separately computed integrals:
$$L_o \approx \left[ \int L_i(\omega_i), D(\omega_i), (\omega_i \cdot n), d\omega_i \right] \cdot \left[ \int f_r, (\omega_i \cdot n), d\omega_i \right]$$
The first bracket is the prefiltered environment: a convolution of the incoming radiance with the distribution function D at the chosen roughness. The second is the BRDF integral, evaluated with the incoming radiance factored out as uniform, which makes it a function of roughness, the cosine of the view angle, and F₀ only.
Products of integrals are not equal to integrals of products. The error this introduces is the whole story of the method’s quality.
Building the prefiltered map
The prefiltered environment is stored as a mip chain, where each successive level holds the environment convolved with a wider distribution. Level zero is the original, and the highest level corresponds to roughness one, where the distribution is very broad.
Each texel of a level is computed by importance-sampling D at the level’s roughness, with the view direction assumed equal to the normal. With D = GGX and the standard half-vector sampling, the sample count per texel is typically in the hundreds to low thousands for a small map; variance at very rough levels is high, because the lobe covers nearly a hemisphere and few samples land anywhere useful. Weighting samples by the solid angle ratio between the sample direction and the direction being written, or by the mip level appropriate to the sample’s distance, reduces the aliasing that otherwise appears as sparkle on smooth surfaces.
The chain’s roughness-to-level mapping is a choice, not a physical law. A common option maps perceptual roughness linearly to mip level, which concentrates resolution at low roughness where the lobe is sharp and the environment detail is most visible. Whatever the mapping, the prefiltering code and the runtime lookup must use the same function; a mismatch produces a surface whose roughness slider changes the reflection sharpness at a slightly wrong rate, which reads as a material that never quite matches its neighbours.
// Prefiltered environment lookup: choose a mip from perceptual roughness and
// sample along the reflection direction with a level-bias correction.
vec3 specularIBL(sampler2D prefiltered, sampler2D brdfLUT,
vec3 N, vec3 V, float perceptualRoughness, vec3 F0) {
vec3 R = reflect(-V, N);
float lod = perceptualRoughness * float(MAX_MIP);
vec3 env = textureLod(prefiltered, dirToUV(R), lod).rgb;
float NoV = max(dot(N, V), 0.0);
vec2 ab = texture(brdfLUT, vec2(NoV, perceptualRoughness)).rg;
return env * (F0 * ab.x + ab.y); // F0 * scale + bias
}
The reflection vector is computed from the shading normal, and the lookup uses the direction with no regard for where along the surface the reflection lands. For a flat mirror this is exact. For a rough surface in a non-uniform environment, the prefiltered value at the reflection direction is the average radiance over the lobe around that direction, which is what the prefilter computed. The direction is right; what is missing is the variation of the lobe’s shape with view angle.
The BRDF table
The second factor is a two-dimensional function of the view-cosine and the roughness, with two channels: the scale multiplying F₀ and the bias added to it. It is generated once, offline, by Monte Carlo integration of the BRDF with a uniform environment, using the same sampling routine as the prefilter so that the two agree.
The table’s resolution requirements are modest. The variation across roughness is smooth, and the variation along the view-cosine is smooth except near the extremes: at NoV close to zero, the integrand’s behavior changes quickly, and at roughness zero the function approaches a delta. A table of 128 by 128 is generous; smaller tables with bilinear filtering are usually indistinguishable in a rendered frame, with the exception of low-roughness, grazing-angle pixels where the interpolation error shows as a thin bright band.
The table is environment-independent, which is why it can be shared across every material and every environment in a scene. Only the prefiltered chain depends on the environment. That separation is the main practical advantage of the approach: one texture per environment, one shared table for the whole project.
/* One texel of the split-sum table: A multiplies F0 and B is added to it.
NoV is the cosine between normal and view, both in a tangent frame. */
static void brdf_lut_texel(double NoV, double alpha, int n,
double *A, double *B) {
double sumA = 0.0, sumB = 0.0;
double V[3] = { sqrt(1.0 - NoV * NoV), 0.0, NoV };
for (int i = 0; i < n; i++) {
double u1 = (i + 0.5) / n; /* stratified first dimension */
double u2 = radical_inverse_vdc(i); /* van der Corput radical inverse, base 2 */
double H[3];
sample_ggx_vndf(alpha, u1, u2, V, H); /* as in the sampling article */
double VoH = V[0]*H[0] + V[1]*H[1] + V[2]*H[2];
double L[3];
for (int j = 0; j < 3; j++) L[j] = 2.0 * VoH * H[j] - V[j];
double NoL = L[2], NoH = H[2];
if (NoL <= 0.0 || NoH <= 0.0) continue;
/* Smith G, then the change of variables factor VoH / (NoH NoV).
lambda_ggx is the Smith Lambda for GGX used by the visibility term. */
double li = lambda_ggx(NoV, alpha), lo = lambda_ggx(NoL, alpha);
double vis = VoH / ((1.0 + li + lo) * NoH * NoV);
double Fc = pow(1.0 - VoH, 5.0);
sumA += (1.0 - Fc) * vis; /* multiplies F0 */
sumB += Fc * vis; /* added to F0 * A */
}
*A = sumA / n;
*B = sumB / n;
}
The two channels are easy to swap, and swapping them inverts the meaning of the table: a material that looks correct for metals and wrong for dielectrics, or the reverse. The second axis is whichever roughness parameter the generator used, α or the perceptual value, and the lookup must index it with the same one. The safe check is a white furnace: render a white dielectric under a uniform environment and confirm the specular reflection matches the analytic Fresnel albedo.
Where the error lives
The dominant error is the n = v assumption baked into the prefilter. It matters most at grazing angles and at high roughness, where the lobe’s shape depends strongly on the view direction; the lobe becomes stretched along the view direction and the true reflected radiance differs from the prefiltered average by a factor that grows toward the silhouette.
A second error comes from splitting the integral into a product. For a bright, small source in an otherwise dark environment, the two factors are strongly correlated: the prefiltered average already includes the bright region’s contribution, and multiplying by the BRDF’s average overcounts or undercounts it depending on where the source is relative to the lobe. The visible symptom is a highlight that survives to too-high roughness values, or disappears too early.
A third is the interaction with the multiple-scattering correction. The standard BRDF table contains only single-scattering energy. For rough metals the missing multiple-scattering energy must be added, and its added cost is one more table or an analytic fit multiplied into the same expression. Skipping it turns a rough metal dark exactly as it does in direct lighting, which is the cheapest way to tell whether a pipeline has been assembled correctly.
The approach also assumes the environment is distant. A large local emitter, a neighbouring object, or a rough surface reflecting a nearby wall breaks the assumption that the incoming radiance is the same over the surface’s visible hemisphere, and the prefiltered lookup cannot see the occlusion at all. This is the point at which the method stops approximating global illumination and starts approximating a distant-lighting integral, and the probe-based alternatives take over.
References
- Real Shading in Unreal Engine 4 — Brian Karis, SIGGRAPH 2013 Course: Physically Based Shading in Theory and Practice
- Moving Frostbite to Physically Based Rendering — Sébastien Lagarde, Charles de Rousiers, SIGGRAPH 2014 Course
- Physically Based Rendering: From Theory to Implementation — Matt Pharr, Wenzel Jakob, Greg Humphreys