Core Idea
A normal is a unit vector standing perpendicular on a surface — the surface’s answer to “which way do I face?” It is not computed at render time from the shape; it’s data stored on the mesh, as much a part of a vertex as its position. That’s why the toggle above works: the geometry never changes — the same triangles, the same silhouette — yet the sphere reads as faceted or curved. Shading is a normal story, not a polygon story.
Face Normals, Vertex Normals
A triangle’s own normal falls out of the cross product of two edges:
n = normalize(cross(v1 − v0, v2 − v0))
Edge order matters — swap them and the normal points into the object instead of out (the winding-order story lives on the cross product page). That gives face normals: one per triangle, flat shading. In the demo’s flat mode, look at any corner — a fan of gold quills, because every triangle meeting there insists on its own direction.
A vertex normal averages that fan into one vector per corner. Switch to smooth mode and watch the fans collapse: the rasterizer then interpolates the corner normals across each triangle and lights every pixel with the blended direction — a flat triangle pretending, pixel by pixel, to be curved. Smooth shading is a graceful lie told entirely by normals; the silhouette stays polygonal because the geometry really is.
Scaling Breaks Them
Positions ride the model matrix; normals must not. Under non-uniform scale the surface flattens one way, so its tangent tilts one way — and the perpendicular must tilt the other way. A normal pushed through M like a position gets dragged along with the tangent instead:
The antidote is the normal matrix — the inverse-transpose, (M⁻¹)ᵀ — which undoes the stretch along the normal’s axis and stays perpendicular no matter what the handles do. Every real renderer ships it:
vNormal = normalize(normalMatrix * normal); // built-in in WebGL/Three.js shaders
Two things to notice in the demo: with uniform scale the wrong answer coincides with the right one — which is exactly how this bug survives in codebases until the first squashed object. And the normalize is not optional: scale changes lengths, and everything downstream assumes unit vectors. (Translation, meanwhile, never touches normals at all — directions carry w = 0, a story homogeneous coordinates tells.)
Where Normals Are Spent
- Lighting —
dot(N, L)turns facing into brightness; the dot product page is built on it. - Backface culling — faces pointing away from the camera are discarded unrendered; see cross product.
- Bounces — physics reflects an incoming velocity across the surface normal; the same reflection gives specular highlights.
- Normal maps — a texture stores a normal per pixel (RGB = XYZ in tangent space), faking dents and grooves on flat geometry — the cheapest detail in graphics; see texturing.
Connections
Normals are born from the cross product, spent by the dot product, and kept honest by matrices — the inverse-transpose being the one place most people first meet that operation. They are directions, not points, so homogeneous coordinates give them w = 0 and translation passes through them. Per-pixel normal tricks and MatCap shading belong to texturing, and the lighting models that consume all of this live in shading.