To create a rasterizer for drawing scenes composed of polygons. This rasterizer will be the basis for a 3D scene renderer next week.
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.
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.
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.
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.
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.
positions
: This is a vector of vertex
coordinates. Its length is a multiple of 3, and every three
elements are the x-, y-, and z- coordinates of one point. You
can use the positions as is, or you may wish to convert them
to a vector of vec4s, with a w coordinate of
1.normals
: This is a vector of normal coordinates
at each vertex. Since a triangle mesh is usually an
approximation of a more detailed surface, it is customary to
specify surface normals at each vertex rather than computing
them for each triangle. These can be used to calculate
smoother lighting effects that hide the triangle mesh
structure. You won't need to use these right away. When you
do, you may wish to convert them to a vector of vec4s
with a w coordinate of 0 (since they represent
directions rather than points).texcoords
: This is a vector of texture
coordinates; if the vector is not empty, there will be two
coordinates per vertex. You can ignore this unless you choose
to implement texture mapping as extra credit.indices
: This is a vector of vertex indices.
Every three indices specify the three vertices of a triangle
that you need to render. For instance, if the first three
array elements are 7, 94, and 13, you need to render a
triangle whose vertices are
at (positions[7*3], positions[7*3+1], positions[7*3+2)),
(positions[94*3], positions[94*3+1], positions[94*3+2)),
and (positions[13*3], positions[13*3+1], positions[13*3+2)).
You may wish to define a simple Face data structure
that contains three indices, and convert this data to a vector
of Faces.material_ids
: This is a vector specifying the
material for each face. There will be one entry for every
triangle in the mesh (i.e. one entry for every group
of three indices). It will be an index into the vector
of materials that LoadOBJ filled in. For now, use
the material's diffuse field as the triangle's color.
For some of the extra credit options, you will use additional
fields.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}}}\).
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:
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.
Implement the following command-line options to determine how to color each pixel:
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:
Submit a .zip file containing your code and the .pro file created by Qt Creator. Please don't include the build directory with your compiled code. Submit your helplog separately as an ASCII text file.