| 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.
<!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>