Normals

A normal is a unit vector standing perpendicular on a surface — the mesh's answer to "which way do I face?". It's stored data, not derived truth: the same triangles read as faceted or curved depending only on the normals they carry, and transforming them wrong is one of the classic graphics bugs.

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

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.