Computing normals to achieve flat and smooth shading

WEBINAR: On-demand webcast

How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >

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



Comments

  • Perfectionism.

    Posted by Syhon on 04/19/2010 03:48am

    I'm very late in replying to this, clearly; however, I would like to note simply that "vertex" is singular and "vertices" is plural. The code has "vertice" written throughout and this is entirely incorrect.

    Reply
  • Did you print out vertex normals?

    Posted by Legacy on 08/10/2000 12:00am

    Originally posted by: Jae Hun, Ryu

    This program is working well. When I printed out vertex
    normal, however, some vertex normal has infinite value.
    Would you check the function of calculating vertex normals?
    I also try to debug it.

    FILE *fp=fopen(szFileName,"w");
    fprintf(fp,"Vertex Normals\n");
    for (int i=0 ; i< this->m_nbVertices; i++){
    float x,y,z;
    x=m_pVertNormals[i].x;
    y=m_pVertNormals[i].y;
    z=m_pVertNormals[i].z;
    fprintf(fp, "%f %f %f\n", x,y,z);
    }
    fclose(fp);

    --------- Output--------------------
    0.706771 -0.005534 -0.707421
    0.827826 -0.465777 -0.312659
    0.633619 -0.747251 0.200356
    0.197954 -0.745792 0.636089
    -0.313860 -0.461939 0.829521
    -0.706423 -0.002511 0.707785
    -0.829539 0.459008 0.318082
    -0.635861 0.747466 -0.192290
    -0.198640 0.749445 -0.631565
    0.314909 0.460325 -0.830020
    -431602080.000000 -431602080.000000 -431602080.000000
    0.698595 -0.168453 -0.695405
    ............................

    Reply
  • more cpu but better result

    Posted by Legacy on 11/24/1999 12:00am

    Originally posted by: Fonzy

    I have yet made the same algorithm to calculate smoot but the result is uggly for object that have small faces next to big one.

    Another way is to multiply the adjacent face normal with the inverse of distance from the vertex (to the center of the adjacent face). Then you do a average and you normalize the result.

    ps : i can not give the source because of copyright.

    see samples at http://ferretti.homepage.com (current project)

    Reply
  • About the efficiency of Computing normals

    Posted by Legacy on 06/04/1999 12:00am

    Originally posted by: EnPengXu


    I think the below method can improve some efficiency.

    void GLSObject::ComputeNormal()
    {
    GLFace * pFace = (GLFace *)&OBJ_FACES;
    GLpoint Normal ;
    memset( m_pStorage , 0 , sizeof(int)*m_nbVertex );
    memset( m_pVertNormals , 0 , sizeof( GLpoint ) * m_nbVertex );
    // Normalize the vertex normal
    for( int i = 0 ; i < m_nbFaces ; i ++ )
    {
    int v1 = pFace[i].v1 ;
    int v2 = pFace[i].v2 ;
    int v3 = pFace[i].v3 ;

    ComputeFaceNormal( OBJ_VERTICES + v1 ,
    OBJ_VERTICES + v2 ,
    OBJ_VERTICES + v3 ,
    &Normal );
    // Now Normal has been normalized
    // GLpoint::operator + fuction should be defined
    m_pVertNormals[v1] += Nromal ;
    m_pVertNormals[v2] += Nromal ;
    m_pVertNormals[v3] += Nromal ;
    }
    for( i = 0 ; i < m_nbVertex ; i ++ )
    VectorNormalize( m_pVertNormals+i, m_pVertNormals+i);
    }

    But in most case , you want to get more 'smooth' effection , you should define three normals for each face, that's say , a vertex should has more than one normals that
    rest on the face numbers it connected. when you computing the normals , you should comput the angles between two faces and compare it to a little value to determine whether
    should average the two triangle's normal or not .

    Reply
  • Addendum reply

    Posted by Legacy on 02/11/1999 12:00am

    Originally posted by: Jean-Edouard

    You are right Petr. Maybe I should have emphasized that the main purpose of my article was to get smooth shading with mono chromatic objects (such as plastic or metal ones). Obviously, shading per-vertex colored objects or texture mapped objects may involve different solutions.

    Reply
  • Addendum

    Posted by Legacy on 02/10/1999 12:00am

    Originally posted by: Petr Novotny

    What you say in the article is almost true, but not 100%.

    1. OpenGL has two shading models - GL_FLAT and GL_SMOOTH. If GL_SMOOTH is used and the triangle you draw has different colors in vertices (after lighting calculations), the colors if interior are interpolated. If GL_FLAT is in effect, the color of the first vertex is used for the whole area. It doesn't have much to do with normals.
    2. If you have different normals in vertices and GL_SMOOTH shading, the result looks really nice; OpenGL does normal interpolation across the surface and it looks much better than simple color interpolation.
    3. You still have some shading, even if you have only one normal per face, which comes if you set glLighModeli(GL_LIGHT_MODEL_LOCAL_VIEWER,1). The shading comes from the fact that the observer, if he's local to the scene, sees different points of the surface under different angles and thus the amount of light reflected to him varies.

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • As all sorts of data becomes available for storage, analysis and retrieval - so called 'Big Data' - there are potentially huge benefits, but equally huge challenges...
  • The agile organization needs knowledge to act on, quickly and effectively. Though many organizations are clamouring for "Big Data", not nearly as many know what to do with it...
  • Cloud-based integration solutions can be confusing. Adding to the confusion are the multiple ways IT departments can deliver such integration...

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date