# ==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]]