vidar.games

Live light-map bake

2026-11-03 · Discuss on X

The soft light in this room is baked into a texture, and it gets baked again every frame while a wall moves.
Each re-bake took 0.7 to 1.1 s on a software GPU, no graphics card.

The problem

Sunlight through a window lights one patch of floor. Everything else in a real room is lit by that patch bouncing around. Without that bounce, the left half is what you get: a bright square and black walls. Computing bounce light every frame is expensive, so most games bake it once, offline, into a light map. The catch: move a wall and the baked light is wrong until someone re-bakes it.

How it works

  1. Unwrap: every surface gets its own patch in one shared texture. Here that is 78 patches in a 512 × 512 atlas, about 20 texels per metre.
  2. Probes: a grid of 8 × 4 × 6 points across the room each renders a tiny 16 px cube view of the scene and stores the light arriving from every direction.
  3. Bake: each texel of the atlas looks up the probes at its own position and normal, 16 jittered samples per texel, and stores that irradiance.

At runtime the sun is still lit live with shadows, and the light map adds only the indirect part on top. The two never overlap, so nothing is counted twice.

This is not the 2D radiance cascades approach, which recomputes light in screen space every frame. A light map is 3D, stored on the surfaces themselves, costs a texture read per pixel once baked, and is only as fresh as its last bake.

What I hit

I wanted a small preview of the light map, so I told the texture to use the first UV set. It was the same texture object the walls use, so the whole room read it with the wrong UVs and turned into a patchwork of random rectangles.

Before each re-bake the light map has to be switched off, or the probes would see last bake's light and feed it back in.

Source choice: on a test box, a voxel GI source needed about 11 s for 16 samples on this software GPU, while the probe grid needed about 1.3 s. I used probes.

My build

three.js (dev branch, r187), WebGPU renderer: unwrapUVs, LightProbeGrid and LightMapper used unchanged, set up like the repo's light-map baking example. The room, the moving red partition and the timeline are mine. The partition moved over 66 frames and every one triggered a full re-bake: 0.69 to 1.08 s each, median 0.84 s, in headless Chromium on SwiftShader. The first bake took about 2 s with shader compiling.

Trade-offs

When to use it

Level editors, building games and rooms that change now and then: re-bake after an edit, not every frame.

New breakdowns go out on X first: @vidar_0208. If this one helped, you can buy me a coffee.

Source and credits

Repo: github.com/mrdoob/three.js (MIT license), branch dev, commit fa01dd6

Code: unwrapUVs (examples/jsm/utils), LightProbeGrid and LightMapper (examples/jsm/lighting) are used unchanged, set up like examples/webgpu_lightmap_baking.html. The room, the moving partition, the re-bake loop, the timer and the insets are my own code.

LightMapper author: @BenHouston3D

Topic credit: @oddjolt's post on live light-map baking, https://x.com/oddjolt/status/2116616806995833109 (demo: oddjolt.itch.io/bounce). None of their code or art is used.