PAJAMA / RELIGHT

03 / 08 · RELIGHTING

Build a moving highlight

Read every factor in the actual GGX shader.

Imagine tiny mirrors

A rough surface has tiny facets pointing in different directions. Facets facing the halfway vector h between light l and viewer v reflect that light toward the viewer. Normalize l+v to get h. D describes their orientation density; G discounts tiny facets hidden by others; F describes angle-dependent reflectance.

These are model choices, not consequences of Gaussian density. We use single-scattering GGX, separable Smith masking and Schlick Fresnel. PBRT explains the full statistical derivation; here we unpack the exact shader model.

h=l+v∥l+v∥,fs=DFG4(n⋅l)(n⋅v)\mathbf h=\frac{\mathbf l+\mathbf v}{\|\mathbf l+\mathbf v\|},\qquad f_s=\frac{DFG}{4(\mathbf n\cdot\mathbf l)(\mathbf n\cdot\mathbf v)}

Roughness controls the spread

The slider is perceptual roughness r. Square it to get a=r², then use a²=r⁴ in D. Let nh=max(n·h,0). Compute nh², multiply by (a²−1), add 1, square, multiply by π, then divide a² by that result. Lower roughness makes a narrower, taller highlight.

At r=0.5 and nh=1, a=0.25, a²=0.0625, D=1/(π×0.0625)≈5.093. D is a density, not a color, so values above 1 are valid. We clamp r to at least 0.12 because this coarse surfel scene cannot resolve near-perfect mirror highlights.

a=r2,D=a2π[(n⋅h)2(a2−1)+1]2,G1(q)=2qq+a2+(1−a2)q2,G=G1(nl)G1(nv)a=r^2,\quad D=\frac{a^2}{\pi[(\mathbf n\cdot\mathbf h)^2(a^2-1)+1]^2},\quad G_1(q)=\frac{2q}{q+\sqrt{a^2+(1-a^2)q^2}},\quad G=G_1(nl)G_1(nv)

Split diffuse and specular energy

For a typical dielectric choose normal-incidence reflectance F₀=0.04. The metallic workflow interpolates it toward base color b as metallic m approaches 1. Schlick adds a grazing-angle increase using (1−v·h)⁵. This is an approximation; 0.04 is not universal.

The diffuse factor (1−F)(1−m) removes diffuse reflection at m=1. The lab isolates Diffuse and Specular before display mapping. It includes no subsurface scattering, transmission or microfacet multiple-scattering compensation.

F0=(1−m)0.04+mb,F=F0+(1−F0)(1−v⋅h)5,fd=(1−F)(1−m)bπF_0=(1-m)0.04+m\mathbf b,\quad F=F_0+(1-F_0)(1-\mathbf v\cdot\mathbf h)^5,\quad f_d=\frac{(1-F)(1-m)\mathbf b}{\pi}

Work through one numerical example

Take n=l=v=(0,0,1), r=0.5, m=0, red base color bᵣ=0.55, E⊥=3. Then h=n, G=1, F=0.04. Specular BRDF =5.093×0.04/4≈0.05093; diffuse BRDF =0.96×0.55/π≈0.16807. Red outgoing radiance =(0.05093+0.16807)×1×3≈0.6570.

Predict it, then run it

Open this experiment →

The formula meets actual code

This excerpt is extracted from the repository during the site build. SH environment lighting and inverse training are future extensions; their chapters point to the current direct-light/data boundary, not an implemented trainer.

src/relight.wgsl · L24–54
fn masking(cosine: f32, a2: f32) -> f32 {
    return 2.0 * cosine / max(cosine + sqrt(a2 + (1.0-a2)*cosine*cosine), 1e-6);
}
fn shade(s: Splat) -> vec3<f32> {
    let n = normalize(s.normal.xyz);
    let v = normalize(g.eye.xyz - s.center.xyz);
    let l = g.light.xyz;
    let mode = u32(g.material.z);
    if mode == 3u { return n * 0.5 + 0.5; }
    if mode == 4u { return s.base.xyz; }
    let nl = max(dot(n,l), 0.0);
    let nv = max(dot(n,v), 0.0);
    if nl <= 0.0 || nv <= 0.0 { return vec3(0.0); }
    let h = normalize(l+v);
    let nh = max(dot(n,h), 0.0);
    let vh = max(dot(v,h), 0.0);
    let a = g.material.x * g.material.x;
    let a2 = a*a;
    let denominator = nh*nh*(a2-1.0)+1.0;
    let d = a2 / (3.14159265359 * denominator * denominator);
    let f0 = mix(vec3(0.04), s.base.xyz, g.material.y);
    let fresnel = f0 + (1.0-f0)*pow(1.0-vh, 5.0);
    let geometry = masking(nl,a2)*masking(nv,a2);
    let specular = d*fresnel*geometry / max(4.0*nl*nv, 1e-6);
    let diffuse = (1.0-fresnel)*(1.0-g.material.y)*s.base.xyz/3.14159265359;
    var brdf = diffuse+specular;
    if mode == 1u { brdf = diffuse; }
    if mode == 2u { brdf = specular; }
    // One white distant light, no occlusion, no indirect illumination.
    return brdf * nl * g.light.w;
}

Check your understanding

Why can metal have colored highlights and zero diffuse response here?

Reveal the reasoning

Base color becomes F₀, while (1−metallic) removes the diffuse lobe. A colored dielectric instead has colored diffuse and mostly neutral specular reflection under white light.

Primary references for this chapter

PBRT 4 · microfacet derivation ↗

Filament · PBR engineering notes ↗