Computer Graphics (rasterisation, ray tracing, WebGPU) Module 7 – Light, shading and transparency
Phong and Blinn-Phong reflection models
Compute the ambient, diffuse and specular terms of the Phong reflection model, swap its reflection vector for the half vector of Blinn-Phong, and compare.
What you will learn
- Compute the ambient, diffuse and specular terms of the Phong reflection model
- Replace the reflection vector with the half vector of Blinn-Phong and compare the highlights
- Match a Blinn-Phong exponent to a Phong exponent and predict where the match fails
- Implement toon shading and explain why neither classic model conserves energy
Before you start
What you’ll learn · Before you start
What you will learn
- Compute the ambient, diffuse and specular terms of the Phong reflection model
- Replace the reflection vector with the half vector of Blinn-Phong and compare the highlights
- Match a Blinn-Phong exponent to a Phong exponent and predict where the match fails
- Implement toon shading and explain why neither classic model conserves energy
Before you start
On this page
Lambert’s law alone makes every surface look like chalk: a polished ball under one lamp needs a bright spot where the lamp’s reflection meets your eye. The Phong and Blinn-Phong reflection models add that highlight as a third term, Phong as (r · v)^α and Blinn-Phong as (n · h)^β, from the half vector, with an exponent about four times larger for a spot of the same size.
Both are still in daily use; three.js’s MeshPhongMaterial, for one, computes its highlights with Blinn-Phong. The sections below build both on one sphere and compare them pixel for pixel.
Phong adds a highlight where the reflected light meets the eye
A mirror sends a lamp’s light in one direction only: r, the vector l mirrored about the normal, r = 2 (n · l) n − l. A polished surface behaves like a blurred mirror, brightest where r points at the viewer and dimmer as v turns away from r. Bui Tuong Phong’s model of 1975 adds three terms for each light:
In words: a constant ambient term: A constant added to every pixel by a classic reflection model, standing in for light bounced around the scene, so that the side facing away from every light is not black. ka for light bounced around the room, Lambert’s diffuse term, and a specular reflection: Reflection gathered around the mirror direction, seen as a highlight that moves when the viewer moves; the Phong and Blinn-Phong models give it as a power of a cosine. term, ks times a factor that peaks at 1 when r points straight at the viewer. The exponent α, the shininess: The exponent of a Phong or Blinn-Phong specular term: the larger it is, the faster the highlight falls away from its peak and the smaller it looks., decides how fast the highlight falls away: 0.9731 to the power 32 is 0.418. Phong chose his exponents from 1 to 10 and wrote that such values “are empirically adjusted for the picture”. The figure shows the vectors at one point:
The five vectors of the Phong and Blinn-Phong models
Text description of the diagram
A flat surface is drawn as a horizontal line, with one point marked on it. Five arrows start at the point. The normal n points straight up. The vector l, towards the light, leans 50° to the left of n. Its mirror image about n, the reflection vector r, is dashed and leans 50° to the right. The vector v, towards the viewer, leans 20° to the right, so the angle between r and v, marked with an arc, is 30°. The half vector h, halfway between l and v, leans 15° to the left of n, and the arc between n and h is marked 15°: half of the 30° between r and v, as it always is when all the vectors lie in one plane. A legend under the drawing names the five vectors.
Step through one pixel of the sphere below, from its three vectors to its value:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The vectors. At the program's pixel the normal is n = (−0.250, 0.250, 0.935). The light is along l = (−0.577, 0.577, 0.577) and the viewer along v = (0, 0, 1), both far away.
- Diffuse. n · l = 0.829, so the diffuse term is kd × 0.829 = 0.6 × 0.829 = 0.497: Lambert's law, unchanged.
- Mirror. Mirroring l about n gives r = 2 (n · l) n − l = (0.163, −0.163, 0.973), the direction a mirror at this point would send the light.
- Specular. r · v = 0.9731: the reflection points almost at the viewer. Raised to the power α = 32 it shrinks to 0.418, and ks × 0.418 = 0.4 × 0.418 = 0.167.
- The sum. Ambient, diffuse and specular add up: 0.1 + 0.497 + 0.167 = 0.764, the value the program prints for Phong with α = 32.
- The Blinn-Phong branch. Blinn-Phong skips the mirror. It compares n with h = (−0.325, 0.325, 0.888): n · h = 0.993. With β = 32 that gives 0.805 and a total of 0.919; with β = 128, 0.420 and 0.765, almost Phong's value.
Checkpoint
Check: The three terms of the Phong model
Blinn-Phong compares the half vector with the normal
James Blinn’s model of 1977 asks another question: how far is the normal from the one a perfect mirror would need to send the light to the viewer? That normal is the half vector: h = normalise(l + v), the direction halfway between the light and the viewer: the normal a perfect mirror would need to reflect the light into the eye. h, halfway between l and v, and the highlight becomes (n · h)^β:
In words: the same three terms, with n · h in place of r · v; at the pixel above n · h is 0.993 where r · v is 0.973. The angle between n and h is about half the angle between r and v, so β must be about four times α for a highlight of the same size. With the light and the viewer both far away, h is the same at every pixel and is computed once, where Phong needs a reflection per pixel; OpenGL 2.1’s fixed-function lighting used h this way. The program counts the samples where each highlight is above half its peak:
// The Phong and Blinn-Phong reflection models on one sphere: one directional light, a viewer far along +z.
// I = ka + kd max(0, n.l) + ks spec, where spec = max(0, r.v)^a for Phong and max(0, n.h)^b for Blinn-Phong.
// Both are 0 when the light is behind the surface (n.l <= 0).
const len = (v) => Math.hypot(v[0], v[1], v[2]);
const normalize = (v) => v.map((c) => c / len(v));
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const fmt = (v) => `(${v.map((c) => c.toFixed(3)).join(', ')})`;
const l = normalize([-1, 1, 1]); // towards the light
const v = [0, 0, 1]; // towards the viewer, the same at every pixel
const h = normalize(l.map((c, k) => c + v[k])); // the half vector: also the same at every pixel
const KA = 0.1;
const KD = 0.6;
const KS = 0.4;
const reflect = (n) => n.map((c, k) => 2 * dot(n, l) * c - l[k]); // r = 2 (n.l) n - l
const phong = (n, a) => (dot(n, l) > 0 ? Math.max(0, dot(reflect(n), v)) ** a : 0);
const blinn = (n, b) => (dot(n, l) > 0 ? Math.max(0, dot(n, h)) ** b : 0);
const total = (n, spec) => KA + KD * Math.max(0, dot(n, l)) + KS * spec;
// One pixel, step by step.
const n = normalize([-0.25, 0.25, Math.sqrt(0.875)]);
const r = reflect(n);
console.log(`n = ${fmt(n)}, n.l = ${dot(n, l).toFixed(3)}`);
console.log(`r = ${fmt(r)}, r.v = ${dot(r, v).toFixed(3)}`);
console.log(`h = ${fmt(h)}, n.h = ${dot(n, h).toFixed(3)}`);
for (const [name, spec] of [['Phong a=32', phong(n, 32)], ['Blinn b=32', blinn(n, 32)], ['Blinn b=128', blinn(n, 128)]]) {
console.log(`${name.padEnd(11)} ${KA} + ${KD} x ${dot(n, l).toFixed(3)} + ${KS} x ${spec.toFixed(3)} = ${total(n, spec).toFixed(3)}`);
}
// The whole sphere, 400 x 400 samples: how large is each highlight?
console.log('\nsamples where spec is above 0.5:');
for (const [name, spec] of [['Phong a=32', (m) => phong(m, 32)], ['Blinn b=32', (m) => blinn(m, 32)], ['Blinn b=128', (m) => blinn(m, 128)]]) {
let count = 0;
for (let j = 0; j < 400; j++) {
for (let i = 0; i < 400; i++) {
const x = ((i + 0.5) / 400) * 2 - 1;
const y = 1 - ((j + 0.5) / 400) * 2;
if (x * x + y * y <= 1 && spec([x, y, Math.sqrt(1 - x * x - y * y)]) > 0.5) count++;
}
}
console.log(` ${name.padEnd(11)} ${String(count).padStart(5)}`);
}
// The sphere under Blinn-Phong, b = 128, in tenths; # is 1.0 or more.
console.log('\nBlinn-Phong, b = 128:');
for (let j = 0; j < 16; j++) {
let row = '';
for (let i = 0; i < 32; i++) {
const x = ((i + 0.5) / 32) * 2.2 - 1.1;
const y = 1.1 - ((j + 0.5) / 16) * 2.2;
if (x * x + y * y > 1) {
row += '.';
continue;
}
const m = [x, y, Math.sqrt(1 - x * x - y * y)];
const value = total(m, blinn(m, 128));
row += value >= 1 ? '#' : String(Math.floor(value * 10));
}
console.log(row);
} Output
n = (-0.250, 0.250, 0.935), n.l = 0.829 r = (0.163, -0.163, 0.973), r.v = 0.973 h = (-0.325, 0.325, 0.888), n.h = 0.993 Phong a=32 0.1 + 0.6 x 0.829 + 0.4 x 0.418 = 0.764 Blinn b=32 0.1 + 0.6 x 0.829 + 0.4 x 0.805 = 0.919 Blinn b=128 0.1 + 0.6 x 0.829 + 0.4 x 0.420 = 0.765 samples where spec is above 0.5: Phong a=32 1342 Blinn b=32 4729 Blinn b=128 1204 Blinn-Phong, b = 128: ................................ .........56666555544432......... ......66666666665554443321...... .....6666666666555544433221..... ...66666667766655554443322111... ..566666679#865555444333221111.. ..6666666677655544443332211111.. .455666655555554444333222111111. .455555555554444433322211111111. ..4455554444444333322211111111.. ..3444444443333322221111111111.. ...23333333332222111111111111... .....2222222221111111111111..... ......11111111111111111111...... .........11111111111111......... ................................
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node phong_blinn.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
At β = 32 the Blinn-Phong highlight covers 4,729 samples, 3.5 times Phong’s 1,342; at β = 128 it covers 1,204, and the pixel’s value is 0.765 against Phong’s 0.764.
Divide Phong's exponent instead
Press Edit and change Phong’s exponent in the counting loop from 32 to 8. Its highlight grows to 5,217 samples, close to Blinn-Phong’s 4,729 at 32: the rule of four again, from the other side.
The table puts the two models side by side:
| Phong | Blinn-Phong | |
|---|---|---|
| Compares | r with v | n with h |
| Distant light and viewer | r at every pixel | h once per frame |
| Exponent for a similar spot | α | about 4α |
| Light near grazing | r · v can turn negative inside the lit area, cutting the highlight off | n · h stays positive while l and v are above the surface |
The Phong and Blinn-Phong reflection models both add a highlight to Lambert’s law, and four more things matter once either is in a renderer. At grazing angles the two highlights take different shapes. Three common shader bugs move or remove the highlight. toon shading: A non-photorealistic shading style, also called cel shading, that cuts the lighting into a few flat bands of tone so that a rendered surface looks drawn rather than lit. cuts the same terms into flat bands. Neither model keeps the light it reflects below the light it receives.
Checkpoint
Check: The half vector of Blinn-Phong
Practise: both models in code
The exercise writes both models. Its sample tests use the pixel above, seen from straight ahead and from one side, a viewer at the mirror direction and a light behind the surface.
Exercise · Medium · JavaScript
Write the Phong and Blinn-Phong reflection models
Write phong(n, l, v, m) and blinnPhong(n, l, v, m) in models.mjs. n, l and v are unit vectors: the surface normal, the direction towards the light and the direction towards the viewer. m holds ka, kd, ks and shininess. Both return one number:
- Phong: ka + kd × max(0, n · l) + ks × max(0, r · v)^shininess, with r = 2 (n · l) n − l;
- Blinn-Phong: ka + kd × max(0, n · l) + ks × max(0, n · h)^shininess, with h = normalise(l + v);
- in both, the specular term is 0 when n · l ≤ 0, so a light behind the surface leaves only the ambient term.
The starter's phong has no highlight at all, and its blinnPhong forgets to normalise h and lights the dark side.
Starter code · models.mjs
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const normalize = (a) => {
const d = Math.hypot(a[0], a[1], a[2]);
return [a[0] / d, a[1] / d, a[2] / d];
};
/** Phong: ka + kd max(0, n.l) + ks max(0, r.v)^shininess, r = 2 (n.l) n - l; no specular when n.l <= 0. */
export function phong(n, l, v, m) {
// Only ambient and diffuse so far: the highlight is missing.
return m.ka + m.kd * Math.max(0, dot(n, l));
}
/** Blinn-Phong: the same, with max(0, n.h)^shininess and h = normalize(l + v). */
export function blinnPhong(n, l, v, m) {
// The half vector is not normalised, and nothing stops the highlight on the dark side.
const h = [l[0] + v[0], l[1] + v[1], l[2] + v[2]];
return m.ka + m.kd * Math.max(0, dot(n, l)) + m.ks * Math.max(0, dot(n, h)) ** m.shininess;
} The sample tests · models.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { phong, blinnPhong } from './models.mjs';
const near = (got, want, what) => assert.ok(Math.abs(got - want) <= 1e-9 * Math.max(1, Math.abs(want)), `${what}: expected ${want}, got ${got}`);
const unit = (a) => {
const d = Math.hypot(a[0], a[1], a[2]);
return [a[0] / d, a[1] / d, a[2] / d];
};
const M = { ka: 0.1, kd: 0.6, ks: 0.4, shininess: 32 };
test("the lesson's pixel under both models", () => {
const n = unit([-0.25, 0.25, Math.sqrt(0.875)]);
const l = unit([-1, 1, 1]);
const v = [0, 0, 1];
near(phong(n, l, v, M), 0.7642488106803615, 'Phong, shininess 32');
near(blinnPhong(n, l, v, M), 0.9192567013305534, 'Blinn-Phong, shininess 32');
near(blinnPhong(n, l, v, { ...M, shininess: 128 }), 0.7652471130101057, 'Blinn-Phong, shininess 128');
const aside = unit([0.2, -0.3, 0.9]);
near(phong(n, l, aside, M), 0.8632785277264826, 'Phong, a viewer off the z axis');
near(blinnPhong(n, l, aside, M), 0.9546921902737963, 'Blinn-Phong, the same viewer');
});
test('a viewer at the mirror direction sees the full highlight under both', () => {
const n = [0, 0, 1];
const l = [0.6, 0, 0.8];
const v = [-0.6, 0, 0.8];
near(phong(n, l, v, M), 0.98, 'Phong: ka + kd x 0.8 + ks');
near(blinnPhong(n, l, v, M), 0.98, 'Blinn-Phong: h is n itself');
const off = unit([-0.6, 0.3, 0.8]);
near(phong(n, l, off, M), 0.6807479050690338, 'Phong, viewer off the plane of incidence');
near(blinnPhong(n, l, off, M), 0.8145718870334656, 'Blinn-Phong, the same viewer');
});
test('no highlight and no diffuse light from a light behind the surface', () => {
const n = [0, 0, 1];
const behind = [0.6, 0, -0.8];
const v = [-0.6, 0, 0.8];
near(phong(n, behind, v, M), 0.1, 'Phong keeps only the ambient term');
near(blinnPhong(n, behind, v, M), 0.1, 'Blinn-Phong keeps only the ambient term');
}); A hint
Compute n · l once and use it three times: for the diffuse term, inside r = 2 (n · l) n − l, and to decide whether there is a specular term at all. For Blinn-Phong, add l and v, divide the sum by its length, and only then take its dot product with n.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Practice that adapts to you
Practise this lesson
6 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Matching the two highlights, with the exponents measured Pro: opens with a Pro, Premium or Ultimate pass
The rule of four comes from geometry in one plane, and outside that plane it breaks. This part derives it, measures the exponent that matches each Phong exponent, and renders a floor on which the matched highlights take different shapes.
In this part:
- Why the angle to h is half the angle to r, and what that makes the exponent
- A floor at a grazing angle: a wide blob against a tall streak
2 runnable examples (JavaScript) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Three shader bugs that move or remove the highlight, reproduced Pro: opens with a Pro, Premium or Ultimate pass
Each of these bugs compiles, runs and gives a picture that looks almost right. This part reproduces all three on the lesson’s sphere and gives the one-line fix for each.
In this part:
- A missing clamp lights the dark side
- reflect() wants the vector towards the surface
- A fractional power of a negative cosine is not a number
1 runnable example (JavaScript) · about 3 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Toon bands and the energy the classic models lose, with numbers Pro: opens with a Pro, Premium or Ultimate pass
The same terms can be cut into flat bands for a drawn look, or corrected for a physical one. This part builds a toon shader with outlines, then measures how much light each model sends back in total.
In this part:
- Toon shading: the cosine in bands, outlines from depth jumps
- Cut lighting into toon bands and find the outlines
- Results of the sample tests
- Neither model conserves energy, measured over the hemisphere
2 runnable examples (JavaScript) · 1 exercise with sample tests · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
Start with the warm-up, answered here in full. Three harder questions follow, on highlights that appear, vanish or shrink when they should not; each row leads to its opening move on the module’s question page:
-
Fresher to mid · Warm-up
What is the difference between the Phong and the Blinn-Phong reflection models?
Both add three terms for each light: a constant ambient term, Lambert's diffuse term kd max(0, n · l), and a specular highlight. Phong mirrors the light direction about the normal, r = 2 (n · l) n − l, and measures how close r comes to the viewer: ks max(0, r · v)^α. Blinn-Phong uses the half vector h = normalise(l + v), the normal a mirror would need to reflect the light into the eye, and measures it against the surface normal: ks max(0, n · h)^β. The angle from n to h is about half the angle from r to v, so β must be about four times α for the same highlight. With a distant light and viewer, h is the same at every pixel and computed once, where r is needed per pixel.
-
Mid to senior
Testers report a faint second highlight on the shadowed side of every shiny prop, opposite the real one. The fragment shader computes the specular term as pow(dot(r, v), 32.0) after r = reflect(-l, n). Explain the bug with numbers, fix it, and say what a fractional shininess would have shown instead.
-
Mid to senior
An engine switches its shiny materials from Phong to Blinn-Phong, and every highlight suddenly looks about three times bigger. The art team asks for one conversion factor for the shininess values they tuned. What do you give them, and where will it still look different?
-
Senior to staff
A lighting artist raises a Phong material's shininess from 8 to 128 to make a car look polished. The highlight shrinks but gets no brighter, and the car looks duller overall. Explain this with an energy argument and numbers, and fix it without leaving the classic model.
Key takeaways
- Phong adds ka, Lambert’s diffuse term and ks max(0, r · v)^α, where r is l mirrored about the normal.
- Blinn-Phong compares the half vector h = normalise(l + v) with the normal, computed once per frame when the light and the viewer are far away.
- The angle from n to h is about half the angle from r to v, so a Blinn-Phong highlight needs an exponent about four times larger: 128 matches Phong’s 32 on the lesson’s sphere.
Terms in this lesson
- ambient term
- A constant added to every pixel by a classic reflection model, standing in for light bounced around the scene, so that the side facing away from every light is not black.
- half vector
- h = normalise(l + v), the direction halfway between the light and the viewer: the normal a perfect mirror would need to reflect the light into the eye.
- shininess
- The exponent of a Phong or Blinn-Phong specular term: the larger it is, the faster the highlight falls away from its peak and the smaller it looks.
- specular reflection
- Reflection gathered around the mirror direction, seen as a highlight that moves when the viewer moves; the Phong and Blinn-Phong models give it as a power of a cosine.
- toon shading
- A non-photorealistic shading style, also called cel shading, that cuts the lighting into a few flat bands of tone so that a rendered surface looks drawn rather than lit.
References
- Illumination for computer generated pictures (Communications of the ACM, vol. 18, no. 6) (Association for Computing Machinery)
- Models of light reflection for computer synthesized pictures (SIGGRAPH Computer Graphics, vol. 11, no. 2) (Association for Computing Machinery)
- The OpenGL Graphics System: A Specification, Version 2.1, section 2.14.1 (lighting with the half vector) (Khronos Group)
- WebGPU Shading Language: built-in functions reflect, pow and normalize (World Wide Web Consortium (W3C))
- ECMAScript language specification: Number::exponentiate (Ecma International (TC39))
- three.js documentation: MeshPhongMaterial (three.js authors)
- Computer Science Curricula 2023: Graphics and Interactive Techniques (GIT-Shading) (ACM, IEEE Computer Society and AAAI)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress