OpenGL Matrices

CS 481/681 2007 Lecture, Dr. Lawlor

The Builtin OpenGL Matrices

OpenGL has a small set of built-in matrices and matrix manipulation routines that are actually quite useful.  As with everything in OpenGL, you choose the current matrix to operate on using a gl... command, in this case glMatrixMode.

There are actually only three things you can pass to glMatrixMode:
The fixed-function hardware essentially performs the following GLSL code on each vertex:
    gl_Position = gl_ProjectionMatrix*gl_ModelViewMatrix*gl_Vertex;
Everything else is actually just a matter of convention.

OpenGL Matrix Manipulation

OpenGL has a whole slew of useful utility routines that act on the current matrix (as set by glMatrixMode):
There are lots of other routines.  You can also extract the matrix values and hand-manipulate them if you like:
	mat4 p; /* stores the Projection matrix */
glGetFloatv(GL_PROJECTION_MATRIX,&p[0][0]); /* read back the projection matrix */
mat4 t=mat4(1.0); /* soon to be a Translation matrix */
t[3].z += dz; /* set translation offset */
p=p*t; /* multiply projection matrix by translation matrix */
glLoadMatrixf(&p[0][0]); /* copy new matrix into OpenGL */


Pushing and Popping Matrices

The coolest thing about OpenGL matrices is pushing and popping.  Let's say to draw one little piece of your world, you need to translate and scale the coordinate system.  Then to draw some other piece, you need to go back to the original coordinate system, and translate and scale differently.  OpenGL makes this easy with glPushMatrix and glPopMatrix, which save the current matrix onto a little OpenGL-internal stack.
	glPushMatrix(); /* save old coordinate system */
glTranslatef(model1.origin.x,model1.origin.y,model1.origin.z);
model1.draw();
glPopMatrix(); /* restore old coordinate system */

glPushMatrix(); /* save old coordinate system */
glTranslatef(model2.origin.x,model2.origin.y,model2.origin.z);
model2.draw();
glPopMatrix(); /* restore old coordinate system */
After this piece of code, the matrix is unchanged.  Without the pushes and pops, you'd be translated by model1.origin + model2.origin.

Setting Up the Builtin OpenGL Matrices

The usual way to initialize the builtin OpenGL matrices is with a little chunk of code at the start of each frame, in the "display" routine.   You've got to at least set up the perspective divide and the camera orientation. My matrix setup code usually looks like this:
	// Load all needed matrices into OpenGL:
glMatrixMode(GL_PROJECTION);
glLoadIdentity(); /*<- clean out any old leftover matrices */
gluPerspective(90.0, /* <- Y camera field-of-view, in degrees */
win_w/(float)win_h, /* viewport's aspect ratio: always width over height */
0.01, /* Near clipping plane depth */
100.0 /* Far clipping plane depth */
);
/* if you're not Dr. Lawlor, you'd switch to GL_MODELVIEW here */
glTranslatef(0,0,-2.0); /* push world origin down the *unrotated* Z axis */
glMultMatrixf(&viewMat[0][0]); /* camera orientation: can also go under GL_MODELVIEW */

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
It's INCREDIBLY IMPORTANT that you begin each frame with a glLoadIdentity for each MatrixMode, because:

Matrix Setup Heresy

Some people prefer putting the GL_PROJECTION setup inside the reshape routine.  This is a bad idea, because some versions of GLUT don't call your reshape routine until the window is actually reshaped.  Plus, this makes it much tricker to animate, e.g., the camera field-of-view.

Other people prefer putting only the perspective divide in the GL_PROJECTION matrix.  This approach works with the fixed-function hardware's (horribly ugly) per-vertex lighting, which for specular lighting assumes the post-GL_MODELVIEW camera is sitting at the origin (fixed-function lighting happens in eye space).  Again, with programmable shaders, you can do anything you like--and I like doing lighting in world space, which is easiest if the *whole* camera transformation is in the GL_PROJECTION matrix.

So I'm something of a matrix heretic--I treat GL_MODELVIEW and GL_PROJECTION like they were actually GL_MODEL and GL_VIEWPROJECTION.  This means my matrices don't work with the fixed-function OpenGL specular lights, which are both clunky to use and ugly.  Since I prefer per-pixel specular lighting anyway, I don't mind this.

Normal Matrix

Normals are funny.  They're vec3's, since you don't want perspective on normals.   And they don't actually scale quite right--a 45 degree surface with a 45 degree normal, scaled by glScalef(1,0.1,1), drops the surface down to near 0 degrees, but actually tilts the normal *up*, in the opposite direction from the surface, to near 90 degrees.

Mathematically, if between two points a and b on the surface, dot(n,b-a)==0, then after applying a matrix M to the points, you want the normal to still be perpendicular.  The question is, what matrix N do you have to apply to the normal to make this happen?  In other words, find N such that
    dot( N * n , M * a - M * b) == 0

We can solve this by noting that dot product can be expresed as matrix multiplication--dot(x,y) = transpose(x) * y, where we treat an ordinary column-vector as a little matrix, and flip it horizontally.  So
   transpose(N * n) * (M*a - M*b) == 0         (as above, but write using transpose and matrix multiplication)
   transpose(N * n) * M * (a-b) == 0              (collect both copies of M)
   transpose(n) * transpose(N) * M * (a-b) == 0    (transpose-of-product is product-of-transposes in opposite order)

OK.  This is really similar to our assumption that the original normal was perpendicular to the surface--that dot(n,b-a) == transpose(n) * (a-b) == 0.  In fact, the only difference is the new matrices wedged in the middle.  If we pick N to make the term in the middle the identity, then our new normal will be perpendicular to the surface too:
    transpose(N) * M == I   (the identity matrix)
This is the definition for matrix inverses, so the "normal matrix" N = transpose(inverse(M)).

If you look up the GLSL definition for "gl_NormalMatrix", it's defined as "the transpose of the inverse of the gl_ModelViewMatrix".  Now you know why!