iPhone 3D Programming : Adding Depth and Realism - Surface Normals (part 2)

1/7/2011 9:12:06 AM

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.

Figure 3. Normal transforms

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 {
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;
void SetInterval(const ParametricInterval& interval);
virtual vec3 Evaluate(const vec2& domain) const = 0;
virtual bool InvertNormal(const vec2& domain) const { return false; }
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.

Video tutorials
- How To Install Windows 8

- How To Install Windows Server 2012

- How To Install Windows Server 2012 On VirtualBox

- How To Disable Windows 8 Metro UI

- How To Install Windows Store Apps From Windows 8 Classic Desktop

- How To Disable Windows Update in Windows 8

- How To Disable Windows 8 Metro UI

- How To Add Widgets To Windows 8 Lock Screen
programming4us programming4us