A JavaScript Ray Tracer
This is my final project for the course CS 488: Introdution to Computer Graphics at the University of Waterloo, taken in Winter 2015. Click any image to view at full resolution.
Online Demo
You can render a few preselected scenes in your browser. Google Chrome is highly recommended due to its relatively good performance. Run the ray tracer in your browser »
Main Objectives
Note: Supersampling was done as an extra objective in the Assignment 4 ray tracer, and has been reimplemented.
Objective 1: Mirror Reflection
Mirror reflection was achieved fairly simply for all objects. Mirrors don't need to perfectly reflect all light; the middle picture above demonstrates a simulation of typical real-world mirrors which are better at reflecting green light.
Objective 2: Refraction and Transparency
Two spheres. The one in front has a refraction index of 1.33, and the one behind has a refraction index of 1.2.
A few partially transparent boxes with no refraction.
Refraction was implemented using the application of Snell's law. Transparency was added by adding an optional per-color alpha channel on objects.
Objective 3: Texture Mapping
Texture mapping on cubes, boxes and planes.
Texture credit
Various scalings on textures are permitted, independent of any transformations on the primitive.
Objective 4: Bump Mapping
A brick sphere with bump mapping enabled. Observe the deformities in the specular highlights.
The same sphere with bump mapping enabled. Note the smoothness of the specular highlights.
Objective 5: Perlin Noise
Perlin noise was accomplished using linear interpolation. A wood-like texture was created by manipulating the generated Perlin data.
Objective 6: Soft Shadows
A cube with soft shadows.
The same cube without soft shadows.
A model with soft shadows, including self shadows.
Soft shadows are supported with spherical light sources.
Objective 7: Adaptive Anti-Aliasing
First, render the scene without any anti-aliasing
Then use a simple edge detection technique to find pixels to anti-alias. Here, the chosen pixels are highlighted.
Re-render the chosen pixels using supersampling.
Detail of the first picture, before anti-aliasing.
Detail of the third picture, after anti-aliasing.
A more efficient anti-aliasing technique was devised wherein only pixels that were near an edge would be rendered with more samples.
Objective 8: Depth of Field Effect
Abandoned due to time and performance constraints.
Objective 9: Uniform Spatial Subdivision
512x512, 4 threads, no anti-aliasing, 2464 triangles. No subdivision: 25.7s
Uniform grid: 6.8s
512x512, 4 threads, no anti-aliasing, 46336 triangles. No subdivision: 143.3s
Uniform grid: 27.4s
Models are partitioned into a uniform grid, and each triangle is placed into voxels. Intersection of a ray and the model are first tested on the voxel bounding boxes before testing the triangles. This results in a considerable efficiency, particularly in more complicated models.
Objective 10: Final Scene — The Immortal Game
The Immortal Game is one of the most remarkable chess games of all time. This scene shows the final position on the board for the game between Adold Anderssen and Lionel Kieseritzky in 1851. White (glass pieces) is down two rooks and a bishop and is has just sacrificed his queen to force black's knight on g8 to give up its protection of the e7 square, allowing white's remaining bishop on d7 to fatally check the black king by moving to e7 after the knight takes the queen. Despite only having three minor pieces available against a full complement of black's pieces, white amazingly manages to eke out a win.
Full resolution, uncropped. 1000x1000, 16xSSAA (adaptive). Approximately 100k triangles. Rendering time: 11 hours, 47 minutes on Google Chrome 41 on Windows 8.1, running on four threads on a Lenovo Y500 with a Intel i7-3630QM.
Black's pieces are slightly reflective (objective 1). White's pieces are refractive (objective 2). Texture mapping is used to create the main chess board (objective 3). Bump mapping is visible on the table (objective 4). Perlin noise is used to generate the border on the chess table (objective 5). Soft shadows are visible to the right of all pieces (objective 6). Adaptive anti-aliasing was used to smooth out edges (objective 7). Octrees were used instead of Uniform Space Partitioning (objective 9) due to performance constraints.
Extra Objectives
The following extra functionality was implemented on top of the stated objectives.
Octrees
512x512, 4 threads, no anti-aliasing, 2464 triangles. No subdivision: 25.7s
Uniform grid: 6.8s
Octrees: 4.9s
512x512, 4 threads, no anti-aliasing, 46336 triangles. No subdivision: 143.3s
Uniform grid: 27.4s
Octrees: 6.8s
Octrees were implemented on top of the uniform spatial subdivision implemented in objective 9. Octrees provide much improved performance for complex models static scenes, and were required for the resonable rendering of the final scene.
Parallel rendering
512x512, no anti-aliasing, octrees, 4-core processor with hyperthreading.1 thread: 436.5s
2 threads: 223.6s
4 threads: 169.3s
8 threads: 160.1s
Multithreading was implemented using the Web Workers API, for the parallel rendering of the scene.
Dithering
No dithering. Click on image to view in detail.
Random dithering. Click on image to view in detail.
Difference between the two images, with color differences exaggerated by a factor of 256.
Though 32-bit color depth is good enough for almost all intents and purposes, some banding is still noticible in smooth gradients, since there are only, for example, 256 shades of pure grey. Random dithering eliminates any perceived banding.




