# ==Voxel rendering==, a survey
<p class="doc-sub">// status: budding</p>
"Voxels" covers a lot of ground. A Minecraft world and an MRI volume are both voxel data but ask for very different rendering strategies. This note maps the territory so the other voxel notes ([[Marching Cubes]], [[Dual Contouring]], [[Sparse Voxel Octrees]]) have a home.
# The basic split
Two big questions decide almost everything:
1. **Are my voxels "blocky" or are they samples of a continuous field?** Minecraft cubes are an enumeration of discrete types; a CT scan's Hounsfield units are samples of something continuous.
2. **Do I rasterize or march rays?** Meshes go through the usual GPU pipeline; octrees and density fields are often sampled by rays.
# Blocky / cubic voxels
The Minecraft family. Each cell has a discrete material or is empty. Typical pipeline:
- **Greedy meshing** — merge coplanar faces of the same material into bigger quads. Dramatically fewer triangles than naively emitting 6 faces per voxel. Mikola Lysenko's write-up is the canonical reference.
- **Face culling** — don't emit faces between two opaque neighbours.
- **Chunking** — group cells (e.g. 16×16×16 or 32×32×32), stream only what's near the camera, remesh dirty chunks async.
- **Lighting** — flood-fill sky/block light per chunk. Smooth lighting = vertex AO from neighbours.
It rasterizes like any other mesh after that. The whole problem is mesh maintenance, not drawing.
## Greedy meshing in more detail
For each axis and each slice along it, reduce the 2D slice to a mask of exposed faces (material ID, or 0 for "no face"). Merge runs into rectangles:
```c
for each slice k along axis X:
build mask[y][z] = exposedFaceMaterial(cell(k, y, z))
while mask has non-zero entries:
find first non-zero (y, z)
extend width w along y while mask[y..y+w][z] all equal
extend height h along z while mask[y..y+w][z..z+h] all equal
emit quad at (k, y, z) with size (w, h, material)
clear the rectangle from the mask
```
Do both `+X` and `-X` faces (different masks). Repeat for Y and Z. A typical 16³ Minecraft chunk drops from ~6000 quads to a few hundred, identical appearance. Mikola Lysenko's [_Meshing in a Minecraft Game_](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) is the canonical reference.
## Ambient occlusion & smooth lighting (blocky)
Per-vertex AO is the Minecraft trick: for each vertex of a face, look at the three neighbour voxels on that corner — if two are solid, the vertex is dark; one, medium; zero, light. The rasterizer interpolates across the quad and you get surprisingly convincing crease shading for free.
Block/sky light uses the same scheme: a chunk-local BFS propagates light values cell-by-cell (Minecraft uses 4-bit sky and 4-bit block channels), stored per-voxel and interpolated at vertices. Re-run the BFS on edits. Modern voxel engines often push this to a compute shader.
## Texture atlases & arrays
Hundreds of materials, one draw call. Two flavours:
- **Atlas** — every material packed into one big image; per-face UV offsets. Beware of bleeding at mip levels — add padding or use a half-texel inset.
- **Array** — `GL_TEXTURE_2D_ARRAY` / Vulkan array image; sample with an extra integer layer index. No bleeding, native mip behaviour. Modern default.
Bindless textures (OpenGL's `GL_ARB_bindless_texture`, Vulkan descriptor indexing) sidestep both — store a texture handle per voxel material and sample directly. See [[OpenGL - learning log]] / [[Vulkan - learning log]].
# Iso-surface extraction
When voxels store a density and the surface is an iso-contour, you extract a mesh:
- [[Marching Cubes]] — the 1987 classic. 256 cases from 8 corner signs, look up the triangulation, interpolate edges. Smooth, simple, has ambiguity artefacts.
- [[Dual Contouring]] — places a vertex per cell by minimising a quadratic error. Preserves sharp features like edges and corners, handles adaptive (octree) resolution.
- **Transvoxel** — Lengyel's extension to stitch chunks at different LODs without cracks. Essential once you have more than one resolution.
- **Surface Nets / Naive Surface Nets** — simpler dual of marching cubes; nice middle ground when you don't need sharp features.
Once you have the mesh, it's a normal rasterization problem again.
# Ray-cast voxels
Skip the mesh; have rays query the voxel grid directly. The grid's structure decides performance.
- **Dense grid + DDA** — Amanatides & Woo's line traversal. Simple, cache-friendly for small worlds.
- [[Sparse Voxel Octrees]] — compress empty space, traverse with stack-based or stackless algorithms (Laine & Karras ESVO is the canonical modern reference).
- **Brickmaps** — octree of dense bricks. Pixar-style volume representation. Good sweet spot between granularity and memory overhead.
- **DDA on hashed grids** — OpenVDB and NanoVDB. The production volumetric standard in VFX.
# Volumetric / participating media
Not a surface — integrate along the ray:
```glsl
vec4 raymarchVolume(vec3 ro, vec3 rd) {
vec4 accum = vec4(0);
float t = 0.0, dt = 0.05;
for (int i = 0; i < 128 && accum.a < 0.99; i++) {
vec3 p = ro + rd * t;
vec4 sample_ = transfer(densityField(p)); // colour + opacity
sample_.rgb *= sample_.a; // premultiply
accum += (1.0 - accum.a) * sample_; // front-to-back
t += dt;
}
return accum;
}
```
Tricks that matter:
- **Empty-space skipping** via a low-res occupancy grid — often 2–10× speedup.
- **Early termination** at `α > 0.99`.
- **Henyey-Greenstein phase function** for anisotropic in-scattering (forward scattering in clouds).
- **Ratio / delta tracking** for unbiased volumetric shadows with a single sample per pixel.
For clouds specifically: low-frequency base shape (Worley + Perlin), high-frequency detail noise, a coverage map for placement. Decima's and _Horizon Zero Dawn_'s cloud papers are the usual starting points.
## Production-grade volumetric formats
- **OpenVDB / NanoVDB** — the industry hierarchical grid, everywhere in VFX. NanoVDB is the read-only GPU form.
- **Brickmap** — octree where leaves are dense `8³` or `16³` bricks. Balances sparse topology with cache-friendly traversal. ==GigaVoxels== is built on this.
- **Clipmaps** — infinite virtual grid centred on the camera. Pairs naturally with voxel cone tracing for GI.
# Voxel cone tracing (GI)
Cyril Crassin's trick for global illumination:
1. Voxelize the scene into a hierarchical 3D texture (usually a sparse octree or clipmap).
2. At each shading pixel, cone-trace a handful of rays through the hierarchy, sampling a wider mip level the further out you go.
3. Compose diffuse + specular indirect from those cones.
Approximate but real-time-capable. NVIDIA's VXGI demos were the poster child. Modern engines have largely moved to DDGI / hardware ray tracing for GI, but cone tracing is still a great fit for voxel-native games.
# Picking a technique
| Need | Start here |
|------|------------|
| Minecraft-style destructible blocks | Greedy meshing + chunks |
| Smooth sculpted terrain (finite view distance) | [[Marching Cubes]] |
| Terrain with hard edges / LOD | [[Dual Contouring]] + Transvoxel |
| Huge sparse static worlds rendered by ray | [[Sparse Voxel Octrees]] |
| Medical / scientific volume | Ray-cast volumetric |
| Real-time GI in a voxel game | Voxel cone tracing |
# Gotchas that bite everyone
- **Chunk seams** — normals averaged only within a chunk leave visible edges. Always sample across boundaries.
- **LOD transitions** — the hardest problem in voxel rendering. Transvoxel, skirts, stitching meshes… pick your poison early.
- **Memory layout matters more than the algorithm** — a naïve `voxels[x][y][z]` of `u32` is usually the bottleneck. Morton order and palette compression pay for themselves fast.
- **Float precision drifts at large coordinates** — rebase the camera, or store voxel positions as ints.
---
Back to [[Index|Notes]] · see also [[Raym - Interactive Terrain Generation with Marching Cubes]]