PBR Materials
Physically Based Rendering models real-world light interaction: a surface scatters, reflects, and absorbs light based on metalness, roughness, and base color. Three.js’s MeshStandardMaterial (and MeshPhysicalMaterial for extras) gives you PBR out of the box once you supply the right textures and lighting.
PBR maps, env lighting, gloss-vs-rough
EXAMPLE
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// 1) Scene + renderer with correct color management
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping; // photographic feel
renderer.toneMappingExposure = 1.0;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 100);
camera.position.set(2, 1.5, 3);
new OrbitControls(camera, renderer.domElement);
// 2) Environment lighting (HDR) — without this, metals look black
new RGBELoader().load('/hdri/studio.hdr', (hdr) => {
hdr.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = hdr; // affects all PBR materials
scene.background = hdr; // optional — show the HDR
});
// 3) PBR material — the right defaults
const gold = new THREE.MeshStandardMaterial({
color: new THREE.Color(0xffd700),
metalness: 1.0,
roughness: 0.25,
});
const plastic = new THREE.MeshStandardMaterial({
color: new THREE.Color(0xff5577),
metalness: 0.0,
roughness: 0.45,
});
const rust = new THREE.MeshStandardMaterial({
color: new THREE.Color(0x8b4513),
metalness: 0.4,
roughness: 0.9,
});
// 4) PBR texture maps — textures must use SRGBColorSpace for color, LinearSRGBColorSpace for data
const tl = new THREE.TextureLoader();
function loadColor(url) {
const t = tl.load(url);
t.colorSpace = THREE.SRGBColorSpace;
return t;
}
function loadData(url) {
const t = tl.load(url);
t.colorSpace = THREE.NoColorSpace; // linear data
return t;
}
const brick = new THREE.MeshStandardMaterial({
map: loadColor('/tex/brick_basecolor.jpg'), // base color (sRGB)
normalMap: loadData('/tex/brick_normal.jpg'), // tangent-space normals
roughnessMap: loadData('/tex/brick_roughness.jpg'), // R or grayscale
metalnessMap: loadData('/tex/brick_metalness.jpg'),
aoMap: loadData('/tex/brick_ao.jpg'), // ambient occlusion
displacementMap: loadData('/tex/brick_displacement.jpg'),
displacementScale: 0.02,
});
// AO map needs a second UV channel — set it on the geometry:
geometry.setAttribute('uv2', geometry.attributes.uv);
// 5) MeshPhysicalMaterial — extras for tricky surfaces
const clearcoat = new THREE.MeshPhysicalMaterial({
color: 0xff0000,
metalness: 0.0,
roughness: 0.4,
clearcoat: 1.0, // car paint, lacquer
clearcoatRoughness: 0.05,
});
const glass = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
transmission: 1.0, // see-through
thickness: 0.5,
roughness: 0.0,
ior: 1.5, // index of refraction
metalness: 0.0,
transparent: true,
});
const velvet = new THREE.MeshPhysicalMaterial({
color: 0x4b0082,
sheen: 1.0,
sheenColor: new THREE.Color(0xffffff),
sheenRoughness: 0.5,
roughness: 0.7,
});
// 6) Lights — even with envMap, add a key light for shadows
const sun = new THREE.DirectionalLight(0xffffff, 1.0);
sun.position.set(5, 10, 7);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.near = 0.5;
sun.shadow.camera.far = 50;
scene.add(sun);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// 7) Loading a PBR-ready glTF model
new GLTFLoader().load('/models/helmet.glb', (gltf) => {
const helmet = gltf.scene;
helmet.traverse((obj) => {
if (obj.isMesh) {
obj.castShadow = true;
obj.receiveShadow = true;
}
});
scene.add(helmet);
});
// 8) Tuning the look (PBR cheat sheet)
// Bright clean metal → metalness 1.0, roughness 0.05-0.2
// Painted metal → metalness 0.0, clearcoat 1.0
// Plastic → metalness 0.0, roughness 0.3-0.7
// Wood (dry) → metalness 0.0, roughness 0.7-0.9
// Wet road → roughness 0.1-0.3, normalMap
// Old rusty surface → metalness 0.3-0.6, roughness 0.8-1.0
// 9) Performance tips
// • Reuse materials and textures — material variants are expensive
// • Bake AO and lightmaps for static scenes; use a smaller envMap intensity
// • Compress textures: KTX2 + BasisU, dramatically smaller and faster to upload
// • Use renderer.info to watch draw calls, triangles, geometries
// 10) Common bugs
// • Everything looks dark and matte → no scene.environment → metals can't reflect anything
// • Color textures look washed out / too bright → colorSpace not set to SRGBColorSpace
// • Normal maps look wrong → wrong format (DirectX vs OpenGL Y-flip)
// • Black model → metalness 1.0 + no env map = nothing to reflect
// • Banding in HDR → tone mapping disabled or exposure too high
// • aoMap has no effect → forgot to add uv2 attribute
Why it matters
PBR needs three things: correct color management (SRGBColorSpace for the renderer and color textures), an environment map for metals to reflect, and tone mapping that doesn’t crush highlights. Once those are in place, metalness and roughness are the two dials that take you most of the way to a believable surface.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// PBR — base color (map), metalness, roughness, normal, ao.
const mat = new THREE.MeshStandardMaterial({
map: colorTex, metalness: 0.2, roughness: 0.4,
});
Try it Yourself »
Discussion
Loading…