Vertex Normals

How to 3D

Chapter 6: Lighting

Vertex Normals

This renderer is misleading because it shows a single face normal on the triangle:

There is no way in WebGL to associate data with an entire triangle. We must define normals as vertex attributes, just as we define positions and colors:

attributes.addAttribute('position', nvertices, 3, positions);
attributes.addAttribute('color', nvertices, 3, colors);
attributes.addAttribute('normal', nvertices, 3, normals);
attributes.addAttribute('position', nvertices, 3, positions);
attributes.addAttribute('color', nvertices, 3, colors);
attributes.addAttribute('normal', nvertices, 3, normals);

In the renderer above, the face normal is duplicated at all three vertices. How should vertex normals be defined when there's more than one triangle stitched together? We have a couple of options.

Discrete Faces

The simplest approach is to disconnect the faces from each other. Wherever two faces share a vertex, a clone is added to the vertex positions buffer. Each face gets its own copy. Then the face normals are written to the vertex normals buffer.

This renderer defaults to rendering its cube with discrete faces:

At first glance, the cube appears to only have 8 vertices. In truth, it has 24. Each position appears in the buffer three times for the three faces and three normals with which it's associated.

Shared Vertices

The other alternative is to keep the faces connected and have them share vertices and normals. Toggle the checkbox in the cube renderer above to see this alternative. It doesn't look good. Sharing doesn't make sense when faces form sharp bends, as they do on a cube, but it does make sense for smooth shapes, like the sphere we saw earlier:

With sharing enabled, the sharp bends are blurred. We can significantly reduce the number of triangles needed in a scene and still get a smooth rendering.

What should the normal of a shared vertex be? The vertex normal is computed as the average of the adjoining faces' normals. To compute the average, we sum up the face normals, which we can do as we process each triangle:

for each triangle
  # compute faceNormal as described earlier

  for each vertex in triangle
    add faceNormal to vertex's normal
for each triangle
  # compute faceNormal as described earlier

  for each vertex in triangle
    add faceNormal to vertex's normal

The vertex normals must be initialized to zero vectors before the face normals start accumulating. And they need to averaged after they are accumulated. Usually averaging is done by dividing the sum by the size of a population. In the case of normals, you can just normalize them. That leads to this complete algorithm for computing a mesh's normals:

for each vertex
  initialize vertex's normal to zero-vector

for each triangle
  # Look up vertex positions.
  positionA = triangle's first position
  positionB = triangle's second position
  positionC = triangle's third position

  # Find two edge vectors.
  vectorAB = positionB - positionA
  vectorAC = positionC - positionA

  faceNormal = cross vectorAB and vectorAC
  normalize faceNormal

  for each vertex in triangle
    add faceNormal to vertex's normal

for each vertex
  normalize vertexNormal
for each vertex
  initialize vertex's normal to zero-vector

for each triangle
  # Look up vertex positions.
  positionA = triangle's first position
  positionB = triangle's second position
  positionC = triangle's third position

  # Find two edge vectors.
  vectorAB = positionB - positionA
  vectorAC = positionC - positionA

  faceNormal = cross vectorAB and vectorAC
  normalize faceNormal

  for each vertex in triangle
    add faceNormal to vertex's normal

for each vertex
  normalize vertexNormal

This algorithm deserves its own method in your Trimesh class. See the algorithm at work in this renderer:

The blue normals are the face normals. They are averaged together to form the orange vertex normals. Observe how these normals change as you drag the vertex positions around.

← Face NormalsInterpolating Normals →