Computer Graphics

Phong Shading and Lighting

Per-pixel normals, ambient + diffuse + specular, and a highlight Gouraud can't draw

Phong shading is a per-pixel lighting technique that interpolates surface normals across a triangle and evaluates the Phong reflection model — ambient + diffuse·(N·L) + specular·(R·V)ⁿ — at every fragment, producing smooth surfaces and sharp specular highlights that per-vertex Gouraud shading misses. Introduced by Bui Tuong Phong in his 1973 Utah dissertation and the 1975 Communications of the ACM paper, it evaluates lighting O(1) per fragment and dominated rendering for three decades. The Blinn-Phong variant swaps the reflection vector for a halfway vector to run faster.

  • Invented byBui Tuong Phong (1973 thesis, 1975 CACM)
  • Reflection modelAmbient + Diffuse + Specular
  • Diffuse termLambert cosine: max(N·L, 0)
  • Specular term(R·V)ⁿ (Phong) / (N·H)ⁿ (Blinn-Phong)
  • Cost per fragmentO(1) per light — O(k) for k lights
  • InterpolatesNormals (Phong) vs colors (Gouraud)
  • Blinn variantJim Blinn, 1977

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

Why Phong shading matters

Before Phong, real-time and early offline renderers had two bad choices. Flat shading computes one color per triangle from the face normal — fast, but every facet is visibly a facet, so a sphere looks like a disco ball. Gouraud shading (Henri Gouraud, 1971) softens this by computing lighting at each vertex and linearly interpolating the resulting colors across the face. That smooths the diffuse shading beautifully, but it has a fatal flaw for shiny surfaces: a specular highlight is a small, bright spot that usually lands in the interior of a triangle, in between the vertices. If none of the three vertices happens to be near the highlight, Gouraud interpolation averages three dim vertex colors and the highlight simply disappears — or, worse, flickers on and off as the mesh moves and the bright spot slides across vertices.

Phong's insight was to interpolate the geometry — the surface normals — instead of the result (the colors). You still evaluate the same lighting equation, but now you evaluate it once per pixel using a normal that varies smoothly across the surface. The highlight is computed exactly where it belongs, at the pixel where the reflection lines up with the eye, no matter how coarse the mesh. That single change is why a Phong-shaded low-poly sphere can look convincingly round with a crisp, correctly-shaped highlight, and it's the conceptual ancestor of every fragment shader you've ever written.

The Phong reflection model, term by term

