Graduates will implement a distribution ray tracer, discussed in Chapter 13 of the textbook as well as the 1984 SIGGRAPH paper Distributed Ray Tracing by Cook, Porter, and Carpenter.

Objectives

This assignment is designed to teach you advanced concepts for ray tracing, but using a methodology that corrects for many of the artifacts in basic ray tracing. Your distribution ray tracer will using recursion and sampling to:

  • Understand models for reflection, translucency, refraction, and shadows of shapes.
  • Apply advanced models for lights that allow for soft shadowing.
  • Modulate how cameras are used to implement antialiasing and depth of field effects.
  • Put all of these components together to produce realistic images a scenes using the framework of distribution ray tracing.

Software Design for Distribution Ray Tracing

As with a traditional ray tracer, your goal will be to develop a system for inverse modeling of the light transport within a scene. But, as you will discover, a number of the components for a distributed ray tracer need to be considered differently from the ground up for this task. Thus, your code will end up having a number of similar components to A03UG, but mixed in different ways.

You will also need to consider carefully generating random numbers and distributions. For this assignment, you are welcome to use C++11’s random number generating facilities instead of C++98’s rand() function. Please do not use any random number generating code that is not cross platform. A nice introduction to C++11’s random number generators can be found at this blog post, with more examples and documentation on cppreference.com. You can also use rand(), but your results may not look as good.

Most of the architecture designed around a standard ray tracer is providing generic functionality to compute when a ray hits the scene. When a hit is encountered, the color for the pixel is determined by using simple lighting models, like Blinn-Phong, and simple checks to skip lights, like hard shadows. In a recursive ray tracer, the central computation is instead not what the ray hits, but rather what color the ray should return. This method, called ray_color(), returns the amount of accumulated color intensity that a ray cast into the scene has found. This computation is done recursively, as when a ray hits a reflective or translucent object, the ray will bounce. Thus, ray_color() must also track a range of possible \(t\) values, \([t_\min, t_\max]\), to avoid hitting the same spot twice as well as a depth value to provide infinite recursive bounces.

Distribution Ray Tracing Algorithm

While recursive ray tracing suggests that a ray can bounce at each hit, distribution ray tracing suggests that multiple rays are bounced for each hit. This collection of rays models a distribution of possibilities. Distribution ray tracing is mostly easily understood by the summary of the algorithm at the end of Cook et al.’s paper, that highlights where such distributions are appropriate. In this assignment, you are required to implement all features except motion blur. Thus, your algorithm will look like:

  1. Choose a position of the eye (for depth of field) and choose a (sub-)pixel (for antialiasing). Cast a ray into the scene and determine what object is hit.
  2. Compute illumination from each light source by choosing a random position on the light (for soft shadows).
  3. Compute reflection by choosing a random direction near the ideal reflected direction (for glossy reflections) and recursively compute the color.
  4. For dieletric materials, choose a random direction near the ideal refracted direction (for glossy refraction) and recursively compute the color.

In the above, every time I mention the word choose you need to compute a random ray. There are a variety of ways to sample such rays, and you will need to be careful to test each.

Because of the stochastic nature of the above, you will then have to modify the high level loop around which you color each pixel. In pseudocode, your loop will look something like this:

for each pixel p {
  color total_c
  for i in num_samples {            //necessary for distribution sampling
    Ray r = camera.get_ray(p)
    c = ray_color(r, ...)           //additional parameters for t and depth
    total_c += c
  }
  total_c = total_c / num_samples   //necessary for distribution sampling
  p.set_color(total_c)
}

In the above num_samples is the number of samples that you will take for each pixel. Fortunately, this structure prevents you from having to separately perform a sampling loop for each of the above features. You can assume that if you sample enough, averaging all the resulting colors will allow you to blend together the effects of each element.

Note our scene files will be modified slightly from A03UG to include a specification of the number of samples. In particular, you should also support:

  • s, followed by 1 integer for the num_samples, the number of samples to compute per pixel

Part 1: Cameras and Rays for Antialiasing and Depth of Field

