Physically Based Rendering (PBR): Theory and Practice
Every AAA game released since 2014 uses Physically Based Rendering.
The reason is compelling: a single set of material parameters —
albedo, roughness, metalness — produces consistent, believable results
across all lighting conditions, from bright sunlight to deep shadow.
PBR replaced ad-hoc Phong and Blinn-Phong shaders by grounding the
lighting model in the physics of light scattering at surfaces. This
article covers the rendering equation, microfacet theory, the
Cook-Torrance BRDF, the metalness/roughness workflow, and image-based
lighting — with full GLSL implementation.
1. The Rendering Equation
The rendering equation (Kajiya 1986) is the master equation of
photorealistic rendering. It describes the outgoing radiance from any
surface point in any direction as the sum of emitted radiance and
reflected radiance from all incoming directions:
// The Rendering Equation (Kajiya 1986) Lₒ(p, ωₒ) = Lₑ(p, ωₒ) + ∫_Ω
f(p, ωᵢ, ωₒ) Lᵢ(p, ωᵢ) (n·ωᵢ) dωᵢ Where: p = surface point ωₒ =
outgoing direction (toward camera) ωᵢ = incoming direction (from
light) Lₒ(p, ωₒ) = outgoing radiance (what we want to compute) Lₑ(p,
ωₒ) = emitted radiance (emissive materials: screens, lights) Lᵢ(p, ωᵢ)
= incoming radiance (from environment or lights) f(p,ωᵢ,ωₒ) = BRDF:
how much incoming light scatters toward ωₒ n·ωᵢ = cosine of angle
between normal and incoming light (Lambert factor) ∫_Ω = integral over
the upper hemisphere of directions The BRDF (Bidirectional Reflectance
Distribution Function): f : [sr⁻¹] — ratio of outgoing radiance to
incoming irradiance Must satisfy energy conservation: ∫_Ω
f(ωᵢ,ωₒ)(n·ωᵢ) dωᵢ ≤ 1 Must satisfy Helmholtz reciprocity: f(ωᵢ,ωₒ) =
f(ωₒ,ωᵢ)
The rendering equation is not directly solvable in real-time — the
hemisphere integral over all incoming directions contains Lᵢ, which
depends on the entire scene's geometry. Real-time PBR makes two
approximations: (1) evaluate only a finite number of lights
analytically, and (2) handle the environment contribution separately
via pre-computed image-based lighting.
2. Microfacet Theory
Real surfaces are not perfectly smooth at the microscopic level. Even
a polished metal surface has nanometre-scale roughness.
Microfacet theory treats a surface as a collection of
tiny, perfectly flat mirror-like facets, each with its own
orientation. The statistical distribution of these facet normals
determines the surface's apparent roughness.
// Microfacet concepts Macrosurface normal: N (the normal you'd
measure with a ruler) Microfacet normal: H (the half-vector between ωᵢ
and ωₒ) H = normalize(ωᵢ + ωₒ) Key insight: only microfacets whose
normal equals H contribute to reflection from ωᵢ to ωₒ (specular
reflection = mirror reflection) Roughness α: α = roughnessParameter²
(or just roughnessParameter, convention varies) α → 0: mirror surface
(all microfacets aligned → sharp highlight) α → 1: diffuse-like
(widely distributed normals → broad highlight) Three physical effects
described by the BRDF: 1. D(H) — Normal Distribution Function: what
fraction of microfacets point toward H? (controls highlight shape and
size) 2. G(ωᵢ, ωₒ) — Geometric Shadowing-Masking: some microfacets are
shadowed by others or mask reflected light 3. F(ωᵢ, H) — Fresnel:
reflectivity depends on viewing angle
3. BRDF Components: D, G, F
D — GGX Normal Distribution Function
The GGX (Trowbridge-Reitz) NDF is the industry standard, producing a
specular highlight with a sharper peak and longer tails than Beckmann
or Blinn-Phong — matching real-world measurements of rough surfaces
more accurately:
// GGX Normal Distribution Function D_GGX(N, H, α) = α² / (π · ((N·H)²
· (α²−1) + 1)²) Where: α = roughness² (perceptual roughness squared
for more linear feel) N·H = cosine of angle between surface normal and
half-vector Properties: - D = probability density of microfacets
oriented toward H - ∫_Ω D(H)(N·H) dH = 1 (by definition of a
distribution) - At α=0: delta function (perfect mirror — infinitely
sharp highlight) - At α=1: broad cosine-like distribution (very rough)
- "Long tail" vs Beckmann: more realistic for rough materials
G — Smith Geometry Function
// Smith Geometry Shadowing-Masking (Schlick approximation) //
Accounts for microfacets being shadowed (incoming) or masked
(outgoing) G_SchlickGGX(NdotV, α) = NdotV / (NdotV · (1−k) + k) where
k = α/2 (for direct lighting) k = (α+1)²/8 seems also popular // Full
Smith G (separable approximation): G_Smith(N, V, L, α) =
G_SchlickGGX(N·V, α) · G_SchlickGGX(N·L, α) // Combined with
denominator in Cook-Torrance: // The 1/(4(N·V)(N·L)) denominator is
often merged with G for stability
F — Fresnel (Schlick Approximation)
// Fresnel-Schlick approximation F_Schlick(F0, V, H) = F0 + (1 − F0) ·
(1 − V·H)⁵ Where: F0 = reflectivity at normal incidence (0° angle) =
((n₁ − n₂)/(n₁ + n₂))² (from Snell's law at normal incidence) Material
F0 values (approximate): Water: F0 = 0.02 (2% reflective head-on)
Glass: F0 = 0.04 Plastic: F0 = 0.04–0.06 Silver: F0 = vec3(0.95, 0.93,
0.88) (metallic — wavelength-dependent) Gold: F0 = vec3(1.00, 0.71,
0.29) (metallic — characteristic golden tint) Aluminium: F0 =
vec3(0.91, 0.92, 0.92) Fresnel effect: At grazing angles (V·H → 0):
all materials become nearly 100% reflective This is why edges of
objects always show strong specular highlights Why wet roads look
mirror-like when viewed from a shallow angle
4. Cook-Torrance BRDF in Full
// Cook-Torrance microfacet specular BRDF f_specular(ωᵢ, ωₒ) = D(H) ·
G(ωᵢ, ωₒ) · F(ωᵢ, H) ─────────────────────────────── 4 · (N·ωᵢ) ·
(N·ωₒ) // Diffuse BRDF (Lambertian): f_diffuse = c_diffuse / π
(constant, isotropic) // Energy conservation: kS + kD = 1 // kS = F
(Fresnel factor — fraction going to specular) // kD = (1 − F) · (1 −
metalness) (no diffuse for metals) // Metals: all absorbed light
becomes specular (F0 from albedo) // Dielectrics: diffuse albedo
contributes to kD term // Full BRDF: f(ωᵢ, ωₒ) = kD · f_diffuse + kS ·
f_specular // Reflection integral for a single punctual light: Lₒ =
(f_diffuse · kD + f_specular) · Lᵢ · (N·L) · lightColor // Importance:
the 4(N·ωᵢ)(N·ωₒ) denominator prevents energy gain // and converts
from half-vector space to light direction space
5. Metalness/Roughness Workflow
Disney (Burley 2012) popularised the artist-friendly
"metalness/roughness" parameterisation that most modern engines and
tools (Substance Painter, Blender, UE5, Unity) use:
// PBR texture maps in metalness/roughness workflow albedo
(baseColor): sRGB RGB — diffuse colour for dielectrics, reflective
colour for metals roughness: linear R — perceptual roughness ∈ [0,1]
metalness: linear R — 0=dielectric, 1=metal (usually binary) normal:
tangent-space normal map AO: ambient occlusion (pre-baked shadow
factor) emissive: self-emitting areas (screens, neon signs) //
Deriving F0 and diffuse albedo from artist inputs: vec3 F0 =
mix(vec3(0.04), albedo.rgb, metalness); // Dielectric: F0 = 0.04
(common plastic/non-metal default) // Metal: F0 = albedo colour (metal
specular IS coloured) vec3 diffuseAlbedo = albedo.rgb * (1.0 -
metalness); // Metal: no diffuse (free electrons absorb all
transmitted light) // Dielectric: diffuse = albedo // Perceptual
roughness → α (for D, G functions): float alpha = roughness *
roughness; // Disney "remapping"
Why roughness²? The perceptual roughness parameter is
squared before passing to the GGX formula. This remapping makes the
roughness slider feel more linear — without it, most of the material
variation would be crammed into roughness values 0–0.3, with the range
0.3–1.0 all looking similarly rough. Squaring spreads the perceptually
important range more evenly across the slider.
6. Image-Based Lighting (IBL)
Punctual lights (point, directional, spot) cannot represent the soft,
wrap-around illumination of an overcast sky or indoor environment.
Image-Based Lighting uses a high-dynamic-range environment map (cube
map or equirectangular) as a light source, effectively sampling
radiance from every direction simultaneously.
// The hemisphere integral for IBL: Lₒ(N, V) = ∫_Ω f(ωᵢ, ωₒ) Lᵢ(ωᵢ)
(N·ωᵢ) dωᵢ // Split-Sum Approximation (Epic Games, Brian Karis 2013):
// The integral is split into two precomputed parts: // Part 1:
Diffuse irradiance (Lambert term) // For each direction N: precompute
the diffuse integral // → Irradiance cube map (lowest mip of blurred
env map, or SH coefficients) vec3 irradiance = texture(irradianceMap,
N).rgb; vec3 diffuse = irradiance * albedo / PI; // Part 2: Specular
(split into environment sample + BRDF LUT) // Pre-filter the
environment map for each roughness level: // At roughness=0: sharp env
map (mip 0) // At roughness=1: heavily blurred env map (highest mip)
vec3 prefilteredColor = textureLod(prefilterMap, R, roughness *
MAX_MIPS).rgb; // BRDF integration LUT: 2D texture indexed by (N·V,
roughness) // stores (scale, bias) for the Fresnel term integral vec2
brdfLUT = texture(brdfLUTTexture, vec2(max(dot(N, V), 0.0),
roughness)).rg; // Combine: vec3 F =
fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3
specular = prefilteredColor * (F * brdfLUT.x + brdfLUT.y); vec3 kS =
F; vec3 kD = (1.0 - kS) * (1.0 - metalness); vec3 Lo = (kD * diffuse +
specular) * ao;
The pre-filtered environment map and the BRDF LUT are both precomputed
once (either offline or at application startup from the HDR
environment). This split-sum approximation makes IBL feasible in
real-time by separating the two halves of the original integral — one
that depends only on the environment, and one that depends only on the
BRDF — into textures that can be sampled cheaply in a fragment shader.
7. Full PBR GLSL Fragment Shader
// Compact PBR GLSL (direct lighting, no IBL) // Inputs: albedo,
roughness, metalness, normal, light data precision mediump float;
uniform vec3 uAlbedo; uniform float uRoughness, uMetalness; uniform
vec3 uLightPos, uLightColor, uCamPos; varying vec3 vWorldPos,
vWorldNormal; const float PI = 3.14159265; float DistributionGGX(vec3
N, vec3 H, float rough) { float a = rough * rough; float a2 = a * a;
float NdH = max(dot(N, H), 0.0); float d = NdH*NdH*(a2-1.0)+1.0;
return a2 / (PI * d * d); } float GeometrySmith(float NdV, float NdL,
float rough) { float k = (rough+1.0)*(rough+1.0)/8.0; float g1 = NdV /
(NdV*(1.0-k)+k); float g2 = NdL / (NdL*(1.0-k)+k); return g1 * g2; }
vec3 FresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0-F0) *
pow(clamp(1.0-cosTheta, 0.0, 1.0), 5.0); } void main() { vec3 N =
normalize(vWorldNormal); vec3 V = normalize(uCamPos - vWorldPos); vec3
L = normalize(uLightPos - vWorldPos); vec3 H = normalize(V + L); float
NdV = max(dot(N, V), 0.0); float NdL = max(dot(N, L), 0.0); vec3 F0 =
mix(vec3(0.04), uAlbedo, uMetalness); // Cook-Torrance specular float
D = DistributionGGX(N, H, uRoughness); float G = GeometrySmith(NdV,
NdL, uRoughness); vec3 F = FresnelSchlick(max(dot(H, V), 0.0), F0);
vec3 specular = D * G * F / max(4.0 * NdV * NdL, 0.001); // Lambertian
diffuse vec3 kD = (vec3(1.0) - F) * (1.0 - uMetalness); vec3 diffuse =
kD * uAlbedo / PI; // Final radiance vec3 Lo = (diffuse + specular) *
uLightColor * NdL; // Ambient (placeholder — replace with IBL) vec3
ambient = vec3(0.03) * uAlbedo; // Tone mapping + gamma correction
vec3 color = Lo + ambient; color = color / (color + vec3(1.0)); //
Reinhard tone mapping color = pow(color, vec3(1.0/2.2)); // linear →
sRGB gl_FragColor = vec4(color, 1.0); }
A two-layer BRDF for car paint, lacquered wood, and similar materials.
The clear coat layer is a smooth dielectric layer (F0=0.04,
roughness≈0.05) on top of the base material layer. The light interacts
with both layers:
Brushed metal and hair have microscale grooves along one direction,
causing the specular highlight to stretch tangentially rather than
being radially symmetric. Anisotropic BRDFs use separate roughness
values along the tangent (αT) and bitangent (αB) directions:
// Anisotropic GGX NDF (Burley 2012) D_aniso(H) = 1 / (π·αT·αB ·
((T·H/αT)² + (B·H/αB)² + (N·H)²)²) // αT = roughness along tangent
direction // αB = roughness along bitangent // A brushed metal:
αT=0.05 (smooth along brush), αB=0.4 (rough across)
Subsurface Scattering (Skin, Wax, Marble)
For translucent materials, light enters the surface, bounces around
inside, and exits at a different point. The standard real-time
approximation is
Screen-Space Subsurface Scattering (SSSSS): blur the
diffuse lighting in screen space with a Gaussian kernel weighted by
material thickness. The Disney Principled BSDF includes a dedicated
subsurface parameter that blends between Lambert diffuse and a
Hanrahan-Krueger subsurface model.
PBR in practice: The metalness/roughness workflow is
now universal. Both Unreal Engine (GGX + Smith + Schlick) and Unity
(same formulation) use virtually identical BRDF implementations. The
key differences between engines are in how they handle IBL, shadow
filtering quality, and the number of supported material layers rather
than the core BRDF math.
PBR shifted graphics from "looks good" to "looks physically plausible
regardless of lighting conditions" — a material created under studio
lighting in Substance Painter just works in outdoor sunlight,
underground caves, and neon-lit alleys without manual tweaking. That
consistency, more than any specific visual quality, is why PBR became
universal.