Blinn-Phong
Purpose
The shiny highlight on a classic 3D render is a second dot product raised to a power — Lambert tells you how much light hits the surface, specular tells you how much of that light bounces toward the camera.
Key insight
Lambert is camera-blind. Move your head around a Lambert sphere and nothing changes — the lit side stays the lit side. But real objects have a bright spot that follows your eye. That's because the camera matters now. Blinn-Phong finds the halfway vector between the light direction and the view direction, then asks how closely the surface normal matches that halfway vector. When they match, the surface is reflecting the light directly at you, so you see the brightest highlight.
Raising that dot to a power controls how tight the highlight is. Low power (say 4) smears it across half the sphere; high power (say 256) shrinks it to a pinpoint. The exponent is the single most important lever in pre-PBR shading — it is the difference between a matte ceramic and a glossy pool ball.
Drag glossiness and watch the highlight tighten from a broad sheen to a hotspot to a star.
Break it
Glossiness to 1. The specular lobe spreads across the entire lit hemisphere, and the surface stops looking "shiny" separately from "bright" — it just looks overexposed.
Teaches: specular needs to be spatially concentrated to read as a highlight. Without tightness, it's just more diffuse. This is why pre-PBR games look plastic when tuned wrong — flat specular everywhere reads as cheap.
Direct Claude
// VERTEX — pass normal + world position so fragment can build a view vector
varying vec3 vNormal;
varying vec3 vWorldPos;
void main() {
vNormal = normalize(normalMatrix * normal);
vec4 wp = modelMatrix * vec4(position, 1.0);
vWorldPos = wp.xyz;
gl_Position = projectionMatrix * viewMatrix * wp;
}
// FRAGMENT — Lambert (diffuse) + Blinn-Phong specular (halfway dot, raised to power)
uniform vec3 uLightDir;
uniform vec3 uBaseColor;
uniform float uGlossiness; // the specular exponent
uniform vec3 uCameraPos;
varying vec3 vNormal;
varying vec3 vWorldPos;
void main() {
vec3 n = normalize(vNormal);
vec3 v = normalize(uCameraPos - vWorldPos); // view direction
vec3 h = normalize(uLightDir + v); // halfway vector
float diffuse = max(0.0, dot(n, uLightDir));
float spec = pow(max(0.0, dot(n, h)), uGlossiness);
vec3 col = uBaseColor * (0.08 + diffuse) + vec3(1.0) * spec;
gl_FragColor = vec4(col, 1.0);
}