Article Rendering Algorithms · ≈ ⏱ 12 min read

PBR: Cook-Torrance BRDF Explained

Almost every physically based renderer — from Unreal Engine to a three-line Three.js MeshStandardMaterial — computes its specular highlight with the same 1982 formula. Here is exactly what D, F and G mean, why they were chosen, and how to write them in GLSL (OpenGL Shading Language, the code that runs on the graphics card to compute pixel colours).

TL;DR: Every modern game and 3D renderer shades shiny surfaces with the same 1982 Cook-Torrance formula, which multiplies three terms — D (how many microscopic facets point the right way), G (how many are shadowed or blocked), and F (how much light reflects at that angle) — to get a physically accurate highlight, using GGX, Smith and Fresnel-Schlick as the standard building blocks.

1. Why a microfacet model?

No real surface is perfectly smooth at the microscopic scale. Under a microscope, even polished metal is a landscape of tiny facets, each acting like a perfect mirror pointed in a slightly different direction. Microfacet theory treats a surface point as a statistical distribution of these facets rather than modelling each one individually — which is exactly what makes it tractable in a real-time fragment shader.

Only facets whose normal h (the half-vector between the view direction v and the light direction l) happens to align with the true geometric normal can reflect light straight into the camera. A rough surface has facet normals scattered widely → broad, dim highlight. A smooth surface has them tightly clustered → a small, bright highlight. This single idea explains why roughness controls both the size and the brightness of a specular highlight.

Where it comes from

Robert Cook and Kenneth Torrance published "A Reflectance Model for Computer Graphics" in 1982, adapting earlier work by Torrance and Sparrow (1967) from optical engineering. It stayed a niche offline-rendering technique for 25 years until Disney's 2012 "principled BRDF" and the GGX distribution made it the default choice for real-time PBR engines (Unreal 4, Unity HDRP, Filament, Three.js).

2. The Cook-Torrance formula

The specular term of the Cook-Torrance BRDF is a fraction with three factors on top and a normalisation term on the bottom:

Cook-Torrance specular BRDF fspec(l, v) = (D(h) · G(l, v, h) · F(v, h)) / (4 · (n·l) · (n·v))
  • D — the Normal Distribution Function: what fraction of microfacets are oriented exactly along the half-vector h?
  • G — the Geometry term: what fraction of those facets are not shadowed by neighbouring facets on the way in, or masked on the way out?
  • F — the Fresnel term: what fraction of light is reflected (rather than refracted/absorbed) at this viewing angle?
  • 4·(n·l)·(n·v) — a normalisation factor that corrects for the change of measure between the microfacet-normal space and the outgoing-direction space.

The final shaded colour also needs a diffuse (Lambertian) term for the light that refracts into the surface, scatters, and exits again — see section 7.

3. D — Normal Distribution Function (GGX)

Cook and Torrance originally used a Beckmann distribution; modern engines almost universally use GGX (also called Trowbridge-Reitz), because it has longer "tails" — it fades out more slowly at grazing angles, matching measured real-world materials far better and producing the soft, glowing highlight edges characteristic of physically based renders.

GGX / Trowbridge-Reitz NDF D(h) = α² / (π · ((n·h)² · (α² − 1) + 1)²)

де α = roughness² (перцептивне маппування — squaring roughness gives more intuitive slider control for artists, popularised by Disney's principled BRDF)
float distributionGGX(vec3 N, vec3 H, float roughness) {
  float a      = roughness * roughness;
  float a2     = a * a;
  float NdotH  = max(dot(N, H), 0.0);
  float denom  = (NdotH * NdotH * (a2 - 1.0) + 1.0);
  denom = PI * denom * denom;
  return a2 / max(denom, 0.0000001);
}

4. G — Geometry (Smith Shadowing-Masking)

Even a facet aligned to reflect light towards the camera might be blocked: shadowing stops light reaching it from the light source, and masking stops reflected light reaching the camera. The Smith model factors this into two independent terms multiplied together, each using the same "Schlick-GGX" building block:

Smith geometry term G(n, v, l, k) = G1(n, v, k) · G1(n, l, k)

G1(n, x, k) = (n·x) / ((n·x) · (1 − k) + k)

де k = (roughness + 1)² / 8   (direct lighting, per Karis/Epic 2013)
float geometrySchlickGGX(float NdotX, float k) {
  return NdotX / (NdotX * (1.0 - k) + k);
}

float geometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
  float r  = (roughness + 1.0);
  float k  = (r * r) / 8.0;
  float ggx1 = geometrySchlickGGX(max(dot(N, V), 0.0), k);
  float ggx2 = geometrySchlickGGX(max(dot(N, L), 0.0), k);
  return ggx1 * ggx2;
}

