vidar.games

Voxel cone tracing global illumination

2026-11-07 · Discuss on X

One light moving through a closed room.
Left of the line: direct light only. Right: voxel cone tracing adds the bounce, so the red and green walls tint the floor, ceiling and boxes.

The problem

Direct lighting stops at the first surface the light hits. Real light keeps bouncing, which is why a white floor next to a red wall turns pink. Baked lightmaps capture that, but only for lights that never move. Path tracing every pixel is too slow for most games.

How it works

Voxel cone tracing gives up exact rays for a blurry, cheap version of the scene:

  1. Voxelize the scene into a 64³ grid of colour, normal and occupancy.
  2. Light every voxel: direct light with a shadow march through the grid, plus the bounce read from last frame's result. Repeat it each frame and the light keeps bouncing.
  3. Downsample into 6 more levels (32³ to 1³), so coarse levels hold averaged light.
  4. Per pixel, trace a fan of cones over the hemisphere. Each step doubles the cone's width and reads a coarser level, until it is 95% blocked.

A cone that widens and reads a blurrier level costs about the same as one ray, but gathers light from a whole region.

What I hit

My first room was open at the front for the camera, so most of the bounce escaped and the effect was barely visible. I closed it with a front wall that is voxelized but never drawn.

The rotated box got striped self-shadowing until I started each pixel's shadow march 1.75 voxels off the surface.

With the source's default box filter the bounce looked blocky. The source also ships a 3×3×3 Gaussian; I used that, and moved each cone start 1 voxel off the surface instead of 0.5.

My build

three.js 0.170 on WebGL2. The voxel lighting, the downsample and the cone tracing are GLSL ports of Looooong's Unity SRP VXGI. It uses its 32-direction cone table, which keeps 11 to 13 cones per pixel. The grid is 64³, the source's Medium preset; 29,574 voxels are filled. Two voxel lighting passes run per frame.

One big change: the source voxelizes on the GPU every frame. My scene never moves, so I voxelize once on the CPU and only the light moves.

The cone-count panel compares 4, 6 and 9 cones against the source's set, showing the bounce only. With fewer cones the bounce gets patchier and the voxel blocks show more.

Trade-offs

When to use it

Rooms and small levels with moving lights, like a torch or a day-night cycle, where bake times hurt. For large open worlds the grid gets too coarse.

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

Source and credits

Technique source: Unity-SRP-VXGI by Looooong (Nguyễn Đức Long), https://github.com/Looooong/Unity-SRP-VXGI, MIT license, commit ef2e1838b13b663f68c3e5e32c6ed3f3cc56138c. Ported to GLSL: the per-voxel lighting (direct light with voxel visibility plus last frame's radiance for multiple bounces), the mipmapper (3x3x3 Gaussian option, filter factor 1.414), the radiance sampling level rule, and the per-pixel indirect diffuse cones with its 32-direction table and sin(atan(1/3)) threshold. No files were copied unchanged.

Render framework: three.js by mrdoob and contributors, https://github.com/mrdoob/three.js, MIT license, version 0.170.0 (npm). Used for the scene, 3D render targets and tone mapping. It is not the source of the voxel cone tracing technique.

My own code: the room scene and its CPU voxelizer (run once; the source voxelizes on the GPU every frame), the hidden front wall, the alternative 4 / 6 / 9 cone sets, the voxel debug view, the camera and light paths, the stage and the labels. No external models or assets. No IA model.