Three.js From Zero · Article s5-04
S5-04 Deferred Rendering
Deferred Rendering — separate geometry from lighting
Forward: shade every object against every light. N×M. Deferred: write geometry+material attributes to a G-buffer, then shade the G-buffer with all lights in one pass. Handles 100+ dynamic lights.
1. The problem with forward
For each mesh, for each fragment, shade against every light that affects it. 100 lights × 1M fragments × N meshes → cost explodes. Even with light culling, overdraw hurts.
2. Deferred in one paragraph
Pass 1 — Geometry pass: render scene, but instead of lighting, write out multiple targets (MRT):
- Albedo (base color)
- Normal (world or view)
- Depth (linear)
- Material (roughness, metalness, etc.)
Pass 2 — Lighting pass: for each light, draw a fullscreen quad (or light volume). Sample G-buffer, apply BRDF, accumulate.
// Geometry pass (GBuffer)
layout(location = 0) out vec4 gAlbedo;
layout(location = 1) out vec4 gNormal;
layout(location = 2) out vec4 gMaterial;
// depth auto
// Light pass (fullscreen)
vec3 pos = reconstructPosition(depth, uv);
vec3 N = decodeNormal(gNormal);
vec3 albedo = gAlbedo.rgb;
for each light: color += BRDF(N, V, L, albedo, material);
3. Live demo — 200 lights on a floor
200 point lights scattered above a bumpy surface. Emulated G-buffer + lighting in Three.js.
4. Light volumes vs fullscreen
Fullscreen quad lights every pixel regardless of light radius. Wasteful for point lights.
Better: draw a sphere mesh at each light, scaled to its attenuation radius. Only those pixels run the lighting shader. 10-50× speedup on many-small-lights scenes.
5. Tradeoffs
| Pro | Con |
|---|---|
| Scales to many lights | MSAA hard (G-buffer pre-shade) |
| Decoupled material from light logic | Transparency needs forward fallback |
| SSR/SSAO free (depth+normal) | G-buffer bandwidth cost |
| Post effects trivial | Limited BRDF variety per frame |
6. Forward+ (clustered forward)
Hybrid. Pre-pass: build a 3D light grid, bin lights into frustum cells. Main forward pass: each fragment loops only the lights in its cell. Handles transparency, still scales. S5-07 dives deep.
7. When deferred still wins
- Hundreds of small lights (torches, fires, muzzle flashes).
- Heavy screen-space post (SSAO, SSR, volumetric) that wants G-buffer anyway.
- Decal projection, light volumes as geometry.
Games: Killzone 2, Battlefield 3, Witcher 3, The Last of Us. Deferred is their backbone.
8. Three.js & WebGL2 support
WebGL 2 supports MRT (up to 8 color attachments). Deferred is fully possible in Three.js with custom WebGLMultipleRenderTargets. Stock Three.js is forward, but you can layer deferred as a custom pipeline.
WebGPU makes this trivial — fragment-per-target writes are first-class.
9. Takeaways
- Deferred = split geometry from lighting into two passes.
- G-buffer: albedo, normal, depth, material.
- Many lights → deferred wins.
- Transparency → forward fallback OR forward+.
- All AAA engines use deferred or forward+ with clustered culling.