Non-photorealistic rendering

Cel shading builder

Cel shading is not a different lighting model — it is the ordinary diffuse term with the smooth ramp thrown away. Start from a normally shaded object on the left, then chop its N·L ramp into flat bands. Drag the band edges on the ramp strip to see exactly where each boundary lands on the surface.

Smooth diffuse continuous N·L
Drag to rotate

Every value of N·L between 0 and 1 maps to its own brightness, so the terminator between lit and unlit is a gradient many pixels wide.

Cel shaded 3 bands
Drag to rotate

The same N·L is snapped to the nearest band. The terminator collapses into a hard line, which is what reads as "drawn" rather than "rendered".

The ramp drag a band edge

The top strip is the smooth ramp, running from fully unlit at the left to fully lit at the right. The bottom strip is the ramp your shader actually samples. In a real engine this is a 1D texture — the fragment shader computes N·L, uses it as a texture coordinate, and reads back whatever colour the artist painted there.

Bands
Band edges at
Terminator width
Passes
Bands
Base colour
0.42
0.006
Extra passes
0.18
0.86
Light
-42°
34°

Move the light and watch the band edges travel across the surface as hard curves. Those curves are contour lines of N·L — the geometry is unchanged.

Why this needs the programmable pipeline
Fixed-function OpenGL 1.1 computes the lighting equation for you and interpolates the result; there is no hook to quantise it. Cel shading needs a fragment shader: compute float d = max(0.0, dot(N, L));, then either step through thresholds or use d as a texture coordinate into a 1D ramp texture. The ink outline is normally a second pass — either the back faces drawn slightly expanded, or the dot(N, V) test this demo uses.