Physically Based Rendering
// status: seedling
"PBR" as shipped in modern engines is roughly 30% physics, 70% convention. The physics gives it consistency across lighting conditions; the conventions make it authorable by artists. These are the bits I keep coming back to.
The rendering equation, in one paragraph
Outgoing radiance L_o at a point, in direction ω_o, equals emission plus the hemispherical integral of incoming radiance L_i weighted by the BRDF f_r and the cosine term:
L_o(\mathbf{x}, \omega_o) = L_e + \int_{\Omega} f_r(\mathbf{x}, \omega_i, \omega_o) \, L_i(\mathbf{x}, \omega_i) \, (\omega_i \cdot \mathbf{n}) \, d\omega_i
Everything in real-time PBR is either (a) picking a good f_r, or (b) approximating that integral.
The microfacet BRDF
The model used by basically every game engine since ~2013. The idea: a macro-scale surface is made of tiny mirror-like microfacets. How rough the surface is tells you how scattered their orientations are. Three factors:
f_r = \frac{D(h) \, F(\omega_o, h) \, G(\omega_i, \omega_o, h)}{4 \, (\omega_i \cdot \mathbf{n})(\omega_o \cdot \mathbf{n})}
D(h)— Normal Distribution Function. How many microfacets point toward the half-vectorh. GGX (Trowbridge-Reitz) won. It has nice tails that match measured materials better than Beckmann or Phong.F— Fresnel. How much light reflects vs refracts at the interface. Schlick's approximationF0 + (1 - F0)(1 - cosθ)^5is cheap and close enough.G— geometric term / shadowing-masking. How much microfacets occlude each other. Smith height-correlated is the current default; old uncorrelated Smith is still common.
The denominator normalises the BRDF correctly.
GGX in shader code
float
vec3
float a = roughness * roughness — Disney's remap, now ubiquitous. It gives perceptually linear roughness to authors.
Metals vs dielectrics
PBR's cleanest split. Two material behaviours, one model:
- Dielectrics (wood, plastic, skin, stone) —
F0 ≈ 0.04(grey), diffuse albedo is coloured, specular is white-ish at grazing. - Metals — no diffuse;
F0is the coloured reflectance (gold, copper, silver). Diffuse term is zero.
The metallic workflow packs this: one baseColor texture, one metallic mask. At shade time:
vec3 F0 = ;
vec3 diffuse = ;Alternative: specular/glossiness workflow with explicit F0. More flexible but more foot-guns; the metallic workflow is better for most asset pipelines.
Energy conservation
The BRDF integrated over the hemisphere must be ≤ 1 (can't reflect more energy than arrives). In practice:
- Kulla-Conty / multi-scattering compensation — GGX loses energy at high roughness because it only models single-bounce microfacet scattering. Add a compensating term; Filament's implementation is the reference.
- Split-sum approximation (Epic UE4 course notes) — the classic IBL trick that splits the lighting integral into two precomputable parts.
Without this, rough metals look unnaturally dark.
Image-based lighting (IBL)
For ambient/environment lighting:
- Prefilter the environment cube map for a range of roughnesses (mip chain).
- Precompute a 2D BRDF integration LUT indexed by
(NoV, roughness). - At shade time: sample prefiltered env at
reflect(-V, N)with mip = f(roughness); multiply byF0 * LUT.r + LUT.g. Add diffuse from an irradiance cube map.
This is the split-sum. It's everywhere.
Colour, gamma, and why it matters
Materials are authored in sRGB, lit in linear, displayed in sRGB. Three choices you must make consistently:
- Albedo/base colour textures → upload as
*_SRGBso the sampler decodes automatically. - Normal maps, roughness, metallic, AO → upload as
*_UNORMor*_RGBA8. They're data, not colour. - Render into a linear HDR target (usually
R16F_G16F_B16F_A16F), tonemap at the end, output to sRGB.
Almost every "why does my metal look wrong" bug is one of the three. See also OpenGL - learning log on sRGB framebuffers.
Tonemapping
Linear HDR → sRGB [0,1] via a tonemapping curve. Common choices:
- Reinhard — simplest, desaturates highlights.
- ACES — filmic, current default for games. Stephen Hill's fit is what everyone uses.
- AgX — newer, perceptually better behaviour in highlights.
- Uncharted 2 — Hable's old curve, still looks good.
Applied after all lighting, before the gamma encode. Pair with exposure control.
Things worth reading
- Real Shading in Unreal Engine 4 — Karis / Epic (SIGGRAPH 2013). The paper that productised modern PBR.
- Physically-Based Shading at Disney — Burley 2012. The paper that broke the clutter.
- Filament materials guide — Google's open-source engine docs, the clearest modern reference.
- Moving Frostbite to Physically Based Rendering — Lagarde & de Rousiers. Production-grade treatment of everything from BRDF to area lights.
- Self-Shadow — shading course archives
Things that tripped me up
- Linear vs sRGB in textures — explained above; costs me an hour every new project.
roughnessvsα—α = roughness². Mixing them gives subtly wrong highlights.- Negative NoL — clamp
max(dot(N, L), 0.0). Backfacing lights should contribute zero. - Area lights — point lights are a simplification. Real-time area lights need Linearly Transformed Cosines (Heitz et al.) — hard to derive, easy to copy.
- Clearcoat, anisotropy, subsurface — the "extensions" to the core model. Add them only when assets need them; each one multiplies the shading cost.
Back to Notes · see also Deferred vs forward rendering · Shadow mapping