Gouraud Versus Phong Shading

Two classic papers name the interpolation choices of Vertex vs Fragment Shading. Henri Gouraud's "Continuous Shading of Curved Surfaces" (1971) computed colors at vertices and interpolated them; Bui Tuong Phong's "Illumination for Computer Generated Pictures" (1975) interpolated normals and shaded every pixel, and introduced the specular term still called the Phong reflection model. "Phong shading" (normal interpolation) and the "Phong model" (the equation) are separate ideas: you can evaluate the Phong model per vertex, which is Gouraud shading.

Flat, Gouraud and Phong shading compared
Shading Lighting computed Interpolated Look Cost
Flat Once per triangle Nothing Faceted Lowest
Gouraud Per vertex Colors Smooth, lost highlights Per vertex
Phong Per fragment Normals, positions Smooth, sharp highlights Per pixel

For a deliberate low-poly look, declare the normal output flat, or derive the face normal from screen-space derivatives, normalize(cross(dFdx(vPosition), dFdy(vPosition))), which also sidesteps Chrome 154 1 's Direct3D 11 bug with flat outputs in programs that draw both lines and triangles (Drawing Modes).

Flat, Gouraud and Phong shading of the same low-poly globe with the same Phong lighting modelHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  .names { display: flex; max-width: 600px; text-align: center; }
  .names div { flex: 1; padding: 4px 2px; }
  .names b { display: block; font-size: 12px; }
</style>
<canvas id="c" width="600" height="200"></canvas>
<div class="names">
  <div><b>Flat</b>face normal from dFdx / dFdy</div>
  <div><b>Gouraud (1971)</b>colours per vertex, interpolated</div>
  <div><b>Phong (1975)</b>normals interpolated, lit per pixel</div>
</div>
<script>
const m4 = {
  perspective(fovy, aspect, near, far) {
    const f = 1 / Math.tan(fovy * Math.PI / 360), d = near - far;
    return [f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) / d, -1, 0, 0, 2 * far * near / d, 0];
  },
  lookAt(eye, target, up = [0, 1, 0]) {
    const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
    const unit = (v) => v.map((c) => c / Math.hypot(...v));
    const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
    const z = unit(eye.map((c, i) => c - target[i])), x = unit(cross(up, z)), y = cross(z, x);
    return [x[0], y[0], z[0], 0, x[1], y[1], z[1], 0, x[2], y[2], z[2], 0, -dot(x, eye), -dot(y, eye), -dot(z, eye), 1];
  },
  multiply(a, b) {
    const out = new Array(16).fill(0);
    for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) for (let k = 0; k < 4; k++) out[c * 4 + r] += a[k * 4 + r] * b[c * 4 + k];
    return out;
  },
};
function sphere(rings, segments) {
  const vertices = [], indices = [];
  for (let i = 0; i <= rings; i++) for (let j = 0; j <= segments; j++) {
    const t = i / rings * Math.PI, p = j / segments * 2 * Math.PI;
    vertices.push(Math.sin(t) * Math.cos(p), Math.cos(t), -Math.sin(t) * Math.sin(p));
  }
  for (let i = 0; i < rings; i++) for (let j = 0; j < segments; j++) {
    const a = i * (segments + 1) + j, b = a + segments + 1;
    indices.push(a, b, a + 1, a + 1, b, b + 1);
  }
  return { vertices: new Float32Array(vertices), indices: new Uint16Array(indices) };
}
const phongModel = `uniform vec3 uLight, uEye;
vec3 phong(vec3 p, vec3 n) {                 // the Phong reflection model (the equation)
  vec3 l = normalize(uLight - p), v = normalize(uEye - p), r = reflect(-l, n);
  vec3 base = vec3(0.71, 0.27, 0.18);
  return base * 0.2 + base * max(dot(n, l), 0.0) + vec3(0.8) * pow(max(dot(r, v), 0.0), 24.0);
}`;
const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec3 aPosition;
uniform mat4 uViewProjection, uModel; out vec3 vPosition, vNormal, vColor;
${phongModel}
void main() {
  vPosition = (uModel * vec4(aPosition, 1.0)).xyz;
  vNormal = mat3(uModel) * aPosition;
  vColor = phong(vPosition, normalize(vNormal));          // Gouraud: evaluated at the vertices
  gl_Position = uViewProjection * vec4(vPosition, 1.0);
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision highp float; in vec3 vPosition, vNormal, vColor; uniform int uMode; out vec4 c;
${phongModel}
void main() {
  if (uMode == 0) {
    vec3 faceNormal = normalize(cross(dFdx(vPosition), dFdy(vPosition)));   // flat: one normal per triangle
    c = vec4(phong(vPosition, faceNormal), 1.0);
  } else if (uMode == 1) c = vec4(vColor, 1.0);
  else c = vec4(phong(vPosition, normalize(vNormal)), 1.0);
}`]]) {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
  gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
const u = (n) => gl.getUniformLocation(program, n);
const globe = sphere(9, 14);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, globe.vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, globe.indices, gl.STATIC_DRAW);
gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE); gl.enable(gl.SCISSOR_TEST);
const eye = [0, 0.3, 3];
gl.uniform3fv(u('uEye'), eye);
gl.uniform3f(u('uLight'), 1.5, 1.8, 2.5);
gl.uniformMatrix4fv(u('uViewProjection'), false, m4.multiply(m4.perspective(40, 1, 0.1, 10), m4.lookAt(eye, [0, 0, 0])));
function frame(ms) {
  const a = ms * 0.0005, c = Math.cos(a), s = Math.sin(a);
  gl.uniformMatrix4fv(u('uModel'), false, [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1]);   // slow spin
  for (let mode = 0; mode < 3; mode++) {
    gl.viewport(mode * 200, 0, 200, 200); gl.scissor(mode * 200 + 2, 0, 196, 200);
    gl.clearColor(0.93, 0.91, 0.87, 1);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.uniform1i(u('uMode'), mode);
    gl.drawElements(gl.TRIANGLES, globe.indices.length, gl.UNSIGNED_SHORT, 0);
  }
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>