Per-Vertex Normals

In which you fit the concept of a surface normal to a system of vertex attributes.

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

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

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

In the renderer above, the same normal is provided at all three vertices.

The normals arrive in the vertex shader. In the early days of OpenGL, shading was performed in the vertex shader, and the resulting colors were interpolated across the triangle. Vertex shading is fast but disappointing.

Instead of letting the shaded color interpolate across a triangle, you want the normal to be interpolated. The vertex shader sends the normal along to the fragment shader through an out variable, just as you have done with per-vertex colors:

in vec3 position;
in vec3 color;
in vec3 normal;

out vec3 mixColor;
out vec3 mixNormal;

void main() {
  gl_Position = vec4(position, 1.0);
  mixColor = color;
  mixNormal = normal; 
}

Consider this side profile of normals being interpolated between two vertices:

An issue arises with interpolated normals: they lose their unit length. You can see in the figure that the blended normals are shorter than the vertex normals. They should look like this:

In the fragment shader, you must renormalize the normal, which you can do with the builtin normalize function:

in vec3 mixNormal;

void main() {
  vec3 normal = normalize(mixNormal); 
  // ...
}

If a vertex is shared between two faces, then so is its normal. This can lead to unrealistic lighting. Consider this cube, each vertex of which is shared by its three adjoining faces:

What should the normal of a shared vertex be? In this case, the vertex normal is computed as the average of the adjoining faces' normals. The averaged normals hide the cube's sharp edges. When you uncheck the checkbox, each vertex is replaced with three separate vertices, which have the same positions but different normals. Separated normals better capture the orientation of the cube's faces.