Sunday, December 14, 2008

Surface Normals in OpenGL

Before we can learn to load objects in from a 3D program, we need to talk about another feature of OpenGL - Surface Normals (usually referred to just as "normals"). A surface normal is nothing more than a vector (or line) that is perpendicular to the surface of a given polygon. Here's a nice illustration of the concept (this one is from Wikipedia, not from me:)

OpenGL doesn't need to know the normals to render a shape, but it does need them when you start using directional lighting. OpenGL needs the surface normals to know how the light interacts with the individual polygons.

Normals can be calculated, but OpenGL ES won't do it for you. To find the normal for a triangle, simply calculate the vector cross product of the three vertices. That's easy enough, right? Well, maybe it's been a while since you took Geometry, so let's dive in a little deeper here (as much for my benefit as for yours - my last Geometry class was over fifteen years ago).

Before we look at the code for calculating the surface normal, let's do a little setup. So far, throughout our code, we've just been dealing with vertex arrays where the first element is the X position, the second is the Y position, and the third is the Z position, then it starts over with the fourth being the X position for the next vertex, etc.. For clarity in our code sample, however, let's define a struct to hold a single vertex:
typedef struct {
GLfloat x;
GLfloat y;
GLfloat z;
} Vertex3D, Vector3D;
That shouldn't need much explanation, right? Three floating point values to represent a point in space. Notice, however, that we've actually defined two types based on this struct,: Vertex3D and Vector3D. Computationally speaking, vectors and vertices are the same - they are represented by a single point in three-dimensional space. Conceptually, they are different, however. A vertex represents a single point in space, while a vector represents both a direction and a distance (usually called "magnitude").

This is one of those little things that people who work with 3D graphics and Euclidean geometry take for granted but rarely bother to explain to newcomers. How can a single point in space represent a distance? It takes two points to make a line, right? Everybody knows that.

The answer is simple - the other point is assumed to be the origin. A vector is a line segment drawn from the origin to the point in space represented by the data structure. As a practical matter, the distinction between a vector and a vertex is often academic, since the data structures are the same. Normals are vectors, not vertices, so we've defined a separate type for each just to be semantically accurate.

Anyway, back from that little tangent, to make the code readable, let's also define a struct to hold a single triangle:
typedef struct {
Vertex3D v1;
Vertex3D v2;
Vertex3D v3;
} Triangle3D;
So, given a triangle, here is how we calculate the surface normal:
Vector3D calculateTriangleSurfaceNormal(Triangle3D triangle)
Vector3D surfaceNormal;
surfaceNormal.x = (triangle.v1.y * triangle.v2.z) - (triangle.v1.z * triangle.v2.y);
surfaceNormal.y = -((triangle.v2.z * triangle.v1.x) - (triangle.v2.x * triangle.v1.z));
surfaceNormal.z = (triangle.v1.x * triangle.v2.y) - (triangle.v1.y * triangle.v2.x);
return surfaceNormal;
Surface normals, however, are usually "normalized" so that they have a length of one. This makes calculations faster. So, to normalize a normal (say that ten times fast standing on your head), we have to first figure out the magnitude (length) of the surface normal:
GLfloat vectorMagnitude(Vector3D vector)
return sqrt((vector.x * vector.x) + (vector.y * vector.y) + (vector.z * vector.z));
Eww... now you're starting to see why OpenGL doesn't do this for us at runtime - performance. Square root is a costly calculation, and it would have to do it for every polygon. But we're still not done. Nope, once we have the length of the vector, then we can normalize it like so:
void normalizeVector(Vector3D *vector)
GLfloat vecMag = vectorMagnitude(*vector);
vector->x /= vecMag;
vector->y /= vecMag;
vector->z /= vecMag;
Note: Things can get confusing with the terminology here. "Normalizing" a vector has nothing to do with a surface "normal". A surface normal is called a "normal" based on the use of the word "normal" as a synonym for "perpendicular". On the other hand, when you're "normalizing" a vector, you're reducing that vector to a standard (or "normal") magnitude. So, normalizing a normal is cool - a normal doesn't have to be normalized, but it often is, and with OpenGL, they should be to avoid causing the shader additional work.

Only, if you look at the illustration above, there are actually two normals, pointing in opposite directions - triangles have two sides, and therefore two potential normals. The polygons in most 3D computer objects only have one normal, and it's pointing outwards from the object. There's no point in defining the normal for the side that light will never hit because it's inside an object (the side without a surface normal is often called a "backface"). OpenGL would have no way of knowing which normal to use if it tried to calculate them for you, so it would have to use both - another performance hit, since it would be doing calculations on polygons that could never be seen.

