Voxel Raytracer Devlog

3D voxel storage texture

2026-07-10

Commit: 67bf4a0

A 3D texture is used for storing the voxel grid.

1
2
-ByteAddressBuffer      voxels : register(t0, space2);
+Texture3D<uint>        voxels : register(t0, space2);

Reading voxels from the texture is more straightforward than the byte buffer:

1
2
3
4
5
6
7
8
9
-uint voxel_at(int3 const p)
-{
-    int const  i = p.x + p.y * uniforms.grid_ext + p.z * uniforms.grid_ext * uniforms.grid_ext;
-    uint const byte_idx = (uint)i;
-    uint const word = voxels.Load(byte_idx & ~3u);
-    uint const byte_shift = 8u * (byte_idx & 3u);
-    return (word >> byte_shift) & 255u;
-}
+uint voxel_at(int3 const p) { return voxels.Load(int4(p, 0)).r; }

The voxels are laid out in memory as v = x + y * grid_extent + z * grid_extent². This makes it difficult to utilize the cache, as the ray tracing may have to access neighboring voxels grid_extent² apart from each other in memory. Using a 3D texture should help as it is designed for spatially adjacent data access.

Metric Before After Delta
FPS 37.9 41.8 10.3%
Avg on-GPU 41.94 ms 35.99 ms -14.2%

Using a 3D texture results in less time spent on the GPU, although the improvement is not huge at this stage. A huge number of voxels are still being traversed by the algorithm.

Pack voxels into byte buffer

2026-07-08

Commit: 25b6fd2

Voxels are stored as bytes instead of wasteful uints:

1
2
-StructuredBuffer<uint> voxels : register(t0, space2);
+ByteAddressBuffer      voxels : register(t0, space2);

Voxels are palette indices, with an index of 0 indicating empty space (hence, in Magicavoxel files, only palette indices > 0 are valid). The palette has 255 entries, so using a byte per voxel is sufficient.

Although peak memory consumption is reduced, there is no impact on runtime performance on my M2 Macbook:

Metric Before After Delta
FPS 39.97 40.17 +0.20 / +0.5%
On-GPU avg 37.07 ms 39.22 ms +2.15 ms / 5.8% slower
Peak app memory 115.80 MiB 104.09 MiB -11.70 MiB / -10.1%
Metal allocated size 61.50 MiB 61.50 MiB 0.00 MiB

Naive DDA

2026-07-04

Commit: 0014bd0

Render Magicavoxel models with naive DDA.

  • Adds ogt_vox.h for parsing Magicavoxel files.
  • Introduces DDA rendering in the pixel shader.

Here’s the gist of the DDA algorithm. For a ray with origin o and direction d, the point at t along the ray is given by

p = o + t * d

The first x-intersection is given by

t = (ceil(o.x) - o.x) / d.x

and similarly for the first y-intersection:

t = (ceil(o.y) - o.y) / d.y

Subsequent x and y-axis intersections are 1 unit apart: t = (ceil(o.x) - o.x) / d.x + (1 / d.x). The DDA algorithm keeps track of which intersection occurred first, and steps along the axis in that direction. The algorithm loops through the grid cells until a stopping condition is met, such as exiting the grid.

Here is a gist of the DDA algorithm, implemented in the pixel shader:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    float3 const entry = origin + tmin * dir;
    // ...
    int3 const   start_cell = clamp(int3(entry), min_cell, max_cell);
    // ...
    float3 const next = float3(start_cell) + max(float3(step_dir), float3(0.0, 0.0, 0.0));
    int3         cell = start_cell;
    float3       tnext = (next - entry) * inv_dir;
    float3 const tdelta = inv_abs_dir;
    // ...
    for (int i = 0; i < 3 * uniforms.grid_ext; ++i)
    {
        if (any(cell < min_cell) || any(cell > max_cell))
        {
            return 0u;
        }

        uint const v = voxel_at(cell);
        if (v > 0u)
        {
            return v;
        }

        // Branchless trick: https://www.shadertoy.com/view/4dX3zl
        float3 const axis_mask = step(tnext, min(tnext.yzx, tnext.zxy));
        tnext += axis_mask * tdelta;
        cell += int3(axis_mask) * step_dir;
    }
    // ...

monu2.vox from ephtracy/voxel-model:

naive-dda

Hello, SDL!

2026-06-28

Commit: 03feb13

All projects have to start somewhere. The rendering infrastructure is set up to render a fullscreen quad.

  • SDL_GPU is used as the rendering abstraction. The Metal backend is used on macOS, and the Vulkan backend on Windows.
  • SDL_shadercross is used to compile/transpile HLSL shaders to SPIR-V and Metal.
  • Shaders are compiled at build time and embedded as byte arrays into the source code.

A window opens displaying the UV coordinates as the color.

fullscreen-quad

Contents