More GLSL: Camera and Lighting

CS 481 Lecture, Dr. Lawlor

Last class, we saw how GLSL generally works.  Now we're going to cover a few more specifics.

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;
As usual, with programmable shaders you can do exactly this, or whatever else you like!

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.  In C++ 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 */
gluLookAt(VEC3_TO_XYZ(camera_location),
VEC3_TO_XYZ(camera_location + camera_direction),
VEC3_TO_XYZ(camera_upvector));

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
It's INCREDIBLY IMPORTANT that you begin each frame with a glLoadIdentity for each MatrixMode, because:
Note that above I'm using my handy "VEC3_TO_XYZ" macro:
/* Convert a struct with .x, .y, .z fields into three arguments */
#define VEC3_TO_XYZ(v) (v).x,(v).y,(v).z

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 (gluPerspective) in the GL_PROJECTION matrix, not gluLookAt like me.  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, but fixed-function lights are both painful and ugly.  I prefer per-pixel specular lighting--see below!

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 (which is to say, the normal is perpendicular to the surface), 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)).

Try it!  If you have a weird non-uniform scaling, multiplying gl_Normal by the gl_ModelViewMatrix gives you normals that are clearly bad, while multiplying by gl_ModelViewMatrixInverseTranspose is clearly correct!  In GLSL,
	myNormalBad = normalize(vec3(gl_ModelViewMatrix * vec4(gl_Normal,0.0)));
myNormalOK = normalize(vec3(gl_ModelViewMatrixInverseTranspose * vec4(gl_Normal,0.0)));
myNormalHandy = normalize(gl_NormalMatrix * gl_Normal);
If you look up the GLSL definition for the handy "gl_NormalMatrix", it's defined as "the transpose of the inverse of the 3x3 submatrix of gl_ModelViewMatrix".  Now you know why!

GLSL "uniform" Variables: Smuggling Data into GLSL from C++

To do specular lighting, we need to know where the camera is.  Only C++ knows this.  Luckily, you can smuggle data from C++ into GLSL using a GLSL "uniform" variable.

Inside your GLSL (vertex or fragment) shader, you declare a uniform variable just like a 'varying' variable:
    uniform vec3 cameraLoc;
After compiling, from C++ you can set this uniform variable.  You do this by passing it's string name to "glGetUniformLocationARB", which returns an integer index (into a table of variables somewhere in the guts of your graphics card).  You can then set a "vec3" uniform's value by passing the uniform's location to glUniform3fvARB (or set a vec4 with glUniform4fvARB, etc).  Here I'm passing one (1) float-pointer as the camera location, which I've named "camera_location" in C++:
	glUseProgramObjectARB(prog);
glUniform3fvARB( /* set the "cameraCoords" uniform variable */
glGetUniformLocationARB(prog, "cameraLoc"),
1, /* <- number of variables to set (just one vec3) */
camera_location /* C++ variable new uniform value is read from */
);
Make sure your program is still bound in use before you try to set uniform variables; the "glUseProgramObjectARB" above is only needed once, but it is needed!

Examples of Lighting

There are several kinds of lighting we'll try out here:
To actually render lighting, you first need a whole set of unit-length direction vectors starting at the point on the surface you're shading.  In your GLSL fragment shader:
	vec3 N = normalize(normal); // Points away from surface
vec3 L1 = normalize(vec3(0,1,0)); // Points toward light 1
vec3 C = normalize(vec3(cameraloc)-position); // Points toward camera
vec3 H1 = normalize(C+L1); // Blinn's 'halfway vector' for light 1
Then you need to compute (clamped) dot products between these vectors:
	float a=0.2; // Ambient illumination
float d=clamp(dot(N,L1),0.0,1.0); // Diffuse light fraction
float s=pow(clamp(dot(N,H1),0.0,1.0),50.0); // Specular fraction. Constant controls highlight size
And finally you just combine these together--you can get lots of different effects by combining them in different ways.  Here I've added in "M" for the object ("Material") color, and "K" for a checkerboard pattern.


a- Ambient, 0.2

d- Diffuse, dot(N,L)

M- Material Color, gl_Color

s- Specular, pow(dot(N,H),...)


M*a
Typical Pure Ambient

M*d
Typical Pure Diffuse

M*(d+a)
Typical Lambertian

M*(d+a)+s
Classic Phong Lighting


K- Checkerboard, using step(fract(position))

K*M*(d+a)+s
Checkerboard controls overall diffuse color.

M*(d+a)+K*s
Checkerboard controls shininess.

M*(d+a)+s+0.5*K
Gently glowing checkerboard.

Try these out with the 481_lighting example program on the main page!