Disney Burley Diffuse Model GLSL Implementation

This article provides a technical breakdown and complete GLSL shader implementation of the Disney Burley diffuse BRDF (bidirectional reflectance distribution function). Unlike standard Lambertian reflectance, Brent Burley’s model captures surface roughness phenomena, notably retro-reflection at grazing angles and edge darkening on smooth surfaces. Readers will learn the underlying mathematical formulation and how to translate it into an efficient, real-time GLSL fragment shader.

The Mathematics Behind Burley Diffuse

Standard Lambertian diffuse assumes an ideal matte surface that reflects light uniformly in all directions (\(f_{\text{diffuse}} = \frac{\rho}{\pi}\)). However, physical materials with microscopic roughness exhibit retro-reflection—microfacets reflect light back along the incident light direction, causing brightened silhouettes when grazing angles align. Conversely, smooth surfaces exhibit a diffuse Fresnel falloff, where diffuse reflectance drops at grazing angles as more energy enters specular reflection.

The Disney Burley diffuse model interpolates between diffuse retro-reflection for rough surfaces and diffuse Fresnel falloff for smooth surfaces using the Schlick Fresnel approximation.

The model is defined as:

\[f_{\text{diffuse}} = \frac{\text{baseColor}}{\pi} \cdot F_D(N \cdot L) \cdot F_D(N \cdot V)\]

Where:

  • \(L\) is the normalized light vector.
  • \(V\) is the normalized view vector.
  • \(N\) is the normalized surface normal.
  • \(H = \frac{L + V}{\|L + V\|}\) is the normalized half-vector.
  • \(\theta_d\) is the difference angle, computed via the dot product \(\cos \theta_d = L \cdot H = V \cdot H\).

The grazing retro-reflection factor \(F_{D90}\) is determined by linear roughness:

\[F_{D90} = 0.5 + 2 \cdot \text{roughness} \cdot (\cos \theta_d)^2\]

Each Fresnel response term is calculated using Schlick's approximation with \(F_0 = 1\) and \(F_{90} = F_{D90}\):

\[F_D(N \cdot L) = 1 + (F_{D90} - 1)(1 - N \cdot L)^5\] \[F_D(N \cdot V) = 1 + (F_{D90} - 1)(1 - N \cdot V)^5\]

When roughness is zero, \(F_{D90} = 0.5\), producing a reduction in diffuse scattering at grazing angles (darkening). When roughness is high, \(F_{D90} > 1.0\) (up to \(2.5\)), creating significant brightening at grazing angles (retro-reflection).


GLSL Implementation

Below is the GLSL function implementing the Disney Burley diffuse BRDF. It clamps all dot products to avoid negative illumination artifacts and safely handles cases where \(L\) and \(V\) oppose each other.

#define PI 3.14159265358979323846

/**
 * Calculates the Disney Burley diffuse response.
 *
 * @param N          World-space surface normal (normalized)
 * @param V          World-space view vector pointing toward camera (normalized)
 * @param L          World-space light vector pointing toward light source (normalized)
 * @param roughness  Linear perceptual roughness [0.0, 1.0]
 * @param baseColor  Albedo / diffuse color of the material
 * @return           Reflected diffuse radiance
 */
vec3 DisneyBurleyDiffuse(vec3 N, vec3 V, vec3 L, float roughness, vec3 baseColor) {
    // Half-vector between view and light directions
    vec3 H = normalize(L + V);

    // Compute clamped dot products
    float NdotL = clamp(dot(N, L), 0.0, 1.0);
    float NdotV = clamp(dot(N, V), 0.0, 1.0);
    float LdotH = clamp(dot(L, H), 0.0, 1.0);

    // Calculate the grazing retro-reflection response
    float energyBias = 0.5;
    float energyFactor = mix(0.0, energyBias, roughness);
    float Fd90 = energyBias + 2.0 * roughness * (LdotH * LdotH);

    // Schlick approximations for incident and view angles
    float lightScatter = 1.0 + (Fd90 - 1.0) * pow(1.0 - NdotL, 5.0);
    float viewScatter  = 1.0 + (Fd90 - 1.0) * pow(1.0 - NdotV, 5.0);

    // Combined diffuse BRDF
    return (baseColor / PI) * lightScatter * viewScatter;
}

Integration in a Fragment Shader

To use the model in a forward lighting pipeline, multiply the returned BRDF value by the incident light irradiance (\(E = \text{lightColor} \cdot (N \cdot L)\)):

void main() {
    // Normalization ensures interpolation artifacts do not skew vectors
    vec3 N = normalize(vNormal);
    vec3 V = normalize(uCameraPosition - vWorldPos);
    vec3 L = normalize(uLightPosition - vWorldPos);

    float NdotL = max(dot(N, L), 0.0);

    // Compute Burley diffuse reflection
    vec3 diffuseBRDF = DisneyBurleyDiffuse(N, V, L, uRoughness, uBaseColor);

    // Final diffuse contribution
    vec3 directDiffuse = diffuseBRDF * uLightColor * NdotL;

    fragColor = vec4(directDiffuse, 1.0);
}

Performance Optimization

The fifth power pow(1.0 - x, 5.0) can be optimized for real-time GPUs to reduce arithmetic overhead:

float fL = 1.0 - NdotL;
float fL2 = fL * fL;
float fL5 = fL2 * fL2 * fL; // Equivalent to pow(1.0 - NdotL, 5.0)

Applying this algebraic expansion eliminates expensive transcendent function calls on mobile or low-power hardware while preserving exact visual output.