The two surface normals for a triangle (or any polygon) point in completely opposite directions - 180° - so it's easy to "flip a normal" so that it's representing the other side of the triangle:
void flipVector(Vector3D *vector)
vector->x = -vector->x;
vector->y = -vector->y;
vector->z = -vector->z;
Now that I've shown you how to calculate, normalize, and flip vectors, forget all about it. In general, you won't need to worry about calculating surface normals because 3D computer modeling programs like Maya, 3D Max, LightWave, and Blender will calculate those normals for your objects, and you can export your 3D data so that you have the pre-calculated, normalized normals available as part of your data model (something we'll look at before too long)

Here is the basic process for using normals in OpenGL. To tell OpenGL that you are going to provide it with normals, you have to call
This is usually done once during startup, but could also be turned on and off during run time if you only had normals for some objects.

Normal data, like pretty much everything else we've worked in with OpenGL ES 1.1, have to be packed into an array (a "normal array"). Before calling glDrawElements(), you have to feed OpenGL that array using the glNormalPointer() function, like:
glNormalPointer(GL_FLOAT, 0, myNormalArray);
The first parameter tells OpenGL that our array is an array of GLfloats. The second... just ignore the second for now - it's that same one I keep telling you to ignore, the one that allows you to skip data in the array. The third and final argument is a pointer to the actual normal data.

That's all there is to it. You won't notice any difference in the drawing of the object with or without surface normals until you start adding some directional lighting (which we'll do in a future blog posting), but I wanted you to have a good grasp on what normals were and why we need them before we get into trying to load a 3D model.

One last note - if you're wondering why we're working so much with C structs and C functions rather than declaring Objective-C classes, the answer once again is performance. There is a bit of additional overhead associated with instantiating Objective-C objects and with dynamic messaging. That overhead is trivial in most applications, but a 3D model will usually be made up of thousands of triangles. You do NOT want the additional instantiation or dispatch overhead in this situation, believe me, especially not on a small device like the iPhone. It might make sense to declare a class to hold a 3D model, but not to hold the individual polygons or vertices. Too much cost, too little benefit.


mpatric said...

Hi Jeff, thanks for the excellent posts - especially the opengl ones and the work you have done on porting NeHe's tutorials. Great work!

I've looked at the surface normal calculation above and believe the calculation (before normalisation) should be:

surfaceNormal.x = (((triangle.v3.y - triangle.v2.y) * (triangle.v2.z - triangle.v1.z)) - ((triangle.v3.z - triangle.v2.z) - (triangle.v2.y - triangle.v1.y)));
surfaceNormal.y = (((triangle.v3.z - triangle.v2.z) * (triangle.v2.x - triangle.v1.x)) - ((triangle.v3.x - triangle.v2.x) - (triangle.v2.z - triangle.v1.z)));
surfaceNormal.z = (((triangle.v3.x - triangle.v2.x) * (triangle.v2.y - triangle.v1.y)) - ((triangle.v3.y - triangle.v2.y) - (triangle.v2.x - triangle.v1.x)));

- Michael.

Mark Andrews said...

Hey Jeff,

First off, I love the iPhone Development Book. Great job there. I've been reading these post and trying to put together a loader for obj files. Is this last post true, what it is saying about the surface normals calculations? Just currious if you had any thoughts on this. I know it was awhile ago, sorry. Thanks in advance,


Geek PC Service said...

Thank you so much for this tutorial. I just started learning OpenGL|ES. I too, little by little porting NeHe's Tut to the iPhone (lost quite a few hair in the process, but it was a great learning experience!), your tutorial really helps, thanks a million.

Zavie said...


Just an obvious cheap hint: since division is usually way more expensive than multiplication (I don't know if the iPhone processor is optimized in some way that would make this rule false, but it is usually true on most platforms), the following snippet has good chances to result significantly faster:

void normalizeVector(Vector3D *vector)
GLfloat invVecMag = 1.f / vectorMagnitude(*vector);
vector->x *= invVecMag;
vector->y *= invVecMag;
vector->z *= invVecMag;

This may do a difference since you usually have various normals to compute.

Also note that there is a so called fast inverse square root trick, which is very fast and pretty accurate.

Zavie said...

Ah but you mentionned it already in a more recent post... :-)

DreamerDomain said...
This comment has been removed by the author.
DreamerDomain said...

Can't we set glEnable(GL_NORMALIZE) to avoid square root?

ricardo said...

Jeff on the day you decide to make this tutorials of opengl and all the other ones a book Post a big sign with a buy now, i will buy it straight away even if the whole content is in the website, ur tutorials saved my skin a few times now.
Btw ur iphone book rocks:) bought that one as well:)

A big thanks for all ur greate work, dont forget the buy now sign for ur next book, not just speaking for me but for others that follow ur blog, I do believe that allot of us that follow your blog wouldn't not hesitate in buying ur book.

Regards Ricardo

Pepe said...

When I first read mpatric it seems to be that he was wrong, but he is right because the cross product must be done using two vectors of the triangle and not the vertex.
For instance, if we have a triangle with vertex A,B,C, the vectors may be AB and AC, and a vector is the substraction of each coordinate:
AB = (Bx-Ax, By-Ay, Bz-Az)
AC = (Cx-Ax, Cy-Ay, Cz-Az)

When you do the cross product, the result will be:
ABxAC = (ABy*ACz-ABz*ACy, ABz*ACx-ABx*ACz, ABx*ACy-ABy*ACx)

If you substitute ABx with Bx-Ax, ABy with By-Ay and so on, the formula will be as mpatric said.

One last thing, ABxAC is different from ACxAB:
ACxAB = (ACy*ABz-ACz*ABy, ACz*ABx-ACx*ABz, ACx*ABy-ACy*ABx)

This difference is because the sense of the normal is changed thus ABxAC = -ACxAB

Hope this helps

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels