Project 3: Transformations
The required functionality in this lab is to implement the OpenGL matrix stacks and associated methods to help project from world coordinates to screen coordinates, as well as basic clipping and depth buffering. It is worth 40 points.
The remaining 60 points of functionality can come from clipping, a camera toolkit class, matrix setting methods, and related extensions.
Overview
In project 2 you wrote methods for drawing basic shapes provided in screen-coordinates. This project will deal with the OpenGL methods which convert from 3D world coordinates into screen coordinates so that 3D objects may be drawn with your earlier methods.
The basic idea is that, given a vector x in world coordinates, there is some matrix M such that M x is the point in screen coordinates. OpenGL splits this matrix into three matrices: a viewport transformation (which is not actually a matrix), a projection matrix, and a modelview matrix. For easy of notation I'll refer to these as V, P, and M, respectively. They are always applied in order; that is, screen = V * (1/w) * P * M * world
The details of matrix manipulation is given below.
In addition to the matrices, there are two additional tasks performed to project from the world view to the screen: division by w and depth buffering.
Throughout, the user will presumably specify vertices in floating-point coordinates. After multiplying them through the matrices and the viewport, you can change the x and y coordinate to integers, if your drawing code from project 2 needs that (don't convert the z coordinate to an integer).
Make sure you removed the glOrtho(0,640, 0,480, -1,1) line inserted in project 2.
About that w coordinate...
In class, in the book, and commonly throughout the computer graphics world, it is assumed that the w coordinate of every vector is 1 and the bottom row of every matrix is (0, 0, 0, 1). From a practical standpoint that is true. From an implementation detail standpoint, though, many graphics libraries actually store in that w coordinate a number for assisting in perspective projections. Due to some principles of homogeneous coordinate mathematics, even when it is not 1 it can still be used for translation. This project puts in place the machinery necessary to do perspective projections later, so you will need to allow for w ≠ 1 , but it shouldn't cause any problems.
OpenGL is clipping/overlapping/messing up my raster!
OpenGL treats your raster just like any other thing it will draw.
As such, it might get depth clipped, frustum clipped, moved with the viewport, or any of a number of other unfortunate things. The way around that is to add yet more code to the already extensive raster drawing code:
GLboolean clippingWasOn[8];
for (int i=0; i<8; ++i) {
clippingWasOn[i] = glIsEnabled(GL_CLIP_PLANE0+i);
glDisable(GL_CLIP_PLANE0+i);
}
GLboolean depthWasEnabled = glIsEnabled(GL_DEPTH_TEST);
glDisable(GL_DEPTH_TEST);
GLint oldviewport[4];
glGetIntegerv(GL_VIEWPORT, oldviewport);
glViewport(0,0,640,480);
GLint oldmatrixmode;
glGetIntegerv(GL_MATRIX_MODE,&oldmatrixmode);
glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity();
glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity();
glRasterPos2f(-1,-1);
glDrawPixels(640,480,GL_RGB,GL_FLOAT,raster);
glMatrixMode(GL_PROJECTION); glPopMatrix();
glMatrixMode(GL_MODELVIEW); glPopMatrix();
glMatrixMode(oldmatrixmode);
glViewport(oldviewport[0],oldviewport[1],oldviewport[2],oldviewport[3]);
if (depthWasEnabled)
glEnable(GL_DEPTH_TEST);
for (int i=0; i<8; ++i) {
if (clippingWasOn[i])
glEnable(GL_CLIP_PLANE0+i);
}
***************New*****************
Some people have had questions about the process you go through to draw stuff in 3D. Here is a short list of the things you
should probably do:
1. Draw your triangles/polygons (i.e. call glBegin, glVertex3f, etc.)
2. Multiply all points by the modelview and projection matrices
3. Divide by w
4. Clip everything to the square from -1,-1 to 1,1 (This is by convention, and because it makes other things work out nicely).
However, if you are only doing point clipping, you can skip this part and do it when you plot individual pixels. If you do that,
you just have to check to make sure the point is in the viewport (not just the screen).
5. Size to your viewport. Basically, add 1 to all x and y coordinates, and then divide by two. This will put all your points
between 0,0 and 1,1. Then, multiply your x coordinate by the viewport width and add the x viewport min. Do the same to
your y coordinate with the height and y viewport min.
6. Use your triangle/polygon/line code as usual using the new coordinates.
7. When you set each pixel, check the z buffer to see if you should set the pixel. This is essentially your clipping in z.
***************New*****************
Required Functionality
In order to receive any credit for this lab, you must implement the following portions of the OpenGL API. Completing the required functionality is worth 40 points.
| Functionality |
Viewport transformation
| | Description |
The active viewport is a rectangle, specified by glViewport( xmin, ymin, width, height ). The viewport transformation should transform the square from -1 to 1 in x and y onto the current viewport, leaving z and w alone. Thus, if the user specifies (-1, -1), you should treat that as the pixel (xmin, ymin); likewise, treat (1, 1) as (xmin+width, ymin+height).
You should not implement this as a matrix because by this step the w coordinates might have been trashed, which prevents matrices from doing translation.
OpenGL lets you make the viewport lie off the screen. To prevent annoying issues with memory access, array bounds, and such we will not do that in this lab.
| | Example |
Change all of project 2 test code to have point coordinates between -1 and 1; you should see them on the screen. Then try changing the size/shape of the viewport; you should see the same image, stretched to fit the new viewport.
| | Credit |
Required
| | References |
pp. 299-305
| | Related electives |
none
|
| Functionality |
Divide-by-w
| | Description |
After a point is multiplied through M and P and before it is point-clipped and sent through the viewing transform, you need to divide the point by its w coordinate (eg, (1, 2, 3, 4) becomes (.25, .5, .75, 1)).
In the next project one of the electives (perspective-correct interpolation) will ask you not to divide the w coordinate by itself in this step (eg, (1, 2, 3, 4) becomes (.25, .5, .75, 4) instead of (.25, .5, .75, 1)). It doesn't matter either way for this project.
| | Example |
Set a non-1 w value for some vertex. You might also try loading a matrix (see Matrix manipulation below) that copies one coordinate into the w field: for instance, 1 0 0 0
0 1 0 0
0 0 1 0
0 1 0 0
As a sneak peak, perspective projection is basically copying z into the w field, though there is a little more to it than that, as you will see in the next project.
| | Credit |
Required
| | References |
| | Related electives |
|
| Functionality |
Depth buffer
| | Description |
Currently, your drawing methods interpolate (x,y) position and color.
Change them to also interpolate (z).
Make an array like raster but containing z values, not colors.
Only draw pixels with z values less than those stored in the z-buffer.
Any time you draw a pixel, put it's z value in the depth buffer.
Initially, the z-buffer should be all 1 s.
When glClear(GLint mode) is called and (mode & GL_DEPTH_BUFFER_BIT) is not zero, reset all z-buffer values to 1.
Only read/write from the depth buffer if GL_DEPTH_TEST is enabled.
It starts disabled.
| | Example |
Draw two triangles of different colors with coordinates (-0.5, 0.2, 0.5) (0.0, -0.5, 0.0) (0.5, 0.2, -0.5) and (-0.5, -0.2 -0.5) (0.0, 0.5, 0.0) (0.5, -0.2, 0.5); you should see them intersect.
| | Credit |
Required
| | References |
pp. 531-544
| | Related electives |
glDepthRange, glPolygonOffset
|
| Functionality |
Easy (point) clipping
| | Description |
Never set a pixel outside the viewport. Never set a pixel if its z value is outside the [-1, 1] range. You can discard entire lines or polygons if all of their vertices fall outside the same side of this clipping region.
| | Example |
Set the viewport to be smaller than the window (in the sense of a window created by the operating system with a title bar, etc) and draw some shapes overlapping the edges.
Additionally, due to clipping in z , a triangle with vertices (0,0,0), (0,.5,-2), (0,-.5,2) should show up as a five-sided polygon.
| | Credit |
Required
| | References |
pp. 315-338
| | Related electives |
Frustum clipping, Clipping planes; Frustum clipping even does this for you.
|
| Functionality |
Matrix manipulation
| | Description |
Implement the following, where C is the current matrix, as specified by the last call to glMatrixMode(GLenum):
- glLoadIdentity(): set C to be the identity matrix
- glLoadMatrixd(double data[16]): load the supplied column-major data into matrix C.
- glMultMatrixd(double data[16]): load the supplied column-major data into matrix a new B and set C = C * B.
Also implement glGetDoublev(GLenum which, double data[16]), which loads the matrix specified by which (either GL_MODELVIEW or GL_PROJECTION) into the data array in column-major format.
At each glVertex*(...) call, multiply the point through the active modelview and projection matrices before drawing.
| | Example |
Try a few matrix combinations before your glBegin() calls and compare OpenGL to your code.
| | Credit |
Required
| | References |
Project 1
| | Related electives |
Other matrices
|
| Functionality |
Matrix stacks
| | Description |
For each matrix mode, there is a stack of matrices in addition to a single active matrix.
OpenGL only guarantees the ability to push 2 projection
and 32 modelview matrices, so limit your test code to that.
When glPushMatrix() is called, push a copy of the active matrix onto the appropriate matrix stack.
When glPopMatrix() is called, replace the active matrix with the top matrix from the appropriate stack.
| | Example |
Here's a funky shape using the matrix stack in C:
tree(int depth) {
static const double r2 = 1/sqrt(2);
static const double mdown[16] = { 0,-r2,0,0, r2,0,0,0, 0,0,1,0, 0,-r2,0,1 };
static const double mup[16] = { 0,r2,0,0, -r2,0,0,0, 0,0,1,0, 0,r2,0,1 };
if (depth <= 0) return;
glBegin(GL_LINES);
glVertex2f(0,-r2);
glVertex2f(0, r2);
glEnd();
glPushMatrix();
glMultMatrixd(mdown);
tree(depth-1);
glPopMatrix();
glPushMatrix();
glMultMatrixd(mup);
tree(depth-1);
glPopMatrix();
}
void draw() {
...
tree(8);
...
}
| | Credit |
Required
| | References |
Project 1
| | Related electives |
Other matrices
|
Elective Functionality
To earn the remaining 60 points for full credit on this lab, choose from among the following functionailities to implement.
| Functionality |
Standard matrix setting methods
| | Description |
It is annoying to have to enter every matrix by hand.
As such, OpenGL provides a large set of convenience methods.
For each of the following methods, generate the appropriate matrix and multiply the current matrix by it.
- glRotatef( ang, x, y, z ): rotate
ang degrees (not radians) through an axis passing through (x,y,z) and the origin.
- glTranslatef( tx, ty, tz ): translate such that a point at the origin before will end up at (tx,ty,tz).
- glScalef( sx, sy, sz ): multiply the x coordinate by sx, etc.
- glOrtho(left,right,bottom,top,near,far): translates and scales so that the given box becomes [-1, 1] in each direction.
| | Credit |
10 points each
| | References |
pp. 262-278; see also the various man pages.
|
| Functionality |
Other matrices
| | Description |
Add the Color and Texture matrices and their stacks. These function just like the 4x4 matrices you have already added (and yes, colors and texture coordinates default to (x, y, 0, 1) just like vertices) and are applied in the same way.
Color matrices are part of the GL_ARB_imaging extension and might not be implemented on older graphics cards.
| | Example |
We haven't dealt with textures yet, so no examples there.
For color, try [0,1,0,0, 1,0,0,0, 0,0,1,0, 0,0,0,1] to swap red and green channels, or [-1,0,0,1, 0,-1,0,1, 0,0,-1,1, 0,0,0,1] to make a negative exposure.
| | Credit |
5 for color only, 10 for both.
| | References |
|
| Functionality |
Frustum clipping
| | Description |
Clip lines, points, and triangles so that –w ≤ x ≤ w, –w ≤ y ≤ w, and –w ≤ z ≤ w in world space. This happens after multiplying by the matrices and before the viewport transformation. Any clipping algorithms discussed in class (except the point-clipping method) should work.
Note that this is six different clipping planes, which can be implemented using the clipping plane elective if desired; for example x ≤ w can be rewritten as –1 x + 0 y + 0 z + 1 w ≥ 0, etc.
| | Example |
Make a viewport smaller than the window.
Turn off point clipping.
Draw some huge shapes.
| | Credit |
5 for points, 10 for lines, and 10 for triangles (25 for all 3).
| | References |
pp. 315-338
|
| Functionality |
Clipping Planes
| | Description |
Implement glClipPlane(GL_CLIP_PLANEi, GLdouble[4]) (and glEnable(GL_CLIP_PLANEi); note that it is always the case that GL_CLIP+PLANEi = GL_CLIP+PLANE0 + i).
Suppose that the vector provided is p;
then let q = p ⋅ M–1 (which moves q into eye space); store q as the clipping plane equation.
If a clipping plane is enabled when a shape is generated,
clip so you keep only qx x + qy y + qz z + qw w ≥ 0.
| | Example |
A clip plane of [1,.2,0,-.5] will take a diagonal chop off the left side of the screen.
| | Credit |
5 for points, 10 for lines, and 10 for triangles (25 for all 3).
| | References |
pp. 315-338
|
| Functionality |
gluLookAt(ex,ey,ez, cx,cy,cz, ux,uy,uz) |
| Description |
This method, from the GLU library which extends OpenGL and is included with virtually every OpenGL distribution, is essentially your "camera" method, and defines a camera which has it's lens at (ex,ey,ez), is looking toward (cx,cy,cz), and is oriented so that it's up vector is as close to (ux,uy,uz) as it can be. Thus, you are to modify the currently active matrix with a rotation matrix such that points previously at (ex,ey,ez) are moved to (0,0,0); points previously along the line between (ex,ey,ez) and (cx,cy,cz) are moved to (0,0,-d) where d is their former distance from (ex,ey,ez), etc.
| | Credit |
10
| | References |
pp. 351-356; see also the "gluLookAt" man page.
Please note that many, if not most, versions of the OpenGL man pages contain an error in the description of this method, normalizing the wrong subset of the vectors it discusses.
|
| Functionality |
Custom Matrix Setters
| | Description |
These are not part of the OpenGL standard. Implement them such that they work for your code AND hardware OpenGL code.
- fixedScale(sx,sy,sz, cx,cy,cz): scale such that the point (cx,cy,cz) does not move.
- fullRotate(ang, ax,ay,az, bx,by,bz): rotate ang degrees (or radians, your choice) around the axis passing through (ax,ay,az) and (bx,by,bz)
- shear(sxy, sxz, syx, syz, szx, szy): shearing, where x moves sxy * y, etc.
This is not part of the OpenGL standard. Modify the currently active matrix to scale by the given factors in sx, sy and sz about the fixed point (cx, cy, cz).
| | Credit |
10 each
| | References |
pp. 262-281.
|
| Functionality |
glDepthRange( near, far ), glClearDepth( depth ), glDepthFunc( mode )
| | Description |
After passing through M and P and being clipped, z values lie in [-1, 1]. The OpenGL spec states that these are mapped to the interval specified by glDepthRange before being drawn, and that 0 <= near <= far <= 1. Rarely used in code I've seen, this does allow for some odd effects like rendering in elliptical spaces.
When glClear is called with the GL_DEPTH_BUFFER_BIT bit set, we set the depth buffer to all 1s. That clearing value can be set to something else using glClearDepth( depth ). It also needs to be in [0, 1], just like the depth range.
The default depth buffer draws if and only if the new depth is less than the existing depth. Allow other comparators to be specified by glDepthFunc( mode ) where, e.g., GL_GEQUAL means you draw if the new depth is >= the old depth, etc.
| | Example |
Take a scene where lots of stuff intersects; set the depth range to [0, 1/2] for some of it and [1/2, 1] for other parts. The [0, 1/2] range images should be in front of the others.
Swap the depth comparator from GL_LESS (the default) to GL_GREATER. Front and back should swap.
Swap the depth comparator from GL_LESS (the default) to GL_GREATER, the clear depth to 0, and the depth range to [1,0] instead of [0,1]. Together, this should not change the scene at all.
| | Credit |
10
| | References |
see also the glDepthFunc man page.
|
| Functionality |
glPolygonOffset( factor, units )
| | Description |
For decals, projected shadows, painted lines, and other effects
it is often nice to ensure an object draws just a bit closer to the camera than another.
When GL_POLYGON_OFFSET_FILL is enabled, at each pixel with depth z, use (z + factor*z + units*1e-6) instead. Note that this assumes you did glDepthRange; if not, use (z + factor*(z+1) + units*1e-6) instead.
| | Example |
Render two different but co-planar polygons. For example, try
(.5,.3,.4) (-.5,.2,0) (.4,.3,-.4)
(.6,.36,.48) (-.6,.24,0) (.48,.36,-.48)
Without polygon offsets, numerical roundoff will introduce a lot of noise. By offseting either one, though, your should be able to make either one show up cleanly in front. The one in front should be in front even if you rotate the view around to the other side.
| | Credit |
10
| | References |
|
| Functionality |
Create a camera class
| | Description |
It is commonly useful to have a camera object for a scene,
rather than depending on direct calls to gluLookAt or other related methods.
Implement something like the following interface:
class Camera {
public:
// moves camera in global coordinates
void translateWorldSpace(x,y,z);
// moves camera relative to it's current orientation
void translateCameraSpace(right,up,back);
// rotates camera, without moving it; axis given in global coordinates
void rotateWorldSpace(theta,x,y,z);
// rotates camera, without moving it; axis in camera space coordinates
void rotateCameraSpace(theta,right,up,back);
void use() {
glMatrixMode(GL_PROJECTION);
// In project 4, load a perspective matrix here
// For now, just load the identity
glMatrixMode(GL_MODELVIEW);
// orient the camera here
}
}
You are free to use vector parameters or the like, if you wish. You can implement it internally by using matrices, vectors, a mix of the two, quaternions, or anything else you'd like.
Make the code work for both OpenGL and your implementation—in other words, don't modify your modelview or projection matrices directly, use glMultMatrix or the like instead.
| | Example |
The following calls are annotated with their cummulative expected results, where c is the camera, assuming you use degrees. Remember you'll need to add c.use() calls and something to render in order to see anything...
// assuming camera at (0,0,0) with (0,1,0) as up and (0,0,-1) as forward
c.translateWorldSpace(.2, 0, 0) // scene shifts to left (camera moves right)
// (.2, 0, -1) now centered
c.translateCameraSpace(0, .2, 0) // scene shifts down (camera moves up)
// (.2, .2, -1) now centered
c.rotateWorldSpace(45, 0, 1, 0) // camera turns to look to the left
// (-.8,.2,-1) now centered
c.rotateCameraSpace(45, 0, 1, 0) // turns further to the left
// (-.8,.2,0) now centered
c.rotateWorldSpace(45, 1, 0, 0) // camera banks; same scene as before, but rotated
// (-.8,.2,0) still centered
c.rotateCameraSpace(90, 1, 0, 0) // camera turns to look up
// (.2,1.2,-1) now centered
c.translateCameraSpace(0, 0, -.2)// camera moves forward
// (.2,1.2,-1) still centered
c.translateWorldSpace(0, 0, -.2) // camera moves down and forward
// (.2,1.2,-.8) now centered
| | Credit |
5 points per implemented method.
An additional 10 points for attaching the camera-space methods to mouse or keyboard input and making your graphics interactive.
This elective will prove valuable in writing a game for project 5 (if you choose to write a game, that is).
| | References |
A good understanding of gluLookAt should help. Also look at pg. 352-354, and in the OpenGl Red book pg. 121-122.
|
E-mail: leemhoward@gmail.com |