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:
- glMatrixMode(GL_MODELVIEW); is the common case.
This makes all subsequent matrix operations affect the
"gl_ModelViewMatrix", which is primarily used for model setup (for
example, shrinking the car model to fit next to the building model).
- glMatrixMode(GL_PROJECTION); switches to the
projection matrix, "gl_ProjectionMatrix". The projection matrix's
job is usually to perform the perspective divide, but I actually often
use it for the entire camera transform; basically ViewProjection
instead of just Projection.
- glMatrixMode(GL_TEXTURE); switches to the texture
matrix. This can be used to adjust the texture coordinates you
pass in. With programmable hardware, it's usually easier to just
adjust gl_MultiTexCoord0 yourself.
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:
- OpenGL doesn't reset the matrices after every frame, so it's easy to end up with horribly bizarre matrices, where you're perspective dividing over and over again. (Try this! It's hideous!)
- MPIglut (which we'll discuss in February) overrides glLoadIdentity to add its subwindow matrix.
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!