3. Normal Transforms Aren’t Normal
Normal vectors can live in these
different spaces too; it turns out that lighting in the vertex shader is
often performed in eye space. (There are certain conditions in which it
can be done in object space.)
So, we need to transform our normals to eye
space. Since vertex positions get transformed by the model-view matrix
to bring them into eye space, it follows that normal vectors get
transformed the same way, right? Wrong! Actually, wrong
sometimes. This is one of the trickier concepts in
graphics to understand, so bear with me.
Look at the heart shape in Figure 3, and consider the surface normal at a
point in the upper-left quadrant (depicted with an arrow). The figure on
the far left is the original shape, and the middle figure shows what
happens after we translate, rotate, and uniformly shrink the heart. The
transformation for the normal vector is almost the same as the model’s
transformation; the only difference is that it’s a vector and therefore
doesn’t require translation. Removing translation from a 4×4
transformation matrix is easy. Simply extract the upper-left 3×3 matrix,
and you’re done.
Now take a look at the figure on the far
right, which shows what happens when stretching the model along only its
x-axis. In this case, if we were to apply the upper 3×3 of the
model-view matrix to the normal vector, we’d get an incorrect result;
the normal would no longer be perpendicular to the surface. This shows
that simply extracting the upper-left 3×3 matrix from the model-view
matrix doesn’t always suffice. I won’t bore you with the math, but it
can be shown that the correct transform for normal vectors is actually
the inverse-transpose of the model-view matrix,
which is the result of two operations: first an inverse, then a
transpose.
The inverse matrix of
M is denoted M-1; it’s the matrix that
results in the identity matrix when multiplied with the original matrix.
Inverse matrices are somewhat nontrivial to compute, so again I’ll
refrain from boring you with the math. The
transpose matrix, on the other hand, is easy to
derive; simply swap the rows and columns of the matrix such that
M[i][j] becomes M[j][i].
Transposes are denoted
MT, so the proper transform for normal
vectors looks like this:
Don’t forget the middle shape in Figure 4-7; it shows that, at least in some
cases, the upper 3×3 of the original model-view matrix
can be used to transform the normal vector. In this
case, the matrix just happens to be equal to its own inverse-transpose;
such matrices are called orthogonal. Rigid body
transformations like rotation and uniform scale always result in
orthogonal matrices.
Why did I bore you with all this mumbo jumbo
about inverses and normal transforms? Two reasons. First, in ES 1.1,
keeping nonuniform scale out of your matrix helps performance because
OpenGL can avoid computing the inverse-transpose of the model-view.
Second, for ES 2.0, you need to understand nitty-gritty details like
this anyway to write sensible lighting shaders!
4. Generating Normals from Parametric Surfaces
Enough academic babble; let’s get back to
coding. Since our goal here is to add lighting to ModelViewer, we need
to implement the generation of normal vectors. Let’s tweak
ISurface in Interfaces.hpp by
adding a flags parameter to GenerateVertices, as
shown in Example 1. New or modified lines are
shown in bold.
Example 1. Modifying ISurface with support for normals
enum VertexFlags { VertexFlagsNormals = 1 << 0, VertexFlagsTexCoords = 1 << 1, };
struct ISurface { virtual int GetVertexCount() const = 0; virtual int GetLineIndexCount() const = 0; virtual int GetTriangleIndexCount() const = 0; virtual void GenerateVertices(vector<float>& vertices, unsigned char flags = 0) const = 0; virtual void GenerateLineIndices(vector<unsigned short>& indices) const = 0; virtual void GenerateTriangleIndices(vector<unsigned short>& indices) const = 0; virtual ~ISurface() {} };
|
The argument we added to
GenerateVertices could have been a boolean instead of
a bit mask, but we’ll eventually want to feed additional vertex
attributes to OpenGL, such as texture coordinates. For now, just ignore
the VertexFlagsTexCoords flag; it’ll come in handy in
the next chapter.
Next we need to open
ParametricSurface.hpp and make the complementary
change to the class declaration of ParametricSurface,
as shown in Example 2. We’ll also
add a new protected method called InvertNormal, which
derived classes can optionally override.
Example 2. ParametricSurface class declaration
class ParametricSurface : public ISurface { public: int GetVertexCount() const; int GetLineIndexCount() const; int GetTriangleIndexCount() const; void GenerateVertices(vector<float>& vertices, unsigned char flags) const; void GenerateLineIndices(vector<unsigned short>& indices) const; void GenerateTriangleIndices(vector<unsigned short>& indices) const; protected: void SetInterval(const ParametricInterval& interval); virtual vec3 Evaluate(const vec2& domain) const = 0; virtual bool InvertNormal(const vec2& domain) const { return false; } private: vec2 ComputeDomain(float i, float j) const; vec2 m_upperBound; ivec2 m_slices; ivec2 m_divisions; };
|
Next let’s open
ParametericSurface.cpp and replace the
implementation of GenerateVertices, as shown in Example 3.
Example 3. Adding normals to ParametricSurface::GenerateVertices
void ParametricSurface::GenerateVertices(vector<float>& vertices, unsigned char flags) const { int floatsPerVertex = 3; if (flags & VertexFlagsNormals) floatsPerVertex += 3;
vertices.resize(GetVertexCount() * floatsPerVertex); float* attribute = (float*) &vertices[0];
for (int j = 0; j < m_divisions.y; j++) { for (int i = 0; i < m_divisions.x; i++) {
// Compute Position vec2 domain = ComputeDomain(i, j); vec3 range = Evaluate(domain); attribute = range.Write(attribute);
// Compute Normal if (flags & VertexFlagsNormals) { float s = i, t = j;
// Nudge the point if the normal is indeterminate. if (i == 0) s += 0.01f; if (i == m_divisions.x - 1) s -= 0.01f; if (j == 0) t += 0.01f; if (j == m_divisions.y - 1) t -= 0.01f; // Compute the tangents and their cross product. vec3 p = Evaluate(ComputeDomain(s, t)); vec3 u = Evaluate(ComputeDomain(s + 0.01f, t)) - p; vec3 v = Evaluate(ComputeDomain(s, t + 0.01f)) - p; vec3 normal = u.Cross(v).Normalized(); if (InvertNormal(domain)) normal = -normal; attribute = normal.Write(attribute); } } } }
|
This completes the changes to
ParametricSurface. You should be able to build
ModelViewer at this point, but it
will look the same since we have yet to put the normal vectors to good
use. That comes next.