Physically Based Rendering (PBR)
PBR is the standard shading model for modern real-time graphics. Instead of arbitrary "diffuse + specular + shininess" sliders, it models light the way physics does — microfacets, Fresnel reflectance, and energy conservation — using just metalness and roughness as inputs.
1. The Rendering Equation
James Kajiya (1986) formulated the master equation that every physically correct renderer must satisfy:
L_o — outgoing radiance in direction ωo
L_e — emitted radiance
f_r — BRDF: bidirectional reflectance distribution function
L_i — incoming radiance from direction ωi
ωi · n̂ = cos θ (Lambert cosine term)
Solving this integral exactly requires path tracing. Real-time PBR approximates it with: (1) direct punctual lights, and (2) precomputed environment maps for the diffuse and specular integrals.
2. Microfacet Theory
At a microscopic level, no surface is a perfect mirror. Every surface is a collection of tiny flat microfacets, each a perfect mirror, but randomly oriented. The distribution of microfacet orientations defines the macroscopic appearance.
A rough surface has many facets pointing in random directions → large, diffuse-looking highlights. A smooth surface has facets nearly all aligned with the surface normal → tiny, sharp specular "hotspots".
Roughness α² = roughness², where roughness ∈ [0, 1] is the artist input. Two effects reduce the visible reflecting facets:
- Masking: A microfacet is blocked by neighboring facets from the camera direction.
- Shadowing: A microfacet lies in the shadow of neighboring facets from the light direction.
3. The Cook-Torrance BRDF Terms
The microfacet specular BRDF (Cook-Torrance) has three terms:
D — Normal Distribution Function (how many facets face h)
G — Geometry/Masking term (self-shadowing)
F — Fresnel term (reflectance vs angle)
D(h, α) = α² / (π · ((n·h)²(α²−1) + 1)²). Most popular: looks great, fast to sample.
G(n,v,k) = (n·v) / ((n·v)(1−k)+k), k = α²/2 (direct light). Applied separately for view + light dirs.
F = F₀ + (1 − F₀)(1 − cosθ)⁵. Simple, accurate. F₀ is reflectance at normal incidence.
The full BRDF = specular Cook-Torrance + diffuse Lambert,
weighted by the Fresnel term: diffuse contribution is (1 − F) × albedo / π.
Metals absorb all refracted light → no diffuse term.
4. Fresnel and F₀
F₀ is the reflectance at normal incidence (0° angle of incidence). This is the "base reflectance" of the surface:
- Non-metals (dielectrics): F₀ ≈ 0.02–0.08 (water: 0.02, glass: 0.04, skin: 0.028)
- Common metals: F₀ ≈ 0.50–0.95 and is chromatic (different R, G, B). Gold: (1.0, 0.71, 0.29). Copper: (0.95, 0.64, 0.54).
At glancing angles (near 90°), everything is highly reflective — you
can see sky reflections on a flat lake even though water has low F₀.
This is the Fresnel effect, and the (1 − cosθ)⁵ power
law is a remarkably good approximation for most materials.
5. Metalness–Roughness Workflow
Artists use three texture maps. This is the "metalness workflow" used by Three.js, Unreal Engine, Unity, Blender, and glTF:
- Albedo / Base Colour (RGB): For dielectrics: the diffuse colour. For metals: the specular colour (F₀). Stored in sRGB.
- Roughness (R, single channel): 0 = mirror, 1 = completely rough diffuse-like specular. Stored as linear.
- Metalness (R, single channel): 0 = dielectric, 1 = metal. Usually binary — no partially-metallic materials exist physically (except smudge on chrome).
Additional optional maps: Normal, Ambient Occlusion, Emissive, Height.
6. Image-Based Lighting (IBL)
Punctual lights (point, directional, spot) only cover a fraction of real-world lighting. IBL uses an HDR environment map as an infinite sphere of area lights. The rendering equation integral is split:
- Diffuse irradiance map: Pre-convolve the environment map with a cosine-weighted hemisphere integral → blurred cube map. Sample with the world normal to get the diffuse contribution.
- Specular prefiltered map: Pre-convolve at multiple roughness levels (mipmaps). Sample with the reflected view vector.
-
BRDF LUT: Precomputed 2D lookup table (NdotV × roughness)
storing the integral of (G × F) over the hemisphere — the
"split-sum" approximation (Epic Games, 2013). Combine at runtime:
specular = prefiltered.rgb * (F₀ * BRDF.r + BRDF.g)
All three textures are computed once offline. Real-time IBL from static environments is essentially "free" in terms of per-frame cost.
7. GLSL PBR Shader
Core Cook-Torrance BRDF functions in GLSL ES 3.0:
const float PI = 3.14159265359;
// GGX Normal Distribution Function
float D_GGX(float NdotH, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float d = (NdotH * NdotH) * (a2 - 1.0) + 1.0;
return a2 / (PI * d * d);
}
// Smith-Schlick Geometry Function
float G_SchlickGGX(float NdotV, float roughness) {
float r = roughness + 1.0;
float k = (r * r) / 8.0;
return NdotV / (NdotV * (1.0 - k) + k);
}
float G_Smith(float NdotV, float NdotL, float roughness) {
return G_SchlickGGX(NdotV, roughness)
* G_SchlickGGX(NdotL, roughness);
}
// Fresnel-Schlick
vec3 F_Schlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
// Full Cook-Torrance BRDF (single directional light)
vec3 PBR(vec3 N, vec3 V, vec3 L,
vec3 albedo, float roughness, float metalness) {
vec3 H = normalize(V + L);
float NdotV = max(dot(N, V), 0.0001);
float NdotL = max(dot(N, L), 0.0);
float NdotH = max(dot(N, H), 0.0);
float VdotH = max(dot(V, H), 0.0);
// F0: dielectric = 0.04, metal = albedo
vec3 F0 = mix(vec3(0.04), albedo, metalness);
float D = D_GGX(NdotH, roughness);
float G = G_Smith(NdotV, NdotL, roughness);
vec3 F = F_Schlick(VdotH, F0);
vec3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001);
vec3 kD = (1.0 - F) * (1.0 - metalness); // metals: no diffuse
vec3 diffuse = kD * albedo / PI;
return (diffuse + specular) * NdotL;
}
THREE.ShaderMaterial and
include Three's built-in GLSL chunks via
#include <lights_pars_begin> to get scene lights automatically.