🖥️ Graphics & Rendering · Shading
📅 April 2026⏱ 16 min🔴 Advanced

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); }

8. Extensions: Clear Coat, Subsurface Scattering, Anisotropy

Clear Coat (Varnish, Car Paint)

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:

// Two-layer clear coat BRDF: brdf_clearcoat = clearcoatIntensity · D_GGX(H, α=0.001) · G · F_clearcoat Lo = (diffuse + specular_base) * (1 - F_clearcoat) + specular_clearcoat

Anisotropic Reflections (Brushed Metal, Hair)

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.