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 about tracing rays on the GPU, 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 also kept it basic by retracing everything every 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 because my voxel world generation was slow. I also 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