Toon / Cel Shading
Purpose
Toon shading is Lambert with a staircase — the smooth gradient becomes two flat plateaus, and every "hand-drawn 3D" look (anime, BOTW, Borderlands) is some variation on where those staircase steps land.
Key insight
Realistic shading wants continuous tone. Stylized shading wants decisions. Toon takes the Lambert value and quantizes it — rounds it off into a small number of flats. Two bands is the classic anime look: a lit tone and a shadow tone with a sharp line between them.
The placement of the threshold is the expressive lever. Pushing it toward 1 makes more of the surface fall into shadow (brooding, high-contrast). Pulling it toward 0 makes everything lit (bright cartoon). The softness of the band edge is the other lever — a razor-sharp line is graphic and draftsman-like; a slightly soft line is painterly without losing the flats.
The sliders are two orthogonal decisions. shadow-threshold is where the line is; edge-hardness is how crisp it is (here, 0 means hard step, 1 means wide painterly smear).
Break it
Shadow-threshold to 0.05, edge-hardness at 0. The object is almost entirely lit with a thin rind of shadow on the dark side. Now push shadow-threshold to 0.9: almost everything falls into shadow.
Teaches: toon thresholds decide how much of the shape reads as "in shadow" — that's a compositional choice, not a lighting one. Moving the threshold is closer to choosing a painting's key than to adjusting a light. Same geometry, same light, completely different mood.
Direct Claude
// VERTEX — standard normal pass-through
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// FRAGMENT — Lambert signal, then smoothstep'd into two flat bands
uniform vec3 uLightDir;
uniform vec3 uShadowColor;
uniform vec3 uLitColor;
uniform float uThreshold; // where the band edge lands
uniform float uHardness; // 0 = hard step, higher = painterly smear
varying vec3 vNormal;
void main() {
vec3 n = normalize(vNormal);
float lambert = max(0.0, dot(n, uLightDir));
// quantize: smoothstep across a soft window centered on the threshold
float w = max(0.001, uHardness);
float band = smoothstep(uThreshold - w, uThreshold + w, lambert);
vec3 col = mix(uShadowColor, uLitColor, band);
gl_FragColor = vec4(col, 1.0);
}