A standard ray tracing approach is to compute the ray from the eye through the center of each pixel. This produces staircase-like artifacts when sampling curve surfaces and sharp features that are the result of aliasing. One method to fix this is sample within the screen coordinate space bounded by the pixel. Such a strategy can be achieved by treating the integer pixel coordinates as a floating point value and sampling within plus or minus half a pixel in both the horizontal and vertical directions. While these floating point values do not refer to the center of a pixel, they can still be mapped to a three-dimensional world space coordinate by the camera, and thus can serve as a target to define the ray direction.

Averaging across all of these samples is similar to blurring the image, but the colors that you average together are the accumulated light intensities for rays, as opposed to averaging just on the pixel colors. Thus, it works much better that an image-based smoothing.

Similarly, perturbing the position of the eye within a small square (or disc) orthogonal to the view direction allows for creating depth of field effects. The idea is that you are sampling light from a lens with positive area as opposed to a pinhole camera. Because of this, it also will become important to make sure that you set an appropriate focal length. Luckily, you can use the lookat point to define the target focal length. Whereas in the standard ray tracer, the ray direction could be defined independent of the eye as in Chapter 4 of the textbook, you will now need to compute a new ray direction as the eye has moved away from \((0,0,0)\) in the \((u,v,w)\) coordinate space.

Put together, depth of field varies the position of the eye and antialiasing effectively varies the position of the pixel, but both are modifications that sample a distribution of possible view rays.

Part 2: Soft Shadows

Given a sampled ray from Part 1, our next step will be to incorporate distribution sampling within the ray_color() method. One way in which this can be included is to use a model for lights that is slightly more complex that point lights.

To achieve this, we will employ area lights. Unlike a point light, that has a fixed position defined by a single three-dimensional vector, area lights are defined by a parallelogram. To specify this, a user will set a position vector \(\mathbf{p}\) for one corner of the light, and two directional vectors \(\mathbf{a}\) and \(\mathbf{b}\). The parallelogram of light positions will then be defined as all points \(\mathbf{l}(\alpha, \beta) = \mathbf{p} + \alpha\mathbf{a} + \beta\mathbf{b}\) for \(\alpha, \beta \in [0,1]\).

To create a soft shadow, one simply samples from the set of light positions when computing illumination. If the light is a point light, you can assume that \(\mathbf{a}\) and \(\mathbf{b}\) are both \((0,0,0)\). Alternatively, if one is specifying an area light in the scene description, one will use:

  • A, followed by 3 reals for the position of the corner of the parallelogram, 6 reals for the \(\mathbf{a}\) and \(\mathbf{b}\) vector, and then 3 reals that are the RGB value (in the range \([0,1]\)) for the light.

Finally, because we will be using doing a recursive approach, instead of using the ambient and specular components of color, we will only compute the diffuse component of color. Otherwise, the (non-recursive) step of our approach will compute shadows and illumination using a similar approach to the standard ray tracer.

Part 3: Glossy Reflection and Refraction

Both reflection and refraction cast and additional ray and accumulate an additional color for each hit. To do so, when a surface is hit one will make an additional call to ray_color(), based on a sampling approach and the type of material that the hit surface is.

Because we are ignoring ambient and specular colors, your ray tracer should interpret these input parameters differently. In particular, in the standard ray tracer each shape was following by a sequence of 10 reals to describe its color:

  • \(k_a\) (ambient color, 3 reals for an RGB in the range \([0,1]\))
  • \(k_d\) (diffuse color, 3 reals for an RGB in the range \([0,1]\))
  • \(k_s\) (specular color, 3 reals for an RGB in the range \([0,1]\))
  • \(p\) (Phong exponent, 1 real in the range \([0,+\infty]\))

Instead, to support glossy reflection and refraction we will interpret these 10 values as:

  • \((a, 0, 0)\) (First real used as a side length \(a\) for perturbing reflect/refract direction, second two ignored)
  • \(k_d\) (diffuse color, 3 reals for an RGB in the range \([0,1]\))
  • \(k_m\) (mirror reflect color, 3 reals for an RGB in the range \([0,1]\))
  • \(n_t\) (refractive index, 1 real in the range \([0,+\infty]\))

