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