The full local-illumination equation for one light is a sum of three physically-inspired terms. Let N be the unit surface normal, L the unit direction from the surface point to the light, V the unit direction to the viewer, and R = reflect(-L, N) = 2(N·L)N − L the reflection of the light about the normal.

  • Ambient. k_a · i_a. A constant flat term that fakes global bounce light so surfaces facing away from every light source aren't pure black. It has no directionality — it's the crudest part of the model and the first thing a modern renderer replaces (with ambient occlusion or image-based lighting).
  • Diffuse (Lambert cosine law). k_d · max(N·L, 0) · i_d. A perfectly matte (Lambertian) surface scatters incoming light equally in all directions, so its apparent brightness depends only on how much light it catches — proportional to the cosine of the angle between the normal and the light, which for unit vectors is exactly N·L. The max(·, 0) clamps the back face to zero (a surface can't be lit by a light behind it).
  • Specular (the highlight). k_s · max(R·V, 0)ⁿ · i_s. A shiny surface reflects light in a tight lobe around the mirror direction R. When your eye V lines up with R, R·V is near 1 and you see a bright spot. The exponent n — the shininess — controls how fast the highlight falls off: bigger n means a tighter, sharper spot.

Summed over all lights, the final color is k_a·i_a + Σ_lights [ k_d·max(N·Lⱼ,0)·i_dⱼ + k_s·max(Rⱼ·V,0)ⁿ·i_sⱼ ]. Every term is a handful of dot products, so lighting a fragment costs O(1) per light and O(k) for k lights — the workload scales linearly in lights and pixels, which is exactly what a GPU is built to parallelize.

How Phong shading works, step by step

The technique lives in the rasterization pipeline. For each triangle:

  1. Per vertex (vertex shader): transform the vertex position to clip space and transform the normal to world (or view) space with the inverse-transpose of the model matrix — using the plain model matrix skews normals under non-uniform scaling. Pass the world-space normal and world-space position down as varyings.
  2. Rasterization: the GPU linearly interpolates the varyings — including the normal and position — across every covered fragment using perspective-correct barycentric weights.
  3. Per fragment (fragment shader): normalize() the interpolated normal (interpolating two unit vectors does not yield a unit vector), compute L, V, and R (or the halfway H), and evaluate the reflection model. Write the resulting color.

Compare that to Gouraud, where step 1 evaluates the entire lighting equation and passes down a single color, step 2 interpolates that color, and step 3 does nothing but write it. Same rasterizer, same interpolation hardware — the only difference is what you interpolate and where you run the lighting math.

Blinn-Phong: the halfway-vector shortcut

In 1977 Jim Blinn noticed you can skip computing the reflection vector entirely. Define the halfway vector H = normalize(L + V) — the unit vector that bisects the light and view directions. The specular term becomes max(N·H, 0)ⁿ'. Geometrically, N·H is maximal exactly when the surface is oriented to mirror the light into the eye, so it captures the same highlight as R·V but with cheaper and better-behaved math.

Two practical wins. First, for a directional light and a distant viewer, both L and V are constant, so H is constant across the whole surface and can be hoisted out of the loop. Second, pure Phong's R·V can go negative and the highlight can vanish abruptly at grazing angles where L and V are nearly opposite; N·H stays well-defined and produces the elongated, streaked highlights you see on rough metal, which is closer to reality. The exponents are not interchangeable — a Blinn-Phong exponent of roughly 2× to 4× the Phong exponent yields a visually similar highlight size, because the angle between N and H is about half the angle between R and V.

Flat vs Gouraud vs Phong vs PBR

FlatGouraudPhongCook-Torrance (PBR)
Lighting evaluatedOnce per faceOnce per vertexOnce per fragmentOnce per fragment
InterpolatesNothing (constant)Vertex colorsNormalsNormals (+ tangents)
Specular highlightWhole facet or noneOnly near verticesCorrect, per-pixelCorrect, physically shaped
Cost per triangle1 lighting eval3 lighting evals~pixel-count evals~pixel-count evals (heavier)
Mach-band artifactsN/A (faceted)Yes on coarse meshNoNo
Energy conserving?NoNoNoYes
Year / originGouraud, 1971Phong, 1973–75Cook & Torrance, 1982

Implementation: a Blinn-Phong fragment shader in GLSL

The canonical modern implementation is a fragment shader. Note the per-fragment normalize(vNormal) — the single most common Phong bug is forgetting it.

// ---- Vertex shader ----
// Pass world-space position and normal to the fragment stage.
uniform mat4 uModel, uView, uProj;
uniform mat3 uNormalMatrix;   // = transpose(inverse(mat3(uModel)))
in  vec3 aPosition;
in  vec3 aNormal;
out vec3 vWorldPos;
out vec3 vNormal;

void main() {
    vec4 world = uModel * vec4(aPosition, 1.0);
    vWorldPos  = world.xyz;
    vNormal    = uNormalMatrix * aNormal;      // NOT normalized yet
    gl_Position = uProj * uView * world;
}

// ---- Fragment shader (Blinn-Phong) ----
uniform vec3  uLightPos;      // world-space point light
uniform vec3  uViewPos;       // camera position, world space
uniform vec3  uAlbedo;        // surface base color (k_d)
uniform vec3  uLightColor;
uniform float uAmbient;       // e.g. 0.08
uniform float uShininess;     // e.g. 64.0  (Blinn exponent)
in  vec3 vWorldPos;
in  vec3 vNormal;
out vec4 fragColor;

void main() {
    vec3 N = normalize(vNormal);                       // MUST renormalize
    vec3 L = normalize(uLightPos - vWorldPos);
    vec3 V = normalize(uViewPos  - vWorldPos);
    vec3 H = normalize(L + V);                          // halfway vector

    float diff = max(dot(N, L), 0.0);                  // Lambert cosine
    float spec = pow(max(dot(N, H), 0.0), uShininess); // gate below is optional
    if (diff == 0.0) spec = 0.0;                       // no highlight on back faces

    vec3 ambient  = uAmbient * uAlbedo;
    vec3 diffuse  = diff * uAlbedo      * uLightColor;
    vec3 specular = spec * vec3(1.0)    * uLightColor; // white specular
    fragColor = vec4(ambient + diffuse + specular, 1.0);
}

Swap the specular two lines for vec3 R = reflect(-L, N); float spec = pow(max(dot(R, V), 0.0), uShininess); and you have textbook Phong instead of Blinn-Phong. Everything else is identical — which underlines that the interpolation scheme (Phong shading) and the reflection formula are independent choices.

A worked example and a note on history

Take a single point on a matte-plastic sphere with albedo (0.2, 0.5, 0.9), ambient 0.1, shininess 32, lit by a white light. Suppose at this pixel the renormalized normal gives N·L = 0.7 and the halfway alignment gives N·H = 0.95. Then the diffuse contribution is 0.7 · (0.2,0.5,0.9) = (0.14, 0.35, 0.63), the specular is 0.95³² ≈ 0.19 of pure white, and ambient adds (0.02, 0.05, 0.09). Sum: roughly (0.35, 0.59, 0.91) — a saturated blue with a bright near-white glint layered on top. Move one pixel toward the highlight center and N·H rises to 0.99; 0.99³² ≈ 0.72, and the glint nearly whites out. That steep exponent curve is the entire visual difference between plastic and chrome.

The history is poignant. Bui Tuong Phong (1942–1975) developed the model as a graduate student at the University of Utah's legendary graphics group — the same lab that produced Ed Catmull, Jim Clark, John Warnock, and the Utah teapot. His 1973 PhD dissertation introduced both normal interpolation and the reflection model; the polished version appeared in the June 1975 Communications of the ACM as "Illumination for Computer Generated Pictures." He died of cancer that same year at age 32. Two years later Jim Blinn published the halfway-vector optimization, and "Blinn-Phong" became the default shading model in OpenGL's fixed-function pipeline and in nearly every game engine until physically based rendering took over in the 2010s. The model is still the first lighting equation every graphics programmer learns.

Common misconceptions and pitfalls

  • Confusing the model with the shading. "Phong shading" is normal interpolation; "the Phong reflection model" is the ambient+diffuse+specular equation. You can mix and match — Gouraud + Phong-model, or Phong-shading + a PBR BRDF.
  • Forgetting to renormalize the interpolated normal. Linear interpolation of unit vectors shortens them, darkening triangle interiors and shrinking highlights. Always normalize() per fragment.
  • Using the model matrix on normals. Under non-uniform scaling, normals must be transformed by the inverse-transpose of the model matrix, or they stop being perpendicular to the surface and lighting goes wrong.
  • Assuming Phong and Blinn-Phong exponents are equal. They aren't — expect to multiply the Phong exponent by ~2–4× to match highlight size in Blinn-Phong.
  • Treating it as physically correct. Classic Phong isn't energy-conserving; you can trivially make a surface reflect more than 100% of incident light. For photorealism use a normalized microfacet BRDF; Phong is a fast, empirical approximation.
  • Working in mixed spaces. All vectors (N, L, V, positions) must live in the same coordinate space — world or view — before you take dot products. Mixing world-space and view-space vectors is a classic source of "lighting rotates with the camera" bugs.

Frequently asked questions

What is the difference between Phong shading and the Phong reflection model?

They are two separate contributions from the same 1975 paper and are easy to conflate. The Phong reflection model is the local illumination equation: color = ambient + diffuse·(N·L) + specular·(R·V)^n. Phong shading is the interpolation scheme: instead of computing lighting at each vertex and interpolating the resulting colors (that's Gouraud), you interpolate the surface normals across the triangle and evaluate the reflection model per pixel. You can use the Phong reflection model with Gouraud interpolation, and you can use Phong interpolation with a different reflection model — the two ideas are orthogonal.

What is the difference between Gouraud shading and Phong shading?

Gouraud shading evaluates the lighting equation once per vertex and linearly interpolates the resulting RGB colors across the triangle's fragments. Phong shading interpolates the normal vectors across the triangle and evaluates the full lighting equation once per fragment. Gouraud is cheaper — a few lighting evaluations per triangle — but it misses specular highlights that fall in a triangle's interior, and it produces Mach-band artifacts on coarse meshes. Phong is more expensive but renders crisp, correctly-shaped highlights regardless of tessellation. On modern GPUs, per-pixel Phong-style shading is the default because fragment shaders make it cheap.

What is the Blinn-Phong halfway vector and why is it faster?

Jim Blinn's 1977 modification replaces the reflection vector R with the halfway vector H = normalize(L + V), the unit vector bisecting the light and view directions. The specular term becomes (N·H)^n' instead of (R·V)^n. It's faster because computing H avoids the reflect() operation, and for distant lights and viewers H can be treated as constant across the surface. It's also more physically plausible: as the view angle grazes the surface, pure Phong's R·V can misbehave, while N·H stays well-defined. The exponents differ — a Blinn-Phong exponent roughly 2–4× the Phong exponent produces a similar highlight size.

Why must you renormalize the interpolated normal in the fragment shader?

Linear interpolation of two unit vectors does not produce a unit vector — the interpolated result is shorter than length 1 everywhere except the endpoints, because a straight line between two points on a sphere dips inside the sphere. If you skip normalize() on the interpolated normal, N·L and the specular term come out too small, darkening the surface mid-triangle and shrinking highlights. Renormalizing per fragment restores unit length and is a single inverse-square-root, so it's cheap and essentially mandatory.

What does the shininess exponent control in Phong shading?

The shininess (or specular) exponent n controls the tightness of the specular highlight. Because the specular term is (R·V)^n and R·V is between 0 and 1, raising it to a higher power makes the falloff steeper: a small n (say 4) gives a broad, soft highlight like brushed metal or plastic, while a large n (say 128 or 256) gives a tiny, sharp highlight like polished chrome or glass. As n approaches infinity the highlight becomes a mirror-like point. The exponent is not physically derived — it's an artistic control that approximates microfacet roughness.

Is Phong shading physically based (PBR)?

No. The classic Phong model is empirical — it was tuned to look right, not derived from physics, and it does not conserve energy (you can make a surface reflect more light than hits it by cranking the coefficients). Modern physically based rendering uses microfacet BRDFs like Cook-Torrance with a normalized GGX distribution, Fresnel, and geometry terms. That said, Phong and Blinn-Phong remain everywhere: they're the mental model for lighting, they're fast, and an energy-normalized Blinn-Phong lobe is a reasonable cheap approximation of a real specular BRDF.

When did Phong shading appear and who invented it?

Bui Tuong Phong developed the model as part of his PhD work at the University of Utah. It appeared in his 1973 dissertation and was published in the widely-cited 1975 Communications of the ACM paper 'Illumination for Computer Generated Pictures.' Phong died of cancer in 1975 at age 32, shortly after the paper appeared. Jim Blinn published the halfway-vector variant in 1977. The model dominated real-time and offline rendering for roughly three decades before physically based rendering became mainstream.