Lighting-- Specular Lighting

CS 381 Lecture, Dr. Lawlor
First, a little aside.  All of the OpenGL vertex description routines actually correspond to builtin values in the GLSL vertex program.
C++ OpenGL Call
GLSL Vertex Program Value
glNormal3f(x,y,z); vec3 n=gl_Normal;
glColor4f(r,g,b,a); vec4 c=gl_Color;
glVertex4f(x,y,z,w); vec4 v=gl_Vertex;
It's important to realize that the call to "glVertex" is what really draws the vertex, passing in the current color and normal.  So you always want to call glColor and glNormal *before* calling glVertex, or your color and normal will actually apply to the *next* glVertex, resulting in weird and horrible artifacts.

So back to generic graphics.  "Specular" reflections are caused by light bouncing directly off a surface--for example, a mirror is a perfect specular reflector.  The question is, how can we draw specular reflections on our simulated objects?

The Right Way to do Specular Reflections

So we know what the surface normal N is.  We know the direction to the light source, L.  We just need to compute the direction that light leaves the surface--R.

A little bit of staring at the figure below shows we just need to find the distance marked "e" on the right.  Then given e, we can start at negative L, and move up by twice e in the N direction to arrive at R.  Now notice that e is just the cosine of the angle between N and L--and because we can compute cosine via dot product for unit vectors, e = dot(N,L) (assuming N and L are unit-length vec3's, which they should be!).  So overall we can compute the reflected vector with:

    R = -L + 2*dot(N,L)*N;

which you can write directly in GLSL.  Be sure N and L are unit vec3's!

Reflected vector computation

Given the reflected vector, we know where light is reflecting from the surface.  If this light hits our camera, we draw a highlight--a specular glint or gleam.  If the reflected light misses our camera, we don't draw the highlight.  Sadly, because both our light source and our camera are single points, this approach will almost never result in a highlight!

Of course, in reality, the light source isn't a single point (so there are really a whole smear of incoming light directions around L), and the surface isn't perfectly smooth (so there really is a whole bundle of surface normals around N).  So really there isn't a single reflected light vector R, but a whole wide assortment of different reflected directions, more like this figure:

Actual distribution of light around center vectors

Properly accounting for all these imperfections isn't easy, but there is an easy and widespread trick that Phong Bui-Tuong came up with back in the 1970's.  Given a unit vector C pointing from the surface to the camera, the "Phong Highlight" is just:

    color = pow(clamp(dot(C,R),0.0,1.0), n);

where n is the "specular exponent", which is typically around a hundred or so.  The rationale is that if C and R are pointing in the same direction (angle==0, so cos angle==dot(C,R)==1.0), then raising it to a high power leaves the color at a white 1.0.  If C and R aren't very similar, like dot(C,R)==0.5, raising 0.5 to a high power results in a small value close to zero or black.  A small exponent thus results in a big specular highlight, and a large exponent results in a small highlight.

Try out the "specExp" field in the glsl_lighting demo (Zip, Tar-gzip) to see how the specular exponent field works for yourself.  The code for reflected light source specularity is in glsl_lighting/programs/reflect_light.

You can actually also compute the reflection Rc of the *camera* about the normal, and compare it to the light vector, like this:
    vec3 Rc = -C + 2*dot(C,N)*N;
    color = pow(clamp(dot(L,Rc),0.0,1.0), n);
This seems to be entirely equivalent to the "reflected light" version above.  See the code in glsl_lighting/programs/reflect_camera.

The Fast Way

A really smart guy by the name of Jim Blinncame up with a cool trick to speed up specular highlight computation.  Notice that if the vector C (pointing toward the camera) equals R, then the normal vector has to be exactly midway between the camera and the light source.  So really, we don't even need to compute R; we can just check if N is halfway between C and L.  Or in the Phong style, we take the dot product of N and the "Blinn Halfway Vector" and raise it to a high power:
    H=normalize(L + C); /* "Blinn Halfway Vector": sits halfway between the light and camera direction */
    color = pow(clamp(dot(N,H),0.0,1.0), n);

This is great because H doesn't depend on N, so we can actually compute the halfway vector in a vertex program and interpolate it to the fragments.

See glsl_lighting/programs/reflect_blinn for the GLSL code for Blinn lighting.

The Ugly Way(s)

There are a bunch of ways to screw up lighting computations.  One classic problem, which was actually a bug in OpenGL 1.0, was to *multiply* the specular highlight by the object color, instead of *adding* the specular highlight to the object color.  This results in a weird-looking highlight, and makes it impossible to have a glinty black object.  See glsl_lighting/programs/separate_specular for the GLSL code.

Why separate specular reflection is a good thing

Caveat: there are times when you want to multiply down the specular highlight.  For example, you might just multiply away the specularity on the seams of a car model to separate the body panels instead of actually using separate polygons.

Another really common error is to do the whole specular reflection computation in the *vertex* shader instead of the *fragment* shader.  Fixed-function OpenGL does this by default, which is atrocious.  This horror even has a name: "Gauroud Shading" ("Gauroud" is pronounced "goro"--it's French).  Don't do this.  Please.  It looks reasonably OK for diffuse lighting, but totally horribly wrong for specular highlights--you can totally see the polygon shape in the highlight.  See glsl_lighting/programs/gauroud for the GLSL code.  Even Phong, back in the 1970's, interpolated normals across his polygons before doing his Phong specular highlight, rather than smearing the highlight across an entire polygon.

Ugly specular highlights with gauroud shading

Caveat: there are times when it make sense to compute lighting in the vertex shader.  For example, it'd be a waste to spend a bunch of time computing a smooth ambient term at every fragment, when the same thing would look identical computed at the vertices.  In a survival-type situation, where the hardware's incredibly slow or the model's incredibly tesselated, you might get a jury to aquit you of using vertex-based specular lighting, but it'd be a close thing   ;-)