Computing Vertex Normals by Weighting

This next section discusses two additional techniques that may be used in computing vertex normals. The first technique weights the normals by the vertex angle on each face. The second weights the normal using the area of each face.

Weighting by Face Angle

To understand why using a weighted normal approach might be used consider the following example:

Create a default Box and convert it to an Editable Mesh. Go into SubObject mode, and find vertex 1 (generally found at the front lower left corner). Display the edges, and go into object properties to unclick "edges only" (so you see the hidden edges). You'll see that vertex 1 is used by bottom faces 1 and 2. (In the SDK, these are vertex 0 and faces 0 and 1.) its also used by front faces 5 and 6, and left face 11. (4,5,10.)

If we simply averaged the normals of all incident faces, we'd get:

Normalize (2*(0,0,-1) + 2*(0,-1,0) + (-1,0,0)), which is (1/3, 2/3, 2/3). 

Because this arrangement of diagonals is haphazard and (inherently) asymmetric, we'd get vertex normals on a box pointing to odd uncoordinated directions.

If we instead weight the normals by the vertex angle on each face, we get:

Normalize (PI/2*(0,0,-1) + PI/2*(0,-1,0) + PI/2*(-1,0,0)) = (1,1,1)/Sqrt(3) 

This is more natural for the user. Each vertex normal points away from all three sides symmetrically. (The individual front and bottom triangles may have varying angles, but the pairs of them always add up to PI/2.)

This seems like the right approach in general -- when you divide a face, for example, you don't wind up changing the vertex normal when the surface hasn't essentially changed. If you change vertex angles by dragging neighboring verticies, the normal changes in a natural fashion.

You can compute the vertex angle by using dot products as shown below:

// Corner is 0, 1, or 2 -- which corner do we want the angle of?
float FindVertexAngle(Mesh *mesh, int face, int corner) {
   int cnext = (corner+1)%3;
   int cprev = (corner+2)%3;
   DWORD *vv = mesh->faces[face];
 
   // Get edge vectors:
   Point3 A = mesh->verts[vv[cnext]] - mesh->verts[vv[corner]];
   Point3 B = mesh->verts[vv[corner]] - mesh->verts[vv[cprev]];
 
   // Normalize the edge-vectors, but return 0 if either has 0 length.
   float len = Length(A);
   if (!len) return len;
   A = A/len;
   len = Length(B);
   if (!len) return len;
   B = B/len;
 
   // The dot product gives the cosine of the angle:
   float dp = DotProd (A,B);
   if (dp>1) dp=1.0f; // shouldn't happen, but might
   if (dp<-1) dp=-1.0f; // shouldn't happen, but might
   return acos(dp);
}

To be efficient when computing all normals, you may want to cache the normalized edge directions (A & B). You can index these by an adjacent edge list for the mesh, where edir[i] is the unit vector pointing from vertex ae->edges[i].v[0] to ae->edges[i].v[1] (AdjEdgeList *ae).

Weighting by Face Area

Another possibility is to weight the normals by the area of each face. The area of a face is very easy to compute, its just half the length of the normal cross product:

void GetAreaAndNormal (Mesh *mesh, int face, Point3 & N, float area)
{
   DWORD *vv = mesh->faces[face].v;
   Point3 A = mesh->verts[vv[1]] - mesh->verts[vv[0]];
   Point3 B = mesh->verts[vv[2]] - mesh->verts[vv[0]];
   N = A^B;
   area = Length (N) / 2.0f;
   Normalize (N);
}

This works using any two edges for A and B.

To weight face normals by area, you can just use the N=A^B vector directly.

Weighting by area gives an interesting result, but perhaps not as satisfactory as weighting by face angle.