Bounding Volume Hierarchy and Recursive Ray Tracing

In this assignment, I implemented BVH as an acceleration structure. When constructing the BVH, instead of putting the same object to two of the leaf nodes, I preferred storing a null pointer and a boolean isLeaf flag. I was expecting that having null checks and boolean flag checks not only loiters the codebase, but they also introduce a subpar performance. However, BVH construction turns out to be quite fast. Here are the BVH construction times for each scene:

cornellbox_recursive.xml: 2.2e-05 seconds
spheres.xml: 2.6e-05 seconds
scienceTree.xml: 0.000152 seconds
chinese_dragon.xml: 0.055613 seconds
other_dragon.xml: 0.119571 seconds


To come up with BVH, bounding volumes of the objects should be defined. While doing the intersection tests, I tend to use double-precision when evaluating the bounding box intersections. Therefore, I stick to tight bounding boxes. I suspect that this approach might not be quite robust as I feel like the ray tracer have difficulties when triangles are intersected from behind, especially when their bounding boxes are thin slabs. I have consulted Tavian Barnes' discussion on ray-box intersections, with an additional check of std::max(tmin, 0.0) <= tmax instead of the suggested std::max(tmin, 0.0) < tmax, as the latter does not work with bounding boxes which are parallel to any of the axes.

While constructing the BVH, instead of the clever two-pointer approach discussed in the class, I just split the node list in the subsequent steps and sorted them with respect to a cycle of the x-y-z axes. Thus, it is quite easy to find the medians and perform the next split. I followed this pragmatic approach as the number of objects in the scene is not in the order of millions so sorting them is quite fast. As evident from the BVH construction times, the BVH-construction routine is not the bottleneck yet, and this approach yields an easy-to-maintain implementation. To come up with the test cases, I have consulted this resource, which provides some of the edge cases for different types of acceleration structures. In order to sort the bounding boxes, it is possible to define a sorting function for the std::sort as following:
bool compareBBoxX(Object* o1, Object* o2){
return (o1->BoundingBox.Center.x < o2->BoundingBox.Center.x);
}   // Comparison for x-axis. Similar functions should be required for y and z axes as well. 

Also, a look-at camera is implemented. It is a rather straightforward implementation, using simple trigonometry that is involving tangent functions. For parsing PLY files without headaches, Nick Sharp's happly library is the most robust implementation that I could consult.

While the metal objects look fine, the transparent ones had a difference in shading. While I could detect them using ImageMagick, it is hard to tell why these differences occur. With ImageMagick, I have used commands such as:

magick compare spheres.png spheres_res.png diff_spheres.png
This command comes up with an image that highlights which pixels are different in two given images. While it is useful to some extent, it does not give a measure of how much of a difference is occurred.

magick compare -metric AE -fuzz 5% scienceTree.png scienceTree_res.png diff_fuzz_scienceTree.png
This command marks the pixels that diverge more than 5%. This would help to figure out minor details, and compensates with differences in sampling during ray tracing.

magick composite spheres.png spheres_res.png -compose difference difference_spheres.png
A rather useful command, this would create an image that is composed of the differences in pixels. In the output, black pixels would suggest that there is no difference.

Below are the outputs, the difference images, and render times for each scene. The smaller images are the outputs of compare, fuzzy compare with 5% difference and composite commands respectively:

chinese_dragon.xml





Loading: 5.67028 seconds
Render: 0.367401 seconds

cornellbox_recursive.xml



Loading: 0.13256 seconds
Render: 0.452494 seconds

other_dragon.xml

Loading: 9.39446 seconds
Render: 1.30085 seconds

scienceTree.xml





Loading: 0.254028 seconds
Render: 1.18438 seconds

spheres.xml

Loading: 0.109115 seconds
Render: 0.284951 seconds

Discussion:
The differences in spheres.xml create Moire patterns, which suggest that there are visible patterns of slight offsets. When considering the 5% fuzz difference image, which shows differences at edges, suggest that the factors such as intersection tests, ray epsilons, and differences in precision could be the cause. other_dragon.xml has differences in some spots, which could also be related to a difference in self-intersections or shadow rays. Although cornellbox_recursive.xml shows a difference, the difference in shade looks as if the ambient light contributes more in my case. Although I explicitly put flags to exclude ambient and point lights in the calculations for transparent objects, that is all I could think of for the time being. The way back-intersections are handled could be one of the causes of the difference in the scienceTree.xml scene. The sharper shadowy edges in chinese_dragon.xml support these ideas as well.
With the help of BVH, the time spent rendering was cut down significantly. Right now reading the scene files and BVH creation are single threaded, as evident from the performance output in the profiler when rendering all the scenes in sequence:

Here are the bloopers from this iteration:

Darker reflections in the scienceTree.xml scene.

A vortex is visible inside the reflection of the conductor surface. I found out that I mistakenly made a transparent material pass on the conductor.

Spheres with darker reflections.

Mistakenly adding an extra contribution from the mirror reflectance results in white pixels.

The reflection amount is not correctly implemented in this instance of other_dragon.xml scene.






Comments