# ==Physically Based Rendering==
<p class="doc-sub">// status: seedling</p>
"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-vector `h`. 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 approximation `F0 + (1 - F0)(1 - cosθ)^5` is 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
```glsl
float D_GGX(float NoH, float a) {
float a2 = a * a;
float d = (NoH * a2 - NoH) * NoH + 1.0;
return a2 / (PI * d * d);
}
vec3 F_Schlick(float VoH, vec3 F0) {
return F0 + (1.0 - F0) * pow(1.0 - VoH, 5.0);
}
float V_SmithGGXCorrelated(float NoV, float NoL, float a) {
float a2 = a * a;
float v = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
float l = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
return 0.5 / (v + l);
}
```
`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; `F0` is the coloured reflectance (gold, copper, silver). Diffuse term is zero.
The ==metallic workflow== packs this: one `baseColor` texture, one `metallic` mask. At shade time:
```glsl
vec3 F0 = mix(vec3(0.04), baseColor, metallic);
vec3 diffuse = mix(baseColor, vec3(0.0), metallic);
```
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:
1. Prefilter the environment cube map for a range of roughnesses (mip chain).
2. Precompute a 2D BRDF integration LUT indexed by `(NoV, roughness)`.
3. At shade time: sample prefiltered env at `reflect(-V, N)` with mip = f(roughness); multiply by `F0 * 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 `*_SRGB` so the sampler decodes automatically.
- Normal maps, roughness, metallic, AO → upload as `*_UNORM` or `*_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](https://google.github.io/filament/Materials.html) — 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](https://blog.selfshadow.com/publications/)
# Things that tripped me up
- **Linear vs sRGB in textures** — explained above; costs me an hour every new project.
- **`roughness` vs `α`** — `α = 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 [[Index|Notes]] · see also [[Deferred vs forward rendering]] · [[Shadow mapping]]