PBR Authoring Tools

Tools for Authoring PBR Materials

Open-source and commercial tools for PBR materials
Tool Kind Licence and price (September 2026)
Blender 5.2 4,391 LTS modeling, painting, baking, glTF GPL, free
Material Maker 1.7 node-based procedural materials MIT, free
ArmorPaint PBR painting on 3D models open source, paid binaries
Adobe Substance 3D Painter, Designer painting, material graphs $59.99/month (Collection)
Poly Haven 82,537 , ambientCG textures, HDRI environments CC0, free

Blender bakes normal and occlusion maps and exports glTF materials; Material Maker (github.com/RodZill4/material-maker (https://github.com/RodZill4/material-maker 5,945 )) exports texture sets from node graphs. Substance dominates game studios; the CC0 libraries are the quickest route to realistic materials.

Four materials generated the way node-based tools build them (noise, masks, parameters), rendered with one PBR shaderHTMLLive
<!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; }
  table { max-width: 600px; width: 100%; border-collapse: collapse; margin-top: 4px; }
  td { padding: 2px 6px; border-bottom: 1px solid #e3ddd0; }
</style>
<canvas id="c" width="600" height="150"></canvas>
<div class="names"><div>walnut shelf</div><div>leather binding</div><div>gold foil</div><div>paper</div></div>
<table>
  <tr><td>Blender 5.2 LTS</td><td>modelling, painting, baking, glTF</td><td>GPL, free</td></tr>
  <tr><td>Material Maker 1.7</td><td>node-based procedural materials</td><td>MIT, free</td></tr>
  <tr><td>ArmorPaint</td><td>PBR painting on 3D models</td><td>open source, paid binaries</td></tr>
  <tr><td>Adobe Substance 3D Painter, Designer</td><td>painting, material graphs</td><td>subscription</td></tr>
  <tr><td>Poly Haven, ambientCG</td><td>textures, HDRI environments</td><td>CC0, free</td></tr>
</table>
<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; uniform float uTurn; out vec3 vNormal, vLocal;
void main() {
  float c = cos(uTurn), s = sin(uTurn);
  vLocal = aPosition; vNormal = mat3(c, 0.0, -s, 0.0, 1.0, 0.0, s, 0.0, c) * aPosition;
  gl_Position = vec4(vNormal.xy * 0.85, -vNormal.z * 0.5, 1.0);
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision highp float;
in vec3 vNormal, vLocal; uniform int uMaterial; out vec4 c;
const float PI = 3.14159265;
// Node-graph building blocks: value noise, fractal sum.
float hash(vec3 p) { return fract(sin(dot(p, vec3(12.9898, 78.233, 37.719))) * 43758.5453); }
float noise(vec3 p) {
  vec3 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
  return mix(mix(mix(hash(i), hash(i + vec3(1,0,0)), f.x), mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x), f.y),
             mix(mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x), mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x), f.y), f.z);
}
float fbm(vec3 p) { return noise(p) * 0.5 + noise(p * 2.0) * 0.25 + noise(p * 4.0) * 0.125 + noise(p * 8.0) * 0.0625; }
void main() {
  vec3 p = vLocal, n = normalize(vNormal), v = vec3(0, 0, 1), l = normalize(vec3(-0.5, 0.6, 0.7));
  vec3 albedo; float roughness, metallic;
  if (uMaterial == 0) {         // wood: rings from distance to an axis, warped by noise
    float rings = fract(length(p.xz * 6.0) + fbm(p * 3.0) * 1.5);
    albedo = mix(vec3(0.25, 0.13, 0.06), vec3(0.45, 0.26, 0.12), smoothstep(0.2, 0.8, rings)); roughness = 0.55; metallic = 0.0;
  } else if (uMaterial == 1) {  // leather: cellular-ish grain from high-frequency noise
    float grain = fbm(p * 30.0);
    albedo = vec3(0.35, 0.06, 0.05) * (0.8 + 0.4 * grain); roughness = 0.6 + 0.3 * grain; metallic = 0.0;
    n = normalize(n + (grain - 0.5) * 0.25);
  } else if (uMaterial == 2) {  // gold foil: metal with slightly uneven roughness
    albedo = vec3(1.0, 0.76, 0.33); roughness = 0.18 + 0.15 * fbm(p * 10.0); metallic = 1.0;
  } else {                      // paper: bright, rough, faint fibres
    albedo = vec3(0.93, 0.9, 0.82) * (0.92 + 0.08 * fbm(p * 40.0)); roughness = 0.9; metallic = 0.0;
  }
  albedo = pow(albedo, vec3(2.2));
  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), a2 = pow(roughness, 4.0);
  float d = a2 / (PI * pow(nh * nh * (a2 - 1.0) + 1.0, 2.0)), k = (roughness + 1.0) * (roughness + 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, metallic), f = f0 + (1.0 - f0) * pow(1.0 - max(dot(h, v), 0.0), 5.0);
  vec3 color = ((1.0 - f) * (1.0 - metallic) * albedo / PI + d * g * f / (4.0 * nv * max(nl, 1e-4))) * nl * 3.5;
  color += albedo * (1.0 - metallic) * 0.3 + f0 * 0.35 * (1.0 - roughness * 0.5);
  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(40, 60);
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);
gl.clearColor(0.97, 0.96, 0.93, 1);
function frame(ms) {
  gl.uniform1f(gl.getUniformLocation(program, 'uTurn'), ms * 0.0004);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  for (let i = 0; i < 4; i++) {
    gl.uniform1i(gl.getUniformLocation(program, 'uMaterial'), i);
    gl.viewport(i * 150 + 5, 5, 140, 140);
    gl.drawElements(gl.TRIANGLES, ball.indices.length, gl.UNSIGNED_SHORT, 0);
  }
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>