PBR vs Phong

PBR Versus the Chapter's Earlier Phong Model

Blinn-Phong and PBR compared
Blinn-Phong (Directional and Point Lighting-Specular Lighting and Fog) Metallic-roughness PBR
Parameters diffuse color, specular color, shininess base color, metallic, roughness
Energy not conserved conserved
Grazing angles no Fresnel brightening Schlick Fresnel
Metals faked with colored specular a physical parameter
Ambient a constant color image-based lighting
Cost a few instructions a few dozen, plus IBL lookups

Phong still suits stylized scenes, diagrams and very weak GPUs. PBR's advantage is predictability: a material authored in Blender 4,391 or Substance looks the same in Three.js 30,815 , Babylon.js 89,603 or your own shader, under any light.

Blinn-Phong (top) against metallic-roughness PBR (bottom): broad Phong highlights stay bright, PBR conserves energy and adds Fresnel rimsHTMLLive
<!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; font-family: monospace; font-size: 10px; }
  .names div { flex: 1; padding: 2px; }
</style>
<canvas id="c" width="600" height="300"></canvas>
<div class="names"><div>shininess 8 / roughness 0.7</div><div>shininess 32 / roughness 0.45</div><div>shininess 128 / roughness 0.25</div><div>shininess 512 / roughness 0.1</div></div>
<script>
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 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; out vec3 vNormal;
void main() { vNormal = aPosition; gl_Position = vec4(aPosition.xy * 0.85, -aPosition.z * 0.5, 1.0); }`],
  [gl.FRAGMENT_SHADER, `#version 300 es
precision highp float;
in vec3 vNormal; uniform bool uPBR; uniform float uShininess, uRoughness; uniform vec3 uLight; out vec4 c;
const float PI = 3.14159265;
void main() {
  vec3 n = normalize(vNormal), v = vec3(0, 0, 1), l = normalize(uLight), h = normalize(v + l);
  vec3 base = vec3(0.12, 0.37, 0.55);
  if (!uPBR) {                                    // Blinn-Phong: free exponent, constant ambient
    float spec = max(dot(n, l), 0.0) > 0.0 ? pow(max(dot(n, h), 0.0), uShininess) : 0.0;
    c = vec4(base * (0.25 + 0.75 * max(dot(n, l), 0.0)) + vec3(spec) * 0.8, 1.0);
    return;
  }
  vec3 albedo = pow(base, vec3(2.2));
  float r = uRoughness, nl = max(dot(n, l), 0.0), nv = max(dot(n, v), 1e-4), nh = max(dot(n, h), 0.0), a2 = pow(r, 4.0);
  float d = a2 / (PI * pow(nh * nh * (a2 - 1.0) + 1.0, 2.0)), k = (r + 1.0) * (r + 1.0) / 8.0;
  float g = nl / (nl * (1.0 - k) + k) * nv / (nv * (1.0 - k) + k);
  vec3 f0 = vec3(0.04), f = f0 + (1.0 - f0) * pow(1.0 - max(dot(h, v), 0.0), 5.0);
  vec3 color = ((1.0 - f) * albedo / PI + d * g * f / (4.0 * nv * max(nl, 1e-4))) * nl * 3.0;
  color += albedo * 0.35 + (f0 + (1.0 - f0) * pow(1.0 - nv, 5.0)) * 0.3 * (1.0 - r);   // ambient + Fresnel rim
  color = color / (1.0 + color);
  c = vec4(pow(color, vec3(1.0 / 2.2)), 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 ball = sphere(32, 48);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, ball.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, ball.indices, gl.STATIC_DRAW);
gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE);
const u = (n) => gl.getUniformLocation(program, n);
const shininess = [8, 32, 128, 512], roughness = [0.7, 0.45, 0.25, 0.1];
gl.clearColor(0.97, 0.96, 0.93, 1);
function frame(ms) {
  const a = 0.6 + Math.sin(ms * 0.0007) * 0.5;               // the light swings toward grazing
  gl.uniform3f(u('uLight'), Math.sin(a), 0.5, Math.cos(a));
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  for (let row = 0; row < 2; row++) for (let i = 0; i < 4; i++) {
    gl.viewport(i * 150 + 10, row === 0 ? 155 : 5, 140, 140);
    gl.uniform1i(u('uPBR'), row);
    gl.uniform1f(u('uShininess'), shininess[i]);
    gl.uniform1f(u('uRoughness'), roughness[i]);
    gl.drawElements(gl.TRIANGLES, ball.indices.length, gl.UNSIGNED_SHORT, 0);
  }
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>