Recently I got access to a GPU that supports accelerated raytracing and the temptation to tinker with DXR is too strong. This means that I will steer away from compute shader raytracing for the foreseeable future. It is a good opportunity though to do a quick retrospective of the past few years of experimenting with “software” raytracing.
It all started about 4 years ago, when DXR was released and the first GPUs that supported it came out. My lowly Intel HD4000 laptop didn’t support DXR of course but not wanting to miss out on all the fun I decided to implement my own raytracing solution based on compute shaders. I was quite ambitious at the time, choosing to implement both raytraced shadows and reflections, which was a huge learning step having to manage not just BVH creation/traversal and collisions but material sampling, hit point lighting and closest hits as well. I also got a first feel of the impact of ray divergence.
The first attempt at raytracing was decent but quite naive. For example I stopped the BVH build at model level, which meant that I had to iterate over each meshes triangles, in a loop, to determine ray-triangle collisions. That sort of worked for the simple spheres and cubes I was using but would never scale. In my second attempt I improved BVH generation to include all triangles which improved the time to find ray collisions a lot. I also started exploring better BVH generation techniques like Surface Area Heuristic which accelerated traversal massively (left is number of steps to find a collision without SAH, right with SAH).
BVH traversal is memory intensive operation as well which highlighted how important it is to choose the best buffer type for the target platform (for eg Intel GPUs seem to prefer ByteAddressBuffers).
Another technique that was gradually gaining traction was mixing screen space with raytracing techniques and for my next experiment I focused on hybrid raytraced reflections. The idea behind this is to raymarch screen space reflections as normal but for those rays that fail to find a collision in screenspace let raytracing pick them up and find the geometry collisions.
This experiment also gave me the opportunity to directly compare SSR and raytraced reflections and highlight their differences, especially in terms of specular lighting of the hitpoints (left SSR, right raytraced)
At some point I came across Intel’s Embree library and realised that I can use it to generate the BVH trees that I use for raytracing in my toy engine. I made some comparisons in terms of memory and traversal costs, there where mixed results depending on the scene (“reference” was my BVH generation code).
Embree offers options to balance generation time and BVH traversal time which is very useful in cases you generate BVHs in the runtime. It was an overall better option and I am using this to generate my BVH trees since.
Up to that point, I was using a single BVH to bake the whole scene in. This in general produces a higher quality tree but it can be wasteful when trying to trace local rays (imagine local light shadows), or if we need to stream in and update the BVH with new models. For this reason I added a two level BVH hierarchy, with one BVH storing a single model (BLAS in DXR’s lingo) and another BVH for all the model instances’ BVHs (aka TLAS). Embree’s fast BVH tree generation was ideal for something that needed per frame generation like the TLAS. This opened to the door to raytraced shadows for animated models for example.
I next revisited hybrid raytracing, mixing raytracing with traditional techniques, this time to explore options to combine shadowmap and raytraced shadows. The idea here is that the requirement of high quality shadows (where raytracing shines) is along the boundary of shadowed and lit areas, so using shadowmaps for the bulk of the shadowed pixels and raytracing for the edges should help reduce the cost (left hybrid shadows, right fully raytraced)
Over the years I also performed a number of smaller scale experiments like exploiting the ray coherence between neighbouring pixels and caching hit triangle index to test before kicking off a full BVH traversal (a big advantage of having full control over the BVH traversal).
I also explored the impact of ray divergence on wave size, using shadowmaps to occlude lighting for hit point lighting and second bounce lighting
Initially I did some comparisons between my implementation and Mitsuba to validate GI intensity (left in engine, right Mitsuba).
I also started dabbling with denoising, using blue noise and temporal refinement to improve resolution of the GI and blurring to suppress the spatial noise.
Having support for raytraced GI, I made a few investigations on indirect lighting directionality, comparing raytraced GI to screen space AO + irradiance cubemaps (left raytraced, right SSAO).
Some more pretty images of raytraced cubemaps here.
Continuing on the theme of GI, I later added a path tracer to the toy engine to further validate the raytraced GI output. I did help me fix some issues with the noise I use to generate the rays.
Later I revisited denoising to implement Metro: Exodus RTGI denoising (this game has been a great source of inspiration to me), which improved noise in GI significantly.
More recently I have started experimenting with software VRS to reduce RTGI cost, which shows promise, but this is a story for another day.
So what is next? More of the same of course but with a heavier focus on DXR. There are some many things to explore with raytracing, the journey is just beginning!