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; |

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?

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!

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:

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.

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.

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.

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 ;-)