PAJAMA / RELIGHT

02 / 08 · RELIGHTING

Count the light, one direction at a time

Build the rendering equation from multiplication and addition.

A tilted card catches less light

n is a unit arrow out of the surface; l points from the surface toward the light. Their dot product is cos θ: multiply matching x, y, z components and add. Straight toward the light gives 1; sideways gives 0. Negative means light is behind the surface, so clamp it to 0.

For n=(0,0,1), l=(0.6,0,0.8), the dot product is 0×0.6+0×0+1×0.8=0.8. A fixed beam spreads over a larger tilted area; this projected-area factor gives the patch 80% of the perpendicular illumination.

q=max⁡(0,n⋅l)=max⁡(0,nxlx+nyly+nzlz)q=\max(0,\mathbf n\cdot\mathbf l)=\max(0,n_xl_x+n_yl_y+n_zl_z)

Why divide by π?

Reflectance ρ is the fraction of incident power reflected by a diffuse surface. Its Lambert BRDF is ρ/π. Outgoing directions share the reflected energy: integrating the cosine over a hemisphere gives π, so division by π makes the total reflected fraction exactly ρ.

Here is the integral in small steps. Integrating azimuth φ from 0 to 2π gives 2π. For the angle from the normal θ, substitute u=sin θ in ∫cos θ sin θ dθ. Between 0 and π/2 it becomes [u²/2]₀¹=1/2. Multiply: 2π×1/2=π.

∫02π∫0π/2cos⁡θsin⁡θ dθ dϕ=π,fd=ρπ\int_0^{2\pi}\int_0^{\pi/2}\cos\theta\sin\theta\,d\theta\,d\phi=\pi,\qquad f_d=\frac{\rho}{\pi}

From one light to the hemisphere

For our ideal distant directional light, E⊥ is irradiance on a patch facing the light. Pure Lambert radiance is (ρ/π)E⊥q. With ρ=0.6, E⊥=2, q=0.8, it is 0.6×2×0.8/π≈0.3056. This is linear radiance, not a display byte. Our GGX lab additionally reserves Fresnel energy, multiplying diffuse by (1−F).

Split the sky into small solid-angle patches Δω. Each contributes BRDF × incident radiance × visibility × cosine × patch size. Add them all; shrinking patches turns the sum into an integral. Visibility V is 0 for a blocked direction and 1 for a clear one.

Lo(x,v)=Le+∫Ω+fr(x,l,v)Li(x,l)V(x,l)max⁡(0,n⋅l) dωL_o(\mathbf x,\mathbf v)=L_e+\int_{\Omega^+}f_r(\mathbf x,\mathbf l,\mathbf v)L_i(\mathbf x,\mathbf l)V(\mathbf x,\mathbf l)\max(0,\mathbf n\cdot\mathbf l)\,d\omega

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

What happens to pure Lambert radiance when q changes from 0.8 to 0.4?

Reveal the reasoning

With material and light fixed, it halves. This applies before nonlinear tone mapping.

Primary references for this chapter

PBRT 4 · light transport equation ↗