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

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

Objective 4: Bump Mapping

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

Soft shadows are supported with spherical light sources.

Objective 7: Adaptive 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

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

Final Scene

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.

Model source

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

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

Multithreading was implemented using the Web Workers API, for the parallel rendering of the scene.

Dithering

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.