[ 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 ]
- vk_mini_path_tracer - Neil Bickford. nvpro-samples.github.io/vk_mini_path_tracer
- Best Practices: Using NVIDIA RTX Ray Tracing (Updated) - Juha Sjoholm. developer.nvidia.com/blog/best-practices-using-nvidia-rtx-ray-tracing
- NVIDIA Nsight Graphics - NVIDIA. developer.nvidia.com/nsight-graphics