Tristan Marrec

Computer Graphics Programmer
Home
LinkedIn
Email contact@tmarrec.dev Updated

Real-Time Voxel Path Tracing -

At university, I studied ray tracing and loved it because of its simplicity. We had a small project to make a CPU ray tracer in C++, and although it worked well, it was very slow. A few years later, I received my first RTX card and immediately started building a Vulkan renderer taking advantage of these RT cores. Around the same time, I was interested in voxel rendering, which was a hyped topic in the computer graphics community.

Code available as ZIP download.

Final result

I used this amazing resource from NVIDIA to get started. As I learned more how to GPU trace rays, I started working on procedural voxel world generation. The goal was to render voxels with real-time path tracing, not to make a game, so the voxel code remained basic and was not optimized.

For path tracing, I’ve also kept it basic by retracing everything at each frame without caching the sampled values. This means there’s of course some noise in the videos, which is okay for a prototype, but it’s very visible in the screenshots.

Beautifully simple

This is the main part of the ray generation shader, it’s almost all that’s needed to render the scene - look how straightforward and elegant it is!

uint totalBounce = 0;
for(int sampleIdx = 0; sampleIdx < NUM_SAMPLES; ++sampleIdx)
{
    vec3 rayOrigin = cameraOrigin;
    vec2 randomPixelCenter = vec2(pixel) + vec2(0.5) + 0.3765 * RandomGaussian(pld.rngState);
    vec2 screenUV = vec2((2.0 * randomPixelCenter.x - resolution.x) / resolution.y,
                        -(2.0 * randomPixelCenter.y - resolution.y) / resolution.y);
    vec3 rayDirection = normalize((vec4(VERTICAL_FOV_RAD * screenUV.x, VERTICAL_FOV_RAD * screenUV.y, -1.0, 1) * pushConstants.cameraView).xyz);

    vec3 accumulatedRayColor = vec3(1.0);
    
    for (int tracedSegments = 0; tracedSegments < TRACED_SEGMENTS; ++tracedSegments)
    {
        traceRayEXT(TLAS, gl_RayFlagsOpaqueEXT, 0xFF, 0, 0, 0, rayOrigin, T_MIN, rayDirection, T_MAX, 0);

        if (tracedSegments == 0)
        {
            depth = pld.depth;
            firstNormal = pld.rayHitSky ? vec3(0) : pld.rayDirection;
            P = pld.rayDirection * pld.depth;
        }

        accumulatedRayColor *= pld.color;

        if (pld.rayHitSky)
        {
            summedPixelColor += accumulatedRayColor;
            break;
        }
        else
        {
            rayOrigin = pld.rayOrigin;
            rayDirection = pld.rayDirection;
            totalBounce++;
        }
    }
}

imageStore(diffuseImage, pixel, vec4(summedPixelColor / float(NUM_SAMPLES), 0.0));
imageStore(normalImage, pixel, vec4(firstNormal, 0.5));
imageStore(depthImage, pixel, vec4(depth, pld.meshID, 0, 0));

Shader reloading

To speed up my work, I implemented live reloading of shaders, which proved very useful as my voxel world generation is slow. Also, I’ve made sure that a hash of the shader code is stored in the shader file, so that only modified shaders are recompiled.

Debugging

NSight Graphics helped me a lot with this project, thanks to its powerful RT debugging capabilities.

Some final screenshots

Some progress videos

References

  1. vk_mini_path_tracer - Neil Bickford. nvpro-samples.github.io/vk_mini_path_tracer

  2. Best Practices: Using NVIDIA RTX Ray Tracing (Updated) - Juha Sjoholm. developer.nvidia.com/blog/best-practices-using-nvidia-rtx-ray-tracing

  3. NVIDIA Nsight Graphics - NVIDIA. developer.nvidia.com/nsight-graphics