Lighting and shading

05-lighting-and-shading.md · updated 2026-10-15

Shading answers one question per pixel: how much light leaves this surface towards my eye? Lambert, Phong, PBR are just different guesses at that answer. The inputs are always the same: the surface normal N, the direction to the light L, the direction to the eye V, and the surface's own colour.

Normals are the whole game

Brightness comes from the normal, not the position.

The normal is the unit vector pointing straight out of the surface. Move a wall and it looks the same; tilt it and it changes completely. Normals must stay unit length (renormalise after interpolation), and non-uniform scaling breaks them unless you transform by the inverse-transpose of the model matrix.

This is why flat vs smooth shading is a normals question, not a geometry question. The same triangles look faceted when each vertex carries its face's normal and rounded when each carries the average of the faces around it — the silhouette stays just as angular. Smooth shading is a lie told by interpolated normals.

Diffuse, specular, ambient

Diffuse = how square-on the surface faces the light. Specular = whether the bounce is heading at my eye.

Sum the three. That's classic shading.

The bridge to PBR

PBR swaps invented knobs for measured ones, and refuses to emit more light than it received.

Shininess "80" never meant anything, so a material tuned under one light looked wrong under another. PBR uses three parameters that do mean something: albedo (the base colour actually reflected), roughness (how scattered the micro-surface bounce is — the honest replacement for shininess), and metalness (metals tint their reflection and have no diffuse; dielectrics have white reflections and do). Energy conservation ties them together — diffuse plus specular can never exceed the incoming light — so shinier automatically means less diffuse. That is what makes a PBR material look right in any lighting instead of needing retuning per scene.

Normal maps are fake normals from a texture: an RGB-encoded normal per texel, perturbing N before lighting. The geometry stays flat, the lighting doesn't. Silhouettes give the lie away.

Demo

Lighting terms, isolated and summed — drag the light, then use the view selector to see each term alone before the sum. Toggle flat normals to watch a sphere facet without changing shape.

Gotchas

Symptom Cause
Object gets brighter when scaled downNormals scaled with the model matrix instead of its inverse-transpose, then not renormalised
Smooth model shows faceted bandsNormals not renormalised in the fragment shader after interpolation
Specular highlight leaks onto the dark sideSpecular not gated by dot(N, L) > 0
Ringed highlight, or black specklespow() on a negative base; clamp the dot product to 0 first
Normal map looks lit from belowGreen channel convention (OpenGL +Y vs DirectX −Y)
Normal-mapped surface lights wrongly as the model rotatesLighting done in the wrong space — tangent-space normals need the TBN basis
Everything looks washed out and flatLighting maths done on sRGB values; work in linear, convert at the end
Hard terminator / banding on a sphere8-bit output with no dither, or mediump normals on mobile

Open the demo →

← all notes & demos