Lambert
Purpose
Diffuse shading is one dot product — how aligned the surface is with the light — and every lighting model from here on is a variation or addition on top of that one dot.
Key insight
In 2D, every pixel asked "where am I on the screen?" In 3D, every pixel also asks "which way am I facing?" That facing is the surface normal, a unit vector pointing straight out of the surface. The light has a direction too. When the surface points at the light, dot(normal, light) is 1 (fully lit). When it's perpendicular, the dot is 0 (the terminator). When it faces away, the dot goes negative — we clamp it to 0, giving pitch black.
That single number, multiplied by the surface color, is Lambert. It is what chalk, matte paper, and unfinished wood look like — light scatters equally in all directions, so brightness depends only on how much light hits the surface, not on where the camera is. Move your head around a Lambert sphere and nothing changes.
Drag sun-angle and watch the bright band follow the light. Everything you'll ever learn about lighting is variations on that band.
Break it
Park the sun directly behind the hero meshes (around sun-angle = 270, with 90 lighting them from the camera's side). The visible faces go pitch black — every normal pointing at the camera points away from the light, so the dot is negative and we clamp.
Teaches: pure Lambert has no ambient term. "Away from the light" means zero photons, full stop. This is why real engines always add a tiny ambient floor or a fill light — otherwise shadowed sides are unreadable.
Direct Claude
// VERTEX — pass the world-space normal to the fragment stage
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// FRAGMENT — one dot product, clamped to zero, times base color
uniform vec3 uLightDir; // world-space light direction (unit vector)
uniform vec3 uBaseColor;
varying vec3 vNormal;
void main() {
vec3 n = normalize(vNormal);
float lambert = max(0.0, dot(n, uLightDir)); // THE lighting signal
vec3 col = uBaseColor * lambert; // no ambient, no specular
gl_FragColor = vec4(col, 1.0);
}