|
In this assignment, you will implement a scanline renderer by hand. Many commercial rendering packages use primarily a scanline rasterization techinque similar in spirit to how OpenGL renders objects. The goal of rendering is, in short, given a viewer (in this case an idealized camera) and a 3D representation of a scene, produce a 2D image. In assignment #4, you implemented a Camera class complete with a projection matrix. The projection matrix gets you halfway there: simply applying that projection matrix to the triangles in the 3D scene will give you a set of triangles in 2D that are correct from the viewpoint of that camera, but our triangles are still just a bunch of points, and looking at a bunch of points isn't very interesting! We have two problems: first we need to figure out how to shade the interior regions of the triangles, and second, we need to make sure that if two triangles overlap, the one that is closer to the viewer takes precedence. In graphics nomenclature, these two tasks are knows as rasterization and hidden surface removal.
Base Code: proj6.zip
For this assignment you will need to modify the following files: rasterizer.cpp
You will be given two base classes to help you on your way to writing the rasterizer. The ImageRenderer class is a subclass of the Renderer base class that you used in project #3 and you will implement the same Render method. ImageRenderer's Render method is already filled in for you, and uses OpenGL calls to simply take a 2D pixel buffer and display its contents in the current OpenGL window; however Render by default doesn't generate any actual image data, so you will start with a blank black screen (feel free to have fun with generating 2D images!). The class Rasterizer subclasses ImageRenderer filling in the GenerateImage method, which is where you will actually plot the pixels outputted by your rasterizer. In general this class is useful any time you want to use OpenGL as a 2D display. In the next project, you will subclass ImageRenderer again to implement a raycaster.
Thi Viewer has also been modified. Viewer is now a base class from which you may derive other Viewers. It has virtual methods that you may override to add functionality specific to your assignment. A sample derived class "myviewer" is included, which will allow you to switch between the OpenGL renderer and the rasterizer using the 'r' key, and between an orthographic and perspective camera using the 'c' key.
Due: Tuesday, Feb 28th
The first two methods you will implement are DrawTriangle and DrawLine. DrawTriangle will take in a set of 3 verticies, and 3 colors, and rasterize the given triangle into the pixel buffer m_pPixels. DrawTriangle starts at the top pixel of the triangle, and moves down, drawing scanlines that range from the far left side of the triangle to the far right (using DrawLine to draw them). Note that for this step, we will only look at the x and y coordinates of the triangle vertices and simply fill in the triangle with white pixels.
Once these methods are working, you should see three identical triangles as below:
You will want to verify that your method works by adding a few more DrawTriangle calls to your GenerateImage to test a variety of triangle configurations.
Methods to Modify/Implement
Just as in project #3, we will implement the DrawNode and DrawModel methods, except instead of making OpenGL calls, we will make calls to Rasterize into the pixel buffer, and instead of using OpenGL's matrix stack, we will need to keep track of matrices ourselves. To help out with this, the GetWorldMatrix method has been implemented for you in the Node class in scenegraph.h. This method will traverse the parent pointers of a node, composing the matrices at each level of the scenegraph to produce the correct object-to-world matrix for that node.
In our DrawModel function in project #3, we were just able to use GL_TRIANGLES, now, however we want to project 3D triangles in our scene into the space of our 2D pixel buffer. Our Camera already has most of the information we need to do this, but keep in mind that we need to scale the final 2D coordinates to the image size and image width as opposed to just the dimensions of our image plane in our canonical viewing volume.
We will want to modify GenerateImage to draw the root node of our scene (here is the venus model):
Make sure all the camera controls for the viewer match the result in OpenGL! (Note: some of the viewer controls have been altered to test this feature, left click rotates all the way around, right click zooms, middle click pans).
Methods to Modify/Implement
At this point your rasterizer should be accurately drawing all of the triangles in the scene, but there are a few problems! The order in which we draw the triangles determines which ones overlap the others. This makes sense in the world of 2D, but in 3D we want depth to determine visibility. The solution to this is a z-buffer! A blank z-buffer is included in the base code, modify the DrawTriangle and DrawLine functions to calculate the z-value at each pixel, and use the z-buffer to remove hidden surfaces.
In addition to adding a z-value to each pixel and interpolating it over the triangle, our triangles are not very interesting, so we will want to interpolate the color values at each vertex within the triangle as well. Note that you must use 'perspective correct' hyperbolic interpolation for both the z-value and the color value.
To implement this, we simply modify the parameter in our linear interpolation (Lerp) to scale by Z. For example, generic linear interpolation between two quantities, a and b, is defined as follows:
Lerp(p0,p1,t) = (1-t)*p0 + t*p1;
This defines a function such that when t=0, Lerp(p0,p1,t) = p0, and when t=1 Lerp(p0,p1,t) = p1, and in the middle it is a smooth blending of the two. To do perspective correct linear interpolation, we must take into account the z-value at p0 and p1 (these are z0 and z1 below). Perspective correct linear interpolation is achieved by making a change of variable of the parameter t to s, where s = t*z0/( t*z0 + (1-t)*z1). Perspective correct interpolation is then defined as:
PerspLerp(p0,p1,z0,z1,t) = (1-s)*p0 + s*p1
Here p0 and p1 were points, but they can be any quantity defined at the triangle vertices, such as colors, normals, or texture values, and in the case of the z-buffer, the z values of the points themselves, which is the special case: PerspLerp(z0,z1,z0,z1).
After implementing this correctly, the three triangles should be colored as follows:
(Note that the three triangles are identical in screen space, but vary in depth).
Methods to Modify/Implement
Just for fun, here are a few extra features you can add to your project:
Functions for the Rasterizer class: