{{FULL_COURSE}} Homework 3 - Rasterizer


0 Goal

To create a rasterizer for drawing scenes composed of polygons. This rasterizer will be the basis for a 3D scene renderer next week.

1 Supplied Code

We provide you code to read 3D scene files in OBJ format, some sample models, and a sample camera that should work well with all the provided models. You will also have to incorporate your linear algebra library and PPM reading/writing code into this project. Copy your mat4.cpp and vec4.cpp files into the project before you begin coding.

Click here to download the basecode.

2 Help Log

Maintain a log of all help you receive and resources you use. Make sure the date and time, the names of everyone you work with or get help from, and every URL you use, except as noted in the collaboration policy. Also briefly log your question, bug or the topic you were looking up/discussing. Ideally, you should also the answer to your question or solution to your bug. This will help you learn and provide a useful reference for future assignments and exams. This also helps us know if there is a topic that people are finding difficult.

3 Preparatory Questions

Before you begin the programming portion of this homework assignment, read and answer the following conceptual questions. These will not be graded but will help you plan out your work.

4 Code Requirements

You are welcome to modify your vec4 and mat4 files (including the headers) from the last assignment. You will submit your modified versions with this assignment, so you are free to add/remove/change any methods or functions you wish. You may also create as many additional .cpp and .h as you like. You will submit all of your code, including the .pro file as a ZIP archive. (You should not need to modify the TinyOBJ code we provide, but you may do so if you wish, since you will need to submit it with everything else anyway.)

Create a C++ command-line (non-GUI) project in Qt Creator, then download and add tiny_obj_loader.h and tiny_obj_loader.cpp to it. Both files are included in the base code. Also add copies of you vec4 and mat4 code and your PPM reading code. You may create either a non-Qt or a Qt project. You do not need Qt for this project, but you are free to use Qt libraries such as QVector, QString, and QImage if you want. The standard C++ libraries such as std::vector will work just as well.

4.1 The TinyOBJ Library

You will use the TinyOBJ library to parse object files into data structures that can be used by your rasterizer. It consists of only two files you downloaded in the previous step, and you only need to use a single function, which will become available if you include tiny_obj_loader.h:

tinyobj::LoadObj(std::vector<shape_t> &shapes, std::vector<material_t> &materials, const char *filename, const char *mtl_basepath = NULL);

TinyOBJ uses the C++ standard library’s std::vector as a resizeable array and works by passing in references to empty std::vectors for the shapes and materials parameters. filename refers to the path to the object file. If the function executes correctly, TinyOBJ will have filled those vectors with shapes and materials parsed from the object file. You can check if this happened by checking if the string returned by LoadObj is the empty string.

Shapes are represented as shape_t structs, and materials as material_t structs. Each shape_t contains a mesh_t struct, which in turn holds a collection of triangles. You will need to rasterize all the triangles in all the meshes. The mesh_t data structure contains the following members that you will need to use.

4.2 Cameras

Camera positions are specified in ASCII text files in the following format (all numbers are floats):

left right bottom top
near far

eye_x eye_y eye_z
center_x center_y center_z
up_x up_y up_z

The first 6 numbers are the view frustum parameters. eye is the camera's position. center is a point that the camera is look straight at, so the z-axis (or forward in the lecture slides) is center - eye. up is the y-axis. Make sure to normalize both the y- and z-axes, then use a cross product to compute the x-axis. We are using this representation of the camera position and orientation to match OpenGL's traditional gluLookAt function.

You will need to compute the projection matrix using the frustum formula, and the view matrix, using the eye, center, and up values. See the documentation for gluLookAt for the view matrix formula. Use the formula below for your frustum:

$$ F = \begin{pmatrix} \frac{2n}{r - l} & 0 & \frac{r + l}{r - l} & 0 \\ 0 & \frac{2n}{t - b} & \frac{t + b}{t - b} & 0 \\ 0 & 0 & \frac{f}{f - n} & \frac{-fn}{f - n} \\ 0 & 0 & 1 & 0 \end{pmatrix} $$

Note that this formula is slightly different than the formula used by OpenGL's glFrustum: OpenGL's convention is to map the near plane to z = -1, but we are mapping it to z = 0 instead. The formula is also appears different than the one based on field-of-view in the lecture slides, although it isn't really. If you assume the camera is looking at the middle of the window, then \(b = -t\) and \(l = -r\). So \(\frac{2n}{t - b} = \frac{n}{t}\). Now consider the triangle formed by the camera center (0, 0, 0), the center of the image at the near plane's depth (0, 0, n), and the point at the top center of the near plane (0, t, n). This is a right triangle whose angle at the origin is half the field of view (remember the field of view stretches from \(t\) to \(-t\)). So the ratio of the opposite edge (from (0, 0, n) to (0, 0, t)) over the adjecent edge (from (0, 0, 0) to (0, 0, n)) is \(\tan \frac{fov}{2}\). The formula above uses the reciprocal \(\frac{2n}{t - b} = \frac{n}{t} = \frac{1}{\tan{\frac{fov}{2}}}\).

4.3 Rendering Triangles

Create a program rasterize that takes the following command lines options:

rasterize <input.obj> <camera.txt> <width> <height> <output.ppm> [--color_option]

Read in the specified OBJ and camera file (you will need to write the code for reading in a camera yourself), and rasterize all the triangle in the OBJ into an image of the specified width and height:

  1. For each triangle, multiply each vertex coordinate by the view matrix, then the projection matrix, divide through by w, and convert the x- and y- coordinates to pixel coordinates using the formula from the slides.
  2. If all three z-coordinates are less than 0 or all three are greater than 1, the triangle is completely in front of the near plane or complete behind the far plane. So skip it because it isn't visible.
  3. Compute the 2D bounding box of the triangle. If it doesn't overlap the image at all, skip the triangle because it isn't visible.
  4. For every row of the image, calculate the x-coordinates where the row crosses each edge of the triangle. Figure out which two edges it actually crosses.

4.4 Z-Buffering

Add a z-buffer, which should be an array with one float per pixel. Initialize each z-buffer value to 2 (or any other value > 1), so that any triangle that is inside the frustum will have a smaller z-value. Update your code to only color a pixel if the triangle's z-coordinate (after projection) is between 0 and 1 and is smaller than the value stored in the z-buffer. Whenever you do color a pixel, update the z-buffer with that point's depth.

You need to use tri-linear interpolation to calculate the depth value at every pixel. Once you figure out where the scan line enters and exits the triangle, interpolate the z values at those points. For each pixel along the scan line, interpolate beween the z values of the two end points.

4.5 Additional Required Elements

Implement the following command-line options to determine how to color each pixel:

4.6 Extra Credit

The following extra features will all qualify for extra credit. If you have other ideas for enhancements, please check with a member of the course staff: