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