Lab 02: Green Mountain Sunset

Goals

By the end of this lab, you will:

  • practice with intersections of rays with spheres and triangles,
  • use the gl-matrix library to perform linear algebra operations.

In this lab, you will start writing your own ray tracer. The objects in our scene will be defined in 3d, but we will mostly be rendering 2d-like scenes. We'll make our scenes look more 3d-like when we talk about shading techniques next week.

Please navigate to our replit Team and open the assignment. Please find someone new to work with and create your group (only one person should create the group).

When you click the "Run" button, you should see a sunset-like background which transitions from pink (top) to blue at the bottom:

All of the code you will write will be in the sunset.js file which contains a few class definitions. In particular, there are class definitions for a Ray, Sphere, Triangle and the SunsetRenderer class.

Note that the SunsetRenderer takes in (1) the canvasId (string) of the canvas in which we would like to render our scene, (2) the vertical field-of-view (fov), as well as the objects in the scene. The first object is an instance of the Sphere class which represents the sun. Next, there is an array of Triangle objects which has a length of 3, for the three triangles representing the Green Mountains.

The SunsetRenderer class saves the incoming information (fov, canvas, sun and mountains), which is then used in the render method to render the scene. Your job primarily consists of modifying the render method as well as the intersect method of the Sphere and Triangle classes.

Note that there is already a loop over the pixels in the scene in the SunsetRenderer render method. The relative y-coordinate is used as the interpolation variable between blue and pink to define the initial background sky color.

Part 1: Creating rays.

Please review the notes and exercise from this week on how to create rays from a camera that pass through a particular pixel. Create a ray within the body of the nested for-loop (i.e. for each pixel). You should use the fov as well as the image (or canvas) dimensions (and aspect ratio). You can use $d = 1$ as the distance from the camera (eye) to the image plane. The eye is located at the origin $(0, 0, 0)$ as we assumed in class.

Part 2: Ray-sphere intersections for the sun.

Please complete the Sphere intersect method to detect intersections between the ray you compute in Part 1 and the sun. The sun is centered at $(-4, 1.5, -10)$ and has a radius of $2$. The color of the sun is already set up for you as:

const cSun = vec3.fromValues(1.0, 0.894, 0.71);

When the ray intersects the sun, please set the pixel color to cSun, and your image should look similar to the following picture:

Part 3: Ray-triangle intersections for the Green Mountains.

Now complete the Triangle intersect method to detect intersections between a ray and a single triangle. Remember to check that all three barycentric coordinates are in the valid range of [0, 1]! Then, set the pixel color according to the closest intersection point (minimum ray $t$ value) between all triangles and the sun. There are two colors to use for the Green Mountains: cMtn1 and cMtn2, already defined for you right after the definition of cSky. The first and third triangles have a color of cMtn1 and the second triangle (this.mountains[1] of the SunsetRenderer class) has a color of cMtn2.

Once this is complete, your image should look similar to the image at the top-right of this lab description. It won't look exactly the same since I changed some parameters after creating that picture, but it should be close. Feel free to adjust the triangle points in the index.html file if you like (but please make sure the same order of the mountains is preserved).

glMatrix alert!

glMatrix doesn't allow you to pass two indices (i.e. row and column) when accessing elements in a matrix - there is only one index. glMatrix actually stores matrices in column-major order (see this picture) in a single 1d array, which means that for a 3x3 matrix like this:

$$ M = \left[ \begin{array}{ccc} a & b & c \\ d & e & f \\ g & h & i \end{array}\right], $$

the corresponding code to represent this matrix using glMatrix would be:

let M = mat3.create();
M[0] = a;
M[1] = d;
M[2] = g;
M[3] = b;
M[4] = e;
M[5] = h;
M[6] = c;
M[7] = f;
M[8] = i;

In other words, think about going one column at a time when indexing the mat3 (as a 1d array) to access the elements.

Part 4: Samples per pixel (spp)

Remember the 0.5 embedded in the equations for $x$ and $y$? Please change this to Math.random() to generate random numbers between 0 and 1. This will now sample each pixel at a random point within the pixel instead of the center. The objects in your scene should look pretty rough with a single sample. Next, add a loop (nested within the i and j loops for each pixel) to send several random rays within each pixel. The final color you assign to the pixel should be the average of all the colors you compute with your random samples - here is a description of this algorithm:

1. for each pixel:
  a. set color = (0, 0, 0) (this will be the variable we use for the final color)
  b. for some # samples (spp):
      i. cast a ray through this pixel (at a random sample location)
      ii. compute the color from this sample
      iii. add the sample color to the final color
  c. divide the final color by the # of samples (to compute the average)
  d. set pixel color to the final color

You should start to see the edges of the mountains look smoother at higher spp. Notice there is a console.log message at the end of the render function which reports the elapsed time to render the scene (in milliseconds) and saves this in the elapsed variable. Please calculate the framerate (in FPS), and complete the table in the questions.md file with the framerate for 1spp, 4spp, 16spp and 32spp.

You can open a tab on the right of your repl to preview the Markdown. Here is some info on what you can do with Markdown in replit:

https://docs.replit.com/tutorials/replit/markdown

Part 5: Distance to the image plane

This part involves a theoretical question and can be completed at any point while working on the lab.

In the implementation for this lab, we set $d = 1$ (we also used this for the in-class exercise). Try setting $d = 10$. Now try $d = 100$. What happens? Recall the expressions we developed for the pixel sample coordinates, ultimately leading to the ray direction:

$$ x = -\frac{w}{2} + w \frac{(i + 0.5)}{n_x}, \quad y = -\frac{h}{2} + h \frac{(n_y - 0.5 - j)}{n_y}, \quad z = -d. $$

And the ray direction is $\vec{r} = \frac{\vec{u}}{\lVert \vec{u}\rVert}$ where $\vec{u} = [x, y, z]^T$ - note we are still assuming the eye is at the origin of our coordinate system.

Please show that $\vec{r}$ is independent of $d$ - i.e. it only depends on $\alpha$, $n_x$, $n_y$, $i$ and $j$. You can omit the $0.5$ if you like (which means we sample each pixel at the top-left corner of the pixel). Feel free to make any other substitions to make the derivation more concise, for example, substituting a helper variable for the aspect ratio: $a = n_x / n_y$.

Please enter your derivation in the questions.md file in your repl.

Unfortunately, math equations are not supported in replit's Markdown, but you can always take a picture of your equations and upload the picture to your repl. Alternatively, you can create a .draw file to draw your equations in an Excalidraw whiteboard.

Submission

The initial submission for the lab is due on Wednesday 09/27 at 11:59pm EDT. I will then provide feedback within 1 week so you can edit your submission.

When you and your partner are ready, please submit the assignment on replit. I will then make comments (directly on your repl) and enter your current grade status at the top of the index.html file.

Please also remember to submit your reflection for this week in the Google Form below.



© Philip Claude Caplan, 2023 (Last updated: 2023-09-21)