Shading and Lighting
Render a plain grey sphere with no lighting at all and you get a flat, dead disc — a coin, not a
ball. What turns that disc into a convincing three-dimensional object is shading:
deciding, for every pixel on the surface, how bright it should be. And brightness, it turns out,
is not a property of the surface alone — it depends on where the light is and
which way the surface faces. Tilt a patch of surface toward the lamp and it glows; turn it
away and it falls into shadow. That single geometric fact is the seed of almost all real-time
lighting.
Now put two balls side by side under the same lamp — a matte rubber ball and a polished billiard
ball. The matte one has a soft, even glow that changes gradually across its surface. The shiny one
has that same soft glow plus a tight, brilliant white dot where the lamp reflects
straight into your eye. Same light, same shape, wildly different look. A good lighting model has to
reproduce both behaviours — and the classic one that does, taught in every graphics course
since 1975, is the Phong reflection model.
The surface normal: which way a patch faces
Everything starts with the surface normal N — a unit
vector sticking straight out of the surface at the point we are shading, perpendicular to it. The
normal is how the geometry tells the lighting equation which way this little patch is pointing. A
patch on the top of a sphere has a normal pointing up; a patch on the side has a normal pointing
sideways. We also need L, the unit vector pointing from the surface
point toward the light, and V, the unit vector pointing toward
the viewer's eye. These three little arrows — N,
L, V — are all the geometry the model needs.
Because N and L are both unit vectors, their
dot product has a
beautiful meaning: it is exactly the cosine of the angle
\theta between them,
N \cdot L = |N|\,|L|\cos\theta = \cos\theta,
so N\cdot L is a ready-made "how well does this patch face the light?"
dial, running from 1 (facing straight at the lamp) down through
0 (edge-on) to -1 (facing away).
Diffuse: Lambert's cosine law
A matte surface — chalk, paper, rough plastic — scatters incoming light equally in all directions.
How bright it looks does not depend on where you stand; it depends only on how square-on
the surface is to the light. A patch tilted at angle \theta to the
incoming rays catches light spread over a larger area, so each point receives less — and the amount
it receives falls off as \cos\theta. This is
Lambert's cosine law, and it gives the diffuse term:
I_\text{diffuse} = k_d \,\max(0,\, N \cdot L).
Here k_d is the surface's diffuse colour/reflectivity (how much of each
colour channel it bounces back). The \max(0,\,\cdot) is not decoration:
when the light is behind the surface, N\cdot L goes
negative, and a negative brightness is meaningless — we clamp it to
0 so a back-facing patch simply sits in shadow instead of glowing with
"negative light". This one term alone already makes a sphere look round.
Turn the light and watch the patch respond
Below, the normal N points straight up out of the surface. Sweep the
light direction L around with the slider and watch two things at once:
the angle \theta between N and
L, and the brightness of the illuminated patch on the right. When the
light is overhead the patch blazes (N\cdot L = 1); as the light swings
toward the horizon it dims to nothing (N\cdot L = 0); once the light
drops below the surface the diffuse term clamps to 0 and the
patch goes dark. That is Lambert's law, live.
Specular: the shiny highlight
Diffuse alone gives us the matte ball. To get the billiard ball's bright dot we add a
specular term, which models a near-mirror bounce. Reflect the light direction
L about the normal to get the ideal reflection direction
R; the highlight is brightest when that reflected ray shoots straight
into the viewer's eye, i.e. when R lines up with
V. So we use R\cdot V, and raise it to a
power to make the spot tight:
I_\text{specular} = k_s \,\big(\max(0,\, R \cdot V)\big)^{n}.
The exponent n is the shininess. A small
n (say 4) gives a broad, soft sheen like
brushed metal; a large n (say 128) squeezes
the highlight into a tiny, mirror-like glint, because raising a number below
1 to a high power crushes it toward zero everywhere except right at the
peak. k_s is the specular colour, usually white-ish, because a highlight
is mostly the colour of the light, not the surface.
Putting it together: the Phong sum
Real scenes also have light bouncing around indirectly — from walls, the floor, the sky — filling
in the shadows so they are never pitch black. Rather than simulate all of that, Phong adds a cheap
constant ambient term. The full model is just the sum of the three:
I = \underbrace{k_a}_{\text{ambient}} \;+\; \underbrace{k_d\,\max(0,\,N\cdot L)}_{\text{diffuse}} \;+\; \underbrace{k_s\,\big(\max(0,\,R\cdot V)\big)^{n}}_{\text{specular}}.
Ambient sets a baseline glow everywhere; diffuse shapes the form as the surface turns toward and
away from the light; specular drops the sparkle on top. Evaluate this once per pixel, per light,
for each colour channel, and a bare grey disc becomes a shiny, rounded ball. It is not a physically
exact simulation — but it is fast, tunable, and looks convincing, which is why it powered real-time
graphics for decades and still shows up in shaders today.
Worked example: computing a diffuse value
Suppose the surface normal is N = (0, 1, 0) (pointing straight up) and
the light direction is L = (0.6, 0.8, 0) — already a unit vector, since
\sqrt{0.6^2 + 0.8^2} = \sqrt{0.36 + 0.64} = 1. The dot product is
N\cdot L = (0)(0.6) + (1)(0.8) + (0)(0) = 0.8.
That is positive, so no clamping is needed, and with a diffuse reflectivity
k_d = 1 the diffuse brightness is
\max(0,\,0.8) = 0.8 — the patch shines at 80% of full. Now swing the
light below the surface to L = (0.6, -0.8, 0): the dot product becomes
-0.8, the \max clamps it to
0, and the patch is correctly dark. Same arithmetic, one sign flip, all
the difference between lit and shadowed.
-
Always normalize your normals (and L). The identity
N\cdot L = \cos\theta holds only when both vectors have length
1. When you interpolate normals across a triangle, or scale a model
with a non-uniform transform, the normals quietly stop being unit length — and then
N\cdot L is |N|\cos\theta, not
\cos\theta. The result: surfaces that are mysteriously too bright, too
dark, or that pulse as they animate. Re-normalize (N/|N|) before you
take the dot product.
-
Never drop the \max(0,\,\cdot). Forget the clamp and
a light behind a surface contributes negative brightness, which either subtracts light
it shouldn't or, once colours are added up, wraps around to something garish. Back-facing light
must contribute exactly zero, not a negative number. The clamp is doing real physical work — keep
it on every term that uses a dot product.
The Phong reflection model above says how to compute brightness at a single point. A
separate choice is where on the triangle you run it — and that choice has a bigger visual
impact than beginners expect.
-
Flat shading: evaluate the lighting once per triangle, using a single normal for
the whole face. Fast, but every facet is one solid tone, so curved objects look like cut gems —
you can see every polygon.
-
Gouraud shading: evaluate the lighting at the three vertices (each with
its own averaged normal) and then interpolate the resulting colours smoothly across the
triangle. Cheap and smooth — but because you interpolate colours, a tiny specular highlight that
falls between vertices can be missed entirely or smeared into a dull blob.
-
Phong shading: interpolate the normals across the triangle and run the
full lighting equation at every pixel. More work, but highlights are crisp and land
exactly where they should. (Note the two uses of "Phong": the reflection model — ambient
+ diffuse + specular — versus per-pixel Phong shading. They are named after the same
person, Bui Tuong Phong, but they are different ideas.)