Metallic-Roughness

The Metallic-Roughness PBR Model

PBR describes what a material is, not how its highlight should look. The metallic-roughness model, from Disney's 2012 BRDF and Epic's Unreal Engine 4 133 , is glTF 2.0's core material:

The glTF 2.0 metallic-roughness material parameters
Parameter Range Meaning
Base color sRGB color diffuse color of dielectrics, reflection color of metals
Metallic 0 or 1 (in between only at edges) conductor or insulator
Roughness 0 (mirror) to 1 (chalk) spread of the microfacet normals
Normal, occlusion, emissive maps relief, ambient shadowing, glow

Two rules drive it. Energy conservation: a broader highlight is a dimmer one, which Phong's free exponent ignores. Fresnel: everything reflects more at grazing angles; head-on, insulators reflect about 4% (F0 = 0.04) whatever their color, while metals reflect far more, tinted by their base color, with no diffuse part. The math runs in linear light: decode sRGB colors, then tone-map and encode the result (sRGB).

What a material is: dielectrics (4% reflectance, coloured diffuse) against metals (tinted reflection, no diffuse)HTMLLive
<!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>paint / gold</div><div>cover cloth / copper</div><div>paper / silver</div><div>plastic / iron</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.82, -aPosition.z * 0.5, 1.0); }`],
  [gl.FRAGMENT_SHADER, `#version 300 es
precision highp float;
in vec3 vNormal; uniform vec3 uBaseColor; uniform float uMetallic, uRoughness; out vec4 c;
const float PI = 3.14159265;
void main() {
  vec3 n = normalize(vNormal), v = vec3(0.0, 0.0, 1.0), l = normalize(vec3(-0.5, 0.6, 0.7));
  vec3 albedo = pow(uBaseColor, vec3(2.2));             // sRGB colour -> linear light
  vec3 h = normalize(v + l);
  float nl = max(dot(n, l), 0.0), nv = max(dot(n, v), 1e-4), nh = max(dot(n, h), 0.0);
  float a2 = pow(uRoughness, 4.0);
  float d = a2 / (PI * pow(nh * nh * (a2 - 1.0) + 1.0, 2.0));
  float k = (uRoughness + 1.0) * (uRoughness + 1.0) / 8.0;
  float g = nl / (nl * (1.0 - k) + k) * nv / (nv * (1.0 - k) + k);
  vec3 f0 = mix(vec3(0.04), albedo, uMetallic);          // 4% for insulators, the base colour for metals
  vec3 f = f0 + (1.0 - f0) * pow(1.0 - max(dot(h, v), 0.0), 5.0);
  vec3 specular = d * g * f / (4.0 * nv * max(nl, 1e-4));
  vec3 diffuse = (1.0 - f) * (1.0 - uMetallic) * albedo / PI;   // metals have no diffuse part
  vec3 color = (diffuse + specular) * nl * 3.5;
  // A soft sky from every direction: Fresnel makes every rim reflect more (a rough ambient term).
  vec3 rimF = f0 + (1.0 - f0) * pow(1.0 - nv, 5.0);
  color += vec3(0.35, 0.38, 0.45) * (rimF * (1.0 - uRoughness * 0.6) + (1.0 - uMetallic) * albedo * 0.4);
  color = color / (1.0 + color);                         // tone map
  c = vec4(pow(color, vec3(1.0 / 2.2)), 1.0);            // linear -> sRGB
}`]]) {
  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 materials = [   // [base colour (sRGB), metallic, roughness]
  [[0.88, 0.6, 0.06], 0, 0.3], [[0.12, 0.37, 0.55], 0, 0.8], [[0.95, 0.93, 0.88], 0, 0.9], [[0.71, 0.27, 0.18], 0, 0.2],
  [[1.0, 0.78, 0.34], 1, 0.3], [[0.95, 0.64, 0.54], 1, 0.35], [[0.97, 0.96, 0.91], 1, 0.25], [[0.56, 0.57, 0.58], 1, 0.5]];
gl.clearColor(0.97, 0.96, 0.93, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
materials.forEach(([color, metallic, roughness], i) => {
  gl.viewport((i % 4) * 150 + 10, i < 4 ? 155 : 5, 140, 140);   // top row dielectrics, bottom row metals
  gl.uniform3fv(u('uBaseColor'), color);
  gl.uniform1f(u('uMetallic'), metallic);
  gl.uniform1f(u('uRoughness'), roughness);
  gl.drawElements(gl.TRIANGLES, ball.indices.length, gl.UNSIGNED_SHORT, 0);
});
</script>