At grazing angles both dot products shrink towards zero, so G correctly darkens the highlight — this is what stops rough spheres from looking like glowing discs at their silhouette.

5. F — Fresnel (Schlick approximation)

The Fresnel effect says every material becomes more reflective at grazing angles — this is why a still lake looks like a mirror near the horizon but transparent looking straight down. The exact Fresnel equations involve the material's index of refraction and are expensive; Christophe Schlick's 1994 approximation is accurate to within a fraction of a percent and costs one pow():

Fresnel-Schlick F(v, h) = F₀ + (1 − F₀) · (1 − (v·h))⁵

F₀ is the reflectance at normal incidence (looking straight at the surface). This is where the metalness workflow plugs in: dielectrics (plastic, wood, skin) have a low, achromatic F₀ around 0.04, while metals have a high, tinted F₀ equal to their albedo colour (gold ≈ (1.0, 0.71, 0.29)).

F₀ = mix(vec3(0.04), albedo, metalness)
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
  return F0 + (vec3(1.0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}

6. Putting it together in GLSL

A minimal single-light Cook-Torrance fragment shader combining all three terms plus a Lambertian diffuse component:

vec3 cookTorrance(vec3 N, vec3 V, vec3 L,
                     vec3 albedo, float roughness, float metalness,
                     vec3 radiance) {
  vec3  H  = normalize(V + L);
  vec3  F0 = mix(vec3(0.04), albedo, metalness);

  float D  = distributionGGX(N, H, roughness);
  float G  = geometrySmith(N, V, L, roughness);
  vec3  F  = fresnelSchlick(max(dot(H, V), 0.0), F0);

  vec3  kS = F;                       // specular contribution ratio
  vec3  kD = (vec3(1.0) - kS) * (1.0 - metalness); // metals have no diffuse

  float NdotL = max(dot(N, L), 0.0);
  float NdotV = max(dot(N, V), 0.0);
  vec3  numerator   = D * G * F;
  float denominator = 4.0 * NdotV * NdotL + 0.0001;
  vec3  specular = numerator / denominator;

  return (kD * albedo / PI + specular) * radiance * NdotL;
}
Multiple lights & IBL

For a full scene you accumulate this function's result over every direct light, then add an image-based lighting (IBL) term from a pre-filtered environment cubemap for the ambient contribution — the diffuse irradiance map and the specular pre-filtered mip chain plus a BRDF integration LUT (split-sum approximation, Karis 2013).

7. Energy conservation and the diffuse term

A physically plausible material must not reflect more light than it received. The code above enforces this with kD = (1 - kS) * (1 - metalness): whatever fraction of light isn't reflected specularly (kS = F) is available for diffuse scattering, and metals get zero diffuse because all the light that enters a conductor is absorbed by free electrons almost immediately — metals have no subsurface scattering.

The Lambertian albedo / π diffuse term is itself an approximation — real diffuse reflectance is not perfectly uniform in all directions (Oren-Nayar is a rougher-surface alternative) — but for most PBR use cases the error is imperceptible next to the specular term's contribution.

🔺 See ray marching render surfaces live

The Ray Marching simulation lets you tweak lighting on SDF surfaces in real time — a good sandbox for experimenting with roughness and Fresnel by eye.

Read Ray Marching article →

🔗 Related Articles

🎨PBR Theory 🌈Path Tracing 🔺Ray Marching 🎞️TAA