First Blood


The core version of the ray tracer includes features such as:

- Parsing routines for the scene definitions.
- A simple OOP-like class structure.
- Primitives for shapes and materials.
- Image plane, camera definition, camera ray generators.
- Effects of diffuse, specular surfaces, as well as the contribution from ambient light.
- Timing functionality to determine how much time does a render take.
- As a bonus, to speed up the debugging processes, multithreaded implementation using OpenMP.

Before rendering the rabbit scene, though, the XML files that are containing scene definitions should be parsed. The implementation utilizes tinxyml2, a lightweight tool that is available for C++, for XML parsing. The PNG output for the rendered images is encoded by the LodePNG library. Although it is quite robust, one should be careful with the alpha channel during encoding. Although the documentation shows examples in which unsigned char type is used, I manage to correctly set the alpha when I use an int value of 255.

The XML files were pretty straightforward, and their nested structure provided hints for a clean design. I also consulted the books such as Kevin Suffern's Ray Tracing From Ground Up, PBRT, Shirley&Morley's Realistic Ray Tracing, Shirley&Marchner's Fundamentals of Computer Graphics, and online resources such as scratchapixel.com before finalizing my design. 

The design has a Scene, which includes pretty much everything in it: CameraLightsMaterialsObjects, etc. The Scene is also responsible for parsing these entities: Although this would give a heart-attack to an OOP guru, for the time being, it will only deal with the same type of XML structure, so a time tradeoff is chosen over a more elaborate design. The Camera has an ImagePlane under it, which is mainly purposed to create the image plane given the camera definition. Then, the Camera class creates the primary rays. The Scene intersects it with the objects, and according to their material properties, it shades the corresponding pixel.

Although the intersection tests seem pretty straightforward, there are subtle details that may hurt if not taken care of. The most important thing that one should take into account is that if there is no negativity check for d in the ray equation of r(t) = o + td, then an object with a negative valued intersection test will always steal the intersection test and render on top of everything. This subtle detail was not possible to observe in this simple Scene as none of the objects are overlapping on the final image. Yet, this simple case provides a great way of testing the basic intersection routines. It also helps to understand that a Mesh object can act as a container for triangles, and a pure virtual intersection function definition in a Shape object such as "virtual bool Intersect(Ray &ray, double &t, RayHitInfo &rayHitInfo) = 0;" will save a lot of time and effort as the shapes would have their intersection methods and with a Shape pointer, it is possible to call the related routine.

Also, the triangles in a scene can have various formats that should be taken care of. I took the approach of calculating the normals once for each triangle. And this required attention while parsing "Faces", "Triangle", and "Mesh" objects. Unfortunately, I forgot to calculate the normals when I parse a "Triangle" and ended up with such images:


In the leftmost image, both triangles that make the plane under the spheres are completely ignored. The image in the middle demonstrates a pathological case in which shadow rays have irrelevant intersections, which is evident by the shading to the below of the image. The rightmost image is another pathological case. The puzzling part was that these anomalies seem to appear at random. To debug, I took renders with smaller resolutions (64x64) and traced the pixels which are not rendered correctly. Then I figured out that although the intersection test seems to work fine, the normals are +INF and the color value is NaN. Which points to a case where the variable Normal for the Triangle objects are not initialized at all! The challenging part was that the ray tracer works gracefully with such errors, which makes debugging quite hard.

For the Scene, "spheres.xml", there is a key observation: In the intersection tests, even though double-precision floating-point types are slower by nature, switching the parts of the code where a lot of multiplication and division is involved from floats to doubles helped the accuracy of intersection tests without passing around twice the data. With only floats in the intersection tests, there were some artifacts around some parts of the edges of a sphere.

Lastly, since the core ray tracing algorithm is embarrassingly parallel, loops were parallelized using OpenMP. I also used the inlining of intersection tests and math utility functions aggressively. However, it is up to the compiler to consider the inline command. I have compared the results with various settings and the render times are as follows:


simple.xml


No multithreading & No inlining
Loading: 0.122952 seconds
Render: 0.218512 seconds
No multithreading & Inlining on
Loading: 0.105989 seconds
Render: 0.178501 seconds
Multithreading on & No inlining
Loading: 0.111129 seconds
Render: 0.173318 seconds
Multithreading on & Inlining on
Loading: 0.100017 seconds
Render: 0.166203 seconds

cornellbox.xml

No multithreading & No inlining
Loading: 0.147888 seconds
Render: 0.69035 seconds
No multithreading & Inlining on
Loading: 0.102053 seconds
Render: 0.511429 seconds
Multithreading on & No inlining
Loading: 0.091088 seconds
Render: 0.345828 seconds
Multithreading on & Inlining on
Loading: 0.102567 seconds
Render: 0.351135 seconds



bunny.xml


No multithreading & No inlining
Loading: 0.092134 seconds
Render: 31.5284 seconds
No multithreading & Inlining on
Loading: 0.059263 seconds
Render: 30.1762 seconds
Multithreading on & No inlining
Loading: 0.060347 seconds
Render: 9.8962 seconds
Multithreading on & Inlining on
Loading: 0.063254 seconds
Render: 5.51554 seconds

spheres.xml

No multithreading & No inlining
Loading: 0.070692 seconds
Render: 0.284334 seconds
No multithreading & Inlining on
Loading: 0.071739 seconds
Render: 0.286212 seconds
Multithreading on & No inlining
Loading: 0.14298 seconds
Render: 0.291104 seconds
Multithreading on & Inlining on
Loading: 0.088131 seconds
Render: 0.232958 seconds



scienceTree.xml


No multithreading & No inlining
Loading: 0.157603 seconds
Render: 61.3567 seconds
No multithreading & Inlining on
Loading: 0.180872 seconds
Render: 61.6729 seconds
Multithreading on & No inlining
Loading: 0.220315 seconds
Render: 21.1752 seconds
Multithreading on & Inlining on
Loading: 0.212274 seconds
Render: 19.4526 seconds


Although the inlining does not yield a significant improvement in runtimes when multithreading is not used, the performance improvement is more dramatic when it is combined with multithreading. The case is evident, especially in the scenes with many objects.












Comments

  1. Excellent work. I especially liked the timing comparisons between different approaches. Keep up the good work!

    ReplyDelete

Post a Comment