Three.js From Zero · Article s5-01
S5-01 Global Illumination
Global Illumination — light that bounces
Direct lighting is "light hits surface, lights it." GI is "that surface then lights everything nearby." It's the difference between sterile and photographic.
1. What GI gives you
Stand in a red room. Everything picks up red. Your face, the ceiling, your clothes. That tint is indirect lighting — photons bouncing off the red walls and hitting you. Without GI, a red room illuminated by a white bulb leaves your face pale and the shadows pitch black. Wrong.
Every realistic render has GI. The question is only: how much do you pre-compute vs. compute live?
2. The rendering equation (one line, infinite depth)
L_out(x, ω_o) = L_emit(x, ω_o) + ∫ f_r(x, ω_i, ω_o) · L_in(x, ω_i) · cos θ dω_i
Light leaving point x in direction ωo = emission + integral over all incoming directions ωi, weighted by BRDF and angle. The catch: L_in is itself the output of other points. Recursive. That's GI.
3. The menu
| Technique | Precompute | Runtime cost | Dynamic |
|---|---|---|---|
| Lightmap (baked) | Hours (baker) | Texture sample | Static geometry only |
| Light probes (SH) | Minutes | 9 dot products | Dynamic objects, static scene |
| Irradiance volume / DDGI | None (runtime) | Grid lookup | Full dynamic |
| SDFGI (Godot) | Seconds | Raymarch SDF | Mostly dynamic |
| SSGI (screen-space) | None | Per-pixel ray | Only what's on screen |
| Path tracing | None | Brutal | Everything |
4. Lightmaps
Classic. Bake indirect lighting into a 2nd UV set per mesh. Offline path-tracer fills texels with incoming irradiance. At runtime: one texture lookup.
Pros: stupidly fast, looks incredible, dynamic range all baked. Cons: static geometry only, long bake, big textures.
Unreal's Lightmass, Blender's bake, three-gpu-pathtracer to bake for Three.js.
5. Light probes (spherical harmonics)
Place probes around the scene. Each stores incoming light from all directions — but compressed into 9 floats using spherical harmonics.
vec3 irradianceSH(vec3 N) {
return sh[0] // constant
+ sh[1]*N.y + sh[2]*N.z + sh[3]*N.x
+ sh[4]*N.x*N.y + sh[5]*N.y*N.z + sh[6]*(3*N.z*N.z-1)
+ sh[7]*N.x*N.z + sh[8]*(N.x*N.x - N.y*N.y);
}
Dynamic object samples nearest probes, trilinear-interpolates. Diffuse GI for characters in pre-baked scenes.
6. DDGI / irradiance volume (runtime)
Grid of probes. Each frame, a few probes fire N rays into the scene, hit something, integrate shading, update. Over frames, irradiance propagates.
That's what RTXGI (NVIDIA) and Godot 4 SDFGI compute at runtime. Near-fully dynamic. Needs ray tracing or SDF proxy.
7. Screen-space GI (SSGI / SSDO)
Cheaper. Per pixel, raymarch in screen space (like SSR) to find nearby surfaces. Sample their color. Accumulate.
- Free if you already have SSR infrastructure.
- Only sees what's currently on screen. Offscreen light doesn't bounce in.
- Great for contact GI (dark corners near the camera).
8. Live demo — GI on/off
A Cornell-ish box you can light with direct-only vs. a fake probe-style indirect bounce. Watch the walls bleed color onto the white diffuse object.
9. The "baked" trick in Three.js right now
If you don't need dynamic lights moving, bake once offline with three-gpu-pathtracer into a lightmap. Load the lightmap texture, plug into the material's lightMap slot. Full GI look, zero runtime cost.
loader.load('scene.gltf', (gltf) => {
gltf.scene.traverse(o => {
if (o.isMesh) {
o.material.lightMap = bakedLightmap;
o.material.lightMapIntensity = 1.0;
}
});
});
10. Takeaways
- GI = light bouncing. It's what makes scenes look photographic.
- Lightmaps: best quality, static only, long bake.
- Probes / DDGI: dynamic, lower-frequency but free.
- SSGI: screen-space tricks for contact GI.
- Path tracing: the ground truth, but expensive.