Three.js From Zero · Article s4-02

Image-Based Lighting Internals

← threejs-from-zeroS4 · Article 02 Season 4
Article S4-02 · Three.js From Zero

Image-Based Lighting Internals

S4-01 handled direct lights. But real scenes get ambient light from everywhere — sky, walls, nearby bounces. You can't represent that with a single directional light. The answer: image-based lighting. Store the full environment in a cubemap, then sample it correctly for every surface.

Three.js's PMREMGenerator (from S1-04) does this. This article is what it's actually doing — the split-sum approximation, the prefiltering, the roll-your-own version.

loading…

The split-sum approximation

The full environment-lighting integral is expensive to compute at runtime:

L_out = ∫ BRDF(ωᵥ, ωₗ) · L_env(ωₗ) · (n·ωₗ) dωₗ

Too slow. Karis (UE4) proposed splitting it into two precomputed parts:

L_out ≈ ( ∫ L_env · weight ) · ( ∫ BRDF · weight )
           ↑ prefiltered envmap       ↑ BRDF LUT

Both halves are precomputed offline (at PMREM generation time). Runtime just samples two textures and multiplies.

Half 1 — prefiltered specular envmap

For a given roughness, pre-integrate the specular reflection over the environment. Low roughness → sharp mirror-like cubemap. High roughness → blurred. Store as mipmaps in one cubemap; roughness selects the mip level.

mip 0: roughness = 0.0 (sharp)
mip 1: roughness = 0.25
mip 2: roughness = 0.5
mip 3: roughness = 0.75
mip 4: roughness = 1.0 (maximally blurred)

PMREM does this on the GPU in milliseconds.

Half 2 — the BRDF LUT

A 256×256 2D texture encoding the BRDF response at every combination of (NdotV, roughness). Two channels: a scale for F0 and a bias for (1 - F0). Same for every scene; ship once.

Runtime sampling

// Sample prefiltered environment at roughness-weighted mip
float mipLevel = roughness * float(NUM_MIPS - 1);
vec3  prefilteredSpec = textureCubeLod(uEnvMap, reflect(-V, N), mipLevel).rgb;

// Sample BRDF LUT
vec2  brdf = texture2D(uBRDFLUT, vec2(NoV, roughness)).rg;

// Combine
vec3  specularIBL = prefilteredSpec * (F0 * brdf.x + brdf.y);

For the diffuse part, you also prefilter the environment for irradiance — a low-frequency version that approximates Lambertian integration:

vec3  irradiance = textureCube(uIrradianceMap, N).rgb;
vec3  diffuseIBL = irradiance * albedo / PI;

Energy conservation with IBL

The direct-lighting weights (S4-01) must be adjusted:

vec3  F = F_Schlick_Roughness(NoV, F0, roughness);
vec3  kS = F;
vec3  kD = (1.0 - kS) * (1.0 - metalness);

vec3  color = kD * diffuseIBL + specularIBL;

Note: F_Schlick_Roughness — a rough version that softens Fresnel at high roughness. Fisher-Price paper, from Sebastien Lagarde.

Three.js's PMREM pipeline

Inside PMREMGenerator.fromScene():

  1. Render the scene to a cubemap (6 faces)
  2. For each mip level 0..N:
    • Compute roughness for this level
    • GPU pass: importance-sample the lower mip, integrate GGX lobe
    • Write to this mip level

The fromEquirectangular entry point does the same but from a 2:1 HDR image instead of a cubemap. fromScene with RoomEnvironment gets you an OK generic studio lighting in ~5ms.

Common first-time pitfalls

  • Specular is too bright at rough values. BRDF LUT missing. The tail-end should darken.
  • Envmap seams visible. Each cubemap face was prefiltered separately without cross-face awareness. Use Three's PMREM, don't roll a naïve prefilter.
  • Envmap intensity = 0 → pitch black. PBR without env = pitch black. Add a tiny directional light.
  • Colors wrong. HDR envmap's color space must be linear, not sRGB. loader.type = THREE.HalfFloatType + linear workflow.

Exercises

  1. Roll your own PMREM: write a ShaderPass that takes a cubemap + roughness, outputs a prefiltered face. Importance-sample GGX.
  2. BRDF LUT bake: render a 256×256 texture with the split-sum's second half. Ship as KTX2. Compare to runtime fetch.
  3. Spherical harmonics diffuse: for low-frequency IBL diffuse, a 9-coefficient SH representation is ~100x cheaper than sampling an irradiance cubemap.

What's next

S4-03 — Shadow techniques. PCF, VSM, CSM, ray-traced. The full catalog of shadow maps.