The Monte Carlo pathtracer is a recent project of mine that I worked on for my Advanced Rendering class. Since I already wrote some detailed posts on how it functions on my 3D specialised Instagram page, I shall leave a link to that page for further details including progress shots, but nevertheless, I will still summarise how I developed the pathtracer from a simple Naive integrator to the full blown Monte Carlo pathtracer using development upon the Light Transport Equation.
Github code (including comments from commits).
Link to 3D Instagram account where I post updates to my pathtracer and other projects.
To the left are some renders that I created with the pathtracer by constructing custom JSON files and then rendering them with the pathtracer. Attached below are more catalogues of my personal favourites, some from the assignment that we had to turn in, and others of my own creation. In the next section, I still wanted to discuss the theory on implementation and how I tackled it, because I find the subject matter quite fascinating. In the future, I want to see if I can optimise the code to be able to take in metallic textures or perhaps more complicated polygon meshes, since the meshes and textures used for these renders are quite basic, in the sense that only normal maps and bump maps are used to render most of these images.
The image that you see on the right is one of the very first renders that I got from my pathtracer using the Naive Integration method. To summarise briefly, in a scene with a few objects and a light source, one would cast an infinite amount of rays from the camera into the scene, and bounce it in a random direction if a hit is registered on an object. We would then keep the ray bouncing until the bounce hits the light source (which has a colour value >= 1.f), and return the accumulated colour from the other bounces. However, a major issue with this method is that the casting of multiple rays and bouncing them until it hits the light source is way too inefficient, and during rendering, it would take a lot of runtime to generate. This is why the next method, the direct lighting method, was introduced.
Here are also some photos from rendering reflections and refractions with the Naive Integrator, along with some interesting mistakes that I made. I go into more detail regarding how these mistakes occurred on my Instagram posts, so do keep a lookout.
Unlike the Naive Integrator, the Direct Light pathtracer did not use a loop to continuously bounce a ray until it hit the light; rather, while we still generate a ray from the camera (which I will call out ray since this is the ray that we will be seeing), once said ray hits an object's surface, we cast a ray directly to a point on the light source and return the colour value of that singular point on the surface. This is also why in this case, because we are no longer casting rays in multiple bounces to the surrounding area which makes us not pick up the colour of other influences, we only see one colour on the objects in the scene. Comparatively though, the runtime of the Direct Lighting is the fastest since it requires no iteration to output a colour.
Multiple Importance Sampling, or MIS for short, is the culmination of both of the above methods, since by themselves, the output generated is not ideal. Therefore, the brief idea of MIS is to generate a weighed average of both results generated from Direct Light and Naive Integration. Below, we can see an exhibit of why using one method over the other is not ideal: in the Direct Light scene, the way the code is set up handles cases with small light sources well, but once a larger light source is sampled, we notice artifacting happening in the larger light, since Direct Light would take more time to generate the results in the larger light. However, the problem for Naive Integration is the exact opposite, where generating smaller light samples leads to much more artifacting because of the lower probability that the light bounces will hit the light. Therefore, in MIS, we use a power heuristic to take a weighed average of both Naive and Direct methods, and return the sum of the weighed averages.
With Naive Integration only
With Direct Light only
The final step for my Monte Carlo pathtracer was to combine global illumination, but also reintroduce the ray bounce concept all the way from the Naive Integrator. With the global illumination calculation, I was able to add environment maps for my renders, and by putting MIS into a for loop which terminates upon hitting the light meant that the render would be very precise, which is a far cry from the somewhat pixelated renders from the Naive Integrator.
Some additional bugs that I encountered in my implementation of Full Integrator, as well as the spot light and point light Full Integrator renders.