CUDA-ACCELERATED MONTE CARLO PATH TRACER
High-speed rasterization pipelines like OpenGL might be able to put a lot of images on the screen in short time, but when it comes to true physical realism, path tracing is king. Used by modern movie studios, path tracing models rays of light by tracing them in reverse from the camera eye to light sources, bouncing around the scene in the process. This allows for the capture of indirect light, as shown below.
The problem with path tracing is that, at its best, it's extrmely slow. This project will aim to accelerate a path tracing system by parallelizing over light rays on the GPU. At a high level, the program will look like this:
PATH GENERATION AND COLLISION
While writing pseudocode like "path.collide(scene)" is easy, this is the most performance-intensive part of my path tracer. The basic approach --
-- is ghastly. With a 900x900 scene, 2,000 primitives, and 12 bounces I could have as many as 19.4 million collision tests to do in one iteration in the worst case. Fortunately, we've got a few optimizations to speed things up.
First, that worst case should never have to happen when we can stop tracing paths that hit lights or fly into space. But we dont want to launch kernels that check if (path is dead) return;, because if a warp of 31 dead paths and 1 active path is launched, it will take just as long as 32 active paths... the last bounce will be as costly as the first!
Instead, I specifically separated active paths from inactive ones using stream compaction, provided by the thrust library, and was thus able to only launch as many threads as was necessary to cover the active ones.
One easy way to save a round of intersections tests is caching the first bounce. Since we're casting them through pixels, they'll always hit the same location. I accurately simulated the first bounce, saved it to device memory, then was able to start on the second bounce every subsequent frame.
When I decided I wanted anti aliasing, this became an issue. A jittered antialiasing solution would change my first bounce, rendering my cache incorrect. To get the best of both worlds, I jittered and re-filled my cache every x frames. This means that after the first bounce my caching is 1/x% less effective, but effective nonetheless.
While 5 or 6 boxes or spheres can be practically nigligible in terms of compute time, even my <1000 triangle elephant blew up iteration time. As the first step to cutting down this nested path-geometry relationship, i implemented a bounding box for my mesh imports. If a ray didn't hit the bounding box (or already had a closer hit), it ignored the rest of the mesh as well.
SHADING AND REDIRECTION
Once we have a collision, we can get to the best part. At the shading stage, we try our best to represent physical meterials and their light reflection, absorbtion, transmission, and/or emission tendencies. In my project, I included the following three material properties, which could be blended together by assigning the likelihood that a given collision would chose any lighting model. Pictured as well are the BRDFs used to represent the new direction of an incident ray:
Because different materials have different amounts of work involved in processing (and also different likelihoods of deactivating a ray on this iteration or the next), we can prevent even more divergence if we sort rays by the material they hit.
The level of randomness in choosing in a direction necessarily produces noise in the output image. While it's impossible to sample the continuous hemisphere around a normal, we can get a good picture given enough random samples:
This is the 'Monte Carlo' in Monte Carlo Path Tracing. Various techniques, like bidirectional, direct, and importance sampling can help accelerate convergence to the final image. These methods have unfortunately not yet been implemented in my project.
The following stats were captured with the test image at the top of this page. Notably, his image had a wall behind the camera preventing rays from being deactivated due to no collision.
With any mesh in the scene, the far-and-away most important improvement is bounding boxes. For paths who do not intersect the mesh, this reduced intersection tests by 95%, especially influential when more than 90% of my GPU time was spent in generating collisions.
Surprisingly, the materials sorting was influential even though my materials were rather simple. My guess is that the important separation was between those rays that hit the mesh (gold material) and those that didn't, since those that did would be guarunteed to hit the mesh's bounding box on the next collision generation.