We will approximate that the refractive index of the ambient “air” medium as \(n = 1.0\), meaning that refractive surfaces should only need to specify \(n_t\). To make our lives a little easier, we will use the convention that if \(n_t = 0\), the surface should neither reflect nor refract (this helps us out because solving for the refraction direction \(\mathbf{t}\) requires dividing by \(n_t\)). Note though that, technically, by the Schlick approximation, if \(n_t = 0\), then \(R_0 = (\frac{n_t-1}{n_t+1})^2 = 1.0\), which would imply no refraction and perfect reflection. Instead, to specify surfaces that have almost no refraction we will use values for \(n_t\) that are either very small, but above zero, or very large (in either case, as \(n_t\) shrinks or grows, \(R_0 \approx 1.0\)).

This simplifying assumption avoids having to build a more complex input file format that supports separately specifying metal and dielectric materials. Instead, we get to specify both with the same framework, and we can also include non-specular materials with the same specification. For the case where $$n_t = 0$, you are welcome to interpret the material parameters as traditional Blinn-Phong colors, but this is not required.

Because we are using distributed ray tracing, instead of computing both a refract ray and a reflect ray, we will use the value of \(R_0\) to send either a reflect ray or a refract ray. In particular, after computing \(R_0\), compute a random number between \(r \in [0,1]\). If \(r < R_0\), then we will proceed with a standard reflection. If \(r >= R_0\), then we will proceed with a standard refraction calculation (where you also check if you are entering or exiting the shape).

In addition, to achieve glossy effects, you should choose a random ray that is nearby the ideal reflect/refract ray. To do this, you will need to compute a coordinate system that is orthogonal to the ideal direction, and sample within a small span around this of size \(a\) (taken from the first coordinate of what was previously the \(k_a\) value. Setting \(a\) can be tricky, but in general the larger the value the more glossy the reflection and the less “metallic” the surface looks.

Finally, shadows behave a bit differently around gloss materials, in particular for materials that are translucent. The right way to handle translucent materials is to in fact bend the ray from the hit position to the light source. This is a bit tricky to do, so instead you can skip computing shadows for any dielectric material that has an \(n_t \neq 0\).

Part 4: Written Questions

Please answer the following written questions. You are not required to typeset these questions in any particular format, but you may want to take the opportunity to include images (either photographed hand-drawings or produced using an image editing tool).

These questions are both intended to provide you additional material to consider the conceptual aspects of the course as well as to provide sample questions in a similar format to the questions on the midterm and final exam. Most questions should able to be answered in 100 words or less of text.

Please create a commit a separate directory in your repo called written and post all files (text answers and written) to this directory.

  1. Given a vector \((1,1,0)\), construct an orthonormal basis. See Sections 2.4.5-2.4.7 of the textbook. State which vector \(\mathbf{t}\) you used. Show your work.

  2. Explain the three possible cases for a ray-sphere intersection.

  3. Explain the motivation for using distribution ray tracing to model glossy reflections as opposed to using ideal reflection.

  4. Exercise 4.2 on pg. 88 of the textbook.

  5. Exercise 10.2 on pg. 241 of the textbook.

Grading

Deductions

Reason Value
Program does not compile. (First instance across all assignments will receive a warning with a chance to resubmit, but subsequence non-compiling assignments will receive the full penalty) -120
Program crashes due to bugs -10 each bug at grader's discretion to fix


Point Breakdown of Features

Note: this assignment is graded out of 12 points instead of 10, and thus we are scoring it out of 120 instead of 100. A grade of 12 requires a score of 120/120.

Requirement Value
Consistent modular coding style 10
External documentation (README.md), Providing a working CMakeLists.txt 5
Class documentation, Internal documentation (Block and Inline). Wherever applicable / for all files 15
Expected output / behavior based on the assignment specification, including

Parsing the scene file5
Computing depth-of-field effects7.5
Computing antialiasing7.5
Computing soft shadows7.5
Implementing glossy reflection15
Implementing glossy refraction15
Displaying your ray traced scene using SDL, allowing the user to adjust the number of samples.5
Designing a scene of your choice that shows off your ray tracer's capabilities, submitted as myscene.txt and myscene.ppm7.5

70
Written Questions 20
Total 120