Vertex Normals

How to 3D

Chapter 3: Meshes

Vertex Normals

This renderer shows a single triangle and its face normal:

However, this renderer is misleading. There is no way in WebGL to associate a solitary face normal with an entire triangle. What's really happening is that the face normal is duplicated at all three vertices.

Normals must be defined as vertex attributes, just as we define positions and colors:

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

How should vertex normals be assigned to the vertices when there's more than one triangle? We have a couple of options.

Disconnected Faces

The simplest approach is to disconnect the faces from each other by duplicating any shared vertices. This renderer defaults to rendering its cube with disconnected 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 allow them to share vertices and normals. Toggle the checkbox in the cube renderer above to see this alternative. It doesn't look good. The vertex normals are averages of their adjacent face normals, pointing off in diagonal directions that don't correspond to any face. Sharing vertices doesn't make sense when faces form sharp bends like they do on a cube.

Sharing vertices does make sense for smooth shapes, like this sphere:

With sharing enabled, the sharp bends are smoothed out. Sharing vertices means we can significantly reduce the number of triangles and still get a pleasant rendering.

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

export class Trimesh {
  computeNormals() {
    const normals = this.positions.map(_ => new Vector3(0, 0, 0));

    for (let face of this.faces) {
      // ...compute face normal...

      normals[face[0]] = normals[face[0]].add(faceNormal);
      normals[face[1]] = normals[face[1]].add(faceNormal);
      normals[face[2]] = normals[face[2]].add(faceNormal);
    }
    
    this.normals = normals.map(normal => normal.normalize());
  }
}
export class Trimesh {
  computeNormals() {
    const normals = this.positions.map(_ => new Vector3(0, 0, 0));

    for (let face of this.faces) {
      // ...compute face normal...

      normals[face[0]] = normals[face[0]].add(faceNormal);
      normals[face[1]] = normals[face[1]].add(faceNormal);
      normals[face[2]] = normals[face[2]].add(faceNormal);
    }
    
    this.normals = normals.map(normal => normal.normalize());
  }
}

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 the population. In the case of normals, we can just normalize them.

Develop a feeling for how the algorithm works 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.

Now we can solve the problem of flat shading of our 3D models. Complete these steps to shade a model by its shape:

Call computeNormals on the Trimesh.
Add the normals as a vertex attribute.
In the vertex shader, add an in variable named normal and an out variable named mixNormal. Assign normal to mixNormal.
In the fragment shader, add an in variable named mixNormal. Assign the normal to the color. Because of the interpolation, the normal is no longer normalized. Write normalize(mixNormal) to renormalize.

Rerender the grid, cylinder, cone, and sphere and enjoy their false but illustrative coloring.

← Face NormalsReading OBJ →