Computing normals to achieve flat and smooth shading

This article demonstrates in practice how to compute face and vertex normals to achieve
flat and smooth shading using OpenGL.

Definitions

In geometry, a vector is an object which defines a direction and a
norm. It is usually symbolized by an arrow pointing in the vector direction ; the length
of the arrow giving the vector norm. When this norm is equal to 1 unit, the vector is normalized.
A vector is normal to a surface when its direction is perpendicular to
the plane which contains this surface:

In 3D computing, the normal vector of a surface defines the orientation of this surface
in space. In particular, its orientation relative to light sources. As a result, OpenGL
uses this vector to determine how much light each point of a given surface receives.
Failing to give this vector alongside the surface definition itself make OpenGL renders a
uniformly lighten object (no shading). When one normal is given alongside the surface,
OpenGL enlighten all the surface points with the same color value. The result is much more
realistic than without any shading and is known as flat shading
rendering. To realize an even more aesthetic enlightenment, you must give to OpenGL, one
normal vector per surface vertex. The result is good looking and is known as smooth
shading
rendering. Look at the figure below how each of these methods renders:

The sample project

You can download below the sample MFC project we used to illustrate this article. It
has been created with Visual C++ 5 and tested on Windows NT4. This sample take a static object
definition to draw through OpenGL the twisted torus you can see in the figure above. This
object is defined by an array of triangular faces, each of them pointing in turn into an
array of vertices. Hereafter is an excerpt of these definitions:


// Static definition of the object's vertices
struct GLpoint {
   GLfloat x, y, z;
} OBJ_VERTICES [] = {

   {(float)54.111641, (float)-0.007899, (float)37.141083},
   {(float)55.552414, (float)-5.571973, (float)41.828125},

   // ... //

   {(float)49.429958, (float)5.559381, (float)35.695301},
   {(float)54.111732, (float)-0.007808, (float)37.141174}
};


// Static definition of the object's faces
struct GLFace {
   unsigned short v1, v2, v3;
} OBJ_FACES[] = {

   {0, 11, 12},
   {0, 12, 1},

   // ... //

   {878, 9, 0},
   {878, 0, 869}
};

Computing face normals

To achieve flat shading in our example, each face normal is computed using the ComputeFaceNormal()
function at the end of the listing below. Using the three vertices of the face, the
function constructs two vectors a and b, which share the middle vextex p2
at their origin. Those vectors are transmitted to the VectorGetNormal()
function which uses the determinant method to cross product the two arguments. The
resulting vector is normal to the surface defined by a and b. The last
step consists to normalize it to avoid additional computation in OpenGL.

// Offset pIn by pOffset into pOut
void VectorOffset (GLpoint *pIn, GLpoint *pOffset, GLpoint *pOut)
{
   pOut->x = pIn->x - pOffset->x;
   pOut->y = pIn->y - pOffset->y;
   pOut->z = pIn->z - pOffset->z;
}

// Compute the cross product a X b into pOut
void VectorGetNormal (GLpoint *a, GLpoint *b, GLpoint *pOut)
{
   pOut->x = a->y * b->z - a->z * b->y;
   pOut->y = a->z * b->x - a->x * b->z;
   pOut->z = a->x * b->y - a->y * b->x;
}

// Normalize pIn vector into pOut
bool VectorNormalize (GLpoint *pIn, GLpoint *pOut)
{
   GLfloat len = (GLfloat)(sqrt(sqr(pIn->x) + sqr(pIn->y) + sqr(pIn->z)));
   if (len)
   {
      pOut->x = pIn->x / len;
      pOut->y = pIn->y / len;
      pOut->z = pIn->z / len;
      return true;
   }
   return false;
}

// Compute p1,p2,p3 face normal into pOut
bool ComputeFaceNormal (GLpoint *p1, GLpoint *p2, GLpoint *p3, GLpoint *pOut)
{
   // Uses p2 as a new origin for p1,p3
   GLpoint a;
   VectorOffset(p3, p2, &a);
   GLpoint b;
   VectorOffset(p1, p2, &b);
   // Compute the cross product a X b to get the face normal
   GLpoint pn;
   VectorGetNormal(&a, &b, &pn);
   // Return a normalized vector
   return VectorNormalize(&pn, pOut);
}

Computing vertex normals

Computing each vertex normal is straightforward when you already have all face normals.
Take a look at the ComputeVerticeNormal() function below:


void GLSObject::ComputeVerticeNormal (int ixVertice)
{
   // Allocate a temporary storage to store adjacent faces indexes
   if (!m_pStorage)
   {
      m_pStorage = new int[m_nbFaces];
      if (!m_pStorage)
         return;
   }

   // Store each face which has an intersection with the ixVertice'th vertex
   int nbAdjFaces = 0;
   GLFace * pFace = (GLFace *)&OBJ_FACES;
   for (int ix = 0; ix < m_nbFaces; ix++, pFace++)
      if (pFace->v1 == ixVertice)
         m_pStorage[nbAdjFaces++] = ix;
      else
         if (pFace->v2 == ixVertice)
            m_pStorage[nbAdjFaces++] = ix;
         else
            if (pFace->v3 == ixVertice)
               m_pStorage[nbAdjFaces++] = ix;

   // Average all adjacent faces normals to get the vertex normal
   GLpoint pn;
   pn.x = pn.y = pn.z = 0;
   for (int jx = 0; jx < nbAdjFaces; jx++)
   {
      int ixFace= m_pStorage[jx];
      pn.x += m_pFaceNormals[ixFace].x;
      pn.y += m_pFaceNormals[ixFace].y;
      pn.z += m_pFaceNormals[ixFace].z;
   }
   pn.x /= nbAdjFaces;
   pn.y /= nbAdjFaces;
   pn.z /= nbAdjFaces;

   // Normalize the vertex normal
   VectorNormalize(&pn, &m_pVertNormals[ixVertice]);
}

To compute the normal at vertex ixVertice, this function search all faces
which share the vertex. The normal of all those adjacent faces are then averaged to get
the vertex normal, as shown on the following picture:

The result is finally normalized into a previously allocated array of vectors; a member
of this.

Exploitation with OpenGL

The current normal vector is set by calling glNormal3*(). OpenGL
assigns this normal to any subsequent vertex definition invoked by glVertex3*().
To achieve flat shading, the principle is to call glNormal3*() before the first
glVextex3*() call for the surface. For smooth shading, each call to glVextex3*() must be
preceded by a corresponding call to glNormal3*():


void GLSObject::Draw (WORD wFlags)
{
   // ... //

   // Draw mesh
   glBegin(GL_TRIANGLES);
   GLFace * pFace = (GLFace *)&OBJ_FACES;
   for (int ix = 0; ix < m_nbFaces; ix++, pFace++)
   {
      if (m_pFaceNormals)
         if (wFlags & DF_FLAT)
            // Flat shading
            glNormal3fv((float *)&m_pFaceNormals[ix]);
         else
            if (m_pVertNormals && (wFlags & DF_SMOOTH))
               // Smooth shading
               glNormal3fv((float *)&m_pVertNormals[pFace->v1]);

      glVertex3fv((float *)&OBJ_VERTICES[pFace->v1]);

      if (m_pVertNormals && (wFlags & DF_SMOOTH))
         // Smooth shading
         glNormal3fv((float *)&m_pVertNormals[pFace->v2]);

      glVertex3fv((float *)&OBJ_VERTICES[pFace->v2]);

      if (m_pVertNormals && (wFlags & DF_SMOOTH))
         // Smooth shading
         glNormal3fv((float *)&m_pVertNormals[pFace->v3]);

      glVertex3fv((float *)&OBJ_VERTICES[pFace->v3]);
   }
   glEnd();


   // ... //
}

Download demo project – 65 KB

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read