Before we can enable lighting, there’s yet
another prerequisite we need to get out of the way. To perform the math
for lighting, OpenGL must be provided with a surface
normal at every vertex. A surface normal (often simply called a
normal) is simply a vector perpendicular to the
surface; it effectively defines the orientation of a small piece of the
surface.1. Feeding OpenGL with Normals
You might recall that normals are one of the
predefined vertex attributes in OpenGL ES 1.1. They can be enabled like
this:
// OpenGL ES 1.1
glEnableClientState(GL_NORMAL_ARRAY);
glNormalPointer(GL_FLOAT, stride, offset);
glEnable(GL_NORMALIZE);
// OpenGL ES 2.0
glEnableVertexAttribArray(myNormalSlot);
glVertexAttribPointer(myNormalSlot, 3, GL_FLOAT, normalize, stride, offset);
I snuck in something new in the previous
snippet: the GL_NORMALIZE state in ES 1.1 and the
normalize argument in ES 2.0. Both are used to
control whether OpenGL processes your normal vectors to make them unit
length. If you already know that your normals are unit length, do not
turn this feature on; it incurs a performance hit.
Warning:
Don’t confuse
normalize, which refers to making any vector into
a unit vector, and normal vector, which refers to
any vector that is perpendicular to a surface. It is not redundant to
say “normalized normal.”
Even though OpenGL ES 1.1 can perform much of
the lighting math on your behalf, it does not compute surface normals
for you. At first this may seem rather ungracious on OpenGL’s part, but
as you’ll see later, stipulating the normals yourself give you the power
to render interesting effects. While the mathematical notion of a normal
is well-defined, the OpenGL notion of a normal is simply another input
with discretionary values, much like color and position. Mathematicians
live in an ideal world of smooth surfaces, but graphics programmers live
in a world of triangles. If you were to make the normals in every
triangle point in the exact direction that the triangle is facing, your
model would looked faceted and artificial; every triangle would have a
uniform color. By supplying normals yourself, you can make your model
seem smooth, faceted, or even bumpy, as we’ll see later.
2. The Math Behind Normals
We scoff at mathematicians for living in an
artificially ideal world, but we can’t dismiss the math behind normals;
we need it to come up with sensible values in the first place. Central
to the mathematical notion of a normal is the concept of a
tangent plane, depicted in Figure 1.
The diagram in Figure 1 is, in itself, perhaps the best definition
of the tangent plane that I can give you without going into calculus.
It’s the plane that “just touches” your surface at a given point
P. Think like a mathematician: for
them, a plane is minimally defined with three points. So, imagine three
points at random positions on your surface, and then create a plane that
contains them all. Slowly move the three points toward each other; just
before the three points converge, the plane they define is the tangent
plane.
The tangent plane can also be defined with
tangent and binormal vectors (u and
v in Figure 1), which are easiest to define within the
context of a parametric surface. Each of these correspond to a dimension
of the domain; we’ll make use of this when we add normals to our
ParametricSurface class.
Finding two vectors in the tangent plane is
usually fairly easy. For example, you can take any two sides of a
triangle; the two vectors need not be at right angles to each other.
Simply take their cross product and unitize the result. For parametric
surfaces, the procedure can be summarized with the following
pseudocode:
p = Evaluate(s, t)
u = Evaluate(s + ds, t) - p
v = Evaluate(s, t + dt) - p
N = Normalize(u × v)
Don’t be frightened by the cross product;
I’ll give you a brief refresher. The cross product always generates a
vector perpendicular to its two input vectors. You can visualize the
cross product of A with B using your right hand. Point your index finger
in the direction of A, and then point
your middle finger toward B; your thumb
now points in the direction of A×B (pronounced
“A cross B,” not “A times B”). See Figure 2.
Here’s the relevant snippet from our C++
library (see the appendix for a full listing):
template <typename T>
struct Vector3 {
// ...
Vector3 Cross(const Vector3& v) const
{
return Vector3(y * v.z - z * v.y,
z * v.x - x * v.z,
x * v.y - y * v.x);
}
// ...
T x, y, z;
};