Physically-Based Rendering

Simulating real-world light behavior with roughness, metalness, and environment maps

What is PBR?

Physically-based rendering (PBR) is a shading approach that models how light actually behaves in the real world. Instead of artistic knobs like "shininess" and "specular color," PBR uses measurable physical properties, roughness, metalness, and index of refraction, to produce materials that look correct under any lighting condition.

Three.js provides two PBR materials: MeshStandardMaterial (the everyday workhorse) and MeshPhysicalMaterial (an extended version with advanced features). This article digs deeper into how PBR works and how to get the most out of it.

Energy Conservation

The core principle of PBR is energy conservation: a surface can't reflect more light than it receives. When light hits a surface, it's either reflected (specular) or absorbed and re-emitted (diffuse). The more a surface reflects, the less it scatters diffusely. PBR materials enforce this automatically, in that you don't need to manually balance diffuse and specular like you do with Phong shading.

The row above shows spheres with increasing roughness from left to right. As roughness increases, specular reflections spread out and become dimmer, while diffuse shading becomes more prominent, but the total light energy stays constant.

typescript
// Energy conservation happens automatically in PBR
// As roughness increases, specular gets broader and dimmer
// while diffuse becomes more visible

const smooth = new THREE.MeshStandardMaterial({
  color: 0xcccccc,
  roughness: 0.0, // Mirror-like, almost all specular
  metalness: 0.0
});

const rough = new THREE.MeshStandardMaterial({
  color: 0xcccccc,
  roughness: 1.0, // Fully diffuse, no visible specular
  metalness: 0.0
});

The Roughness-Metalness Model

PBR in Three.js uses the metallic-roughness workflow, defined by two parameters:

  • roughness (0–1): How smooth or rough the microsurface is. 0 is a perfect mirror; 1 is completely diffuse.
  • metalness (0–1): Whether the material is a dielectric (0, like plastic, wood, skin) or a metal (1, like gold, steel, copper).

Metals and dielectrics behave fundamentally differently. Metals absorb almost all refracted light, so they have no diffuse component. Their color comes entirely from tinted reflections. Dielectrics reflect a small amount of white light at grazing angles (Fresnel effect) and scatter the rest as diffuse color.

The grid above maps roughness (left to right) against metalness (bottom to top). The bottom-left sphere is a smooth dielectric (like polished plastic). The top-right is a rough metal (like brushed steel). The top-left is a smooth metal (like chrome).

typescript
// Dielectric: colored diffuse + white specular reflections
const plastic = new THREE.MeshStandardMaterial({
  color: 0xcc4444,
  roughness: 0.3,
  metalness: 0.0  // No metallic behavior
});

// Metal: no diffuse, colored specular reflections
const gold = new THREE.MeshStandardMaterial({
  color: 0xffcc44,  // This tints the reflections, not diffuse
  roughness: 0.2,
  metalness: 1.0   // Fully metallic
});

// In-between: rarely physically correct, but can be useful
const coated = new THREE.MeshStandardMaterial({
  color: 0x88aacc,
  roughness: 0.4,
  metalness: 0.5
});

The Fresnel Effect

All surfaces become more reflective at grazing angles. Look at a table from a steep angle and it reflects the room. This is the Fresnel effect, and PBR handles it automatically. For dielectrics, reflections are strongest at edges. For metals, the entire surface is reflective, but the color shifts at grazing angles.

The large sphere above uses a low roughness dielectric material. Notice how the edges appear brighter and more reflective than the center. That's the Fresnel effect in action! The flat plane behind it also shows Fresnel reflections at a shallow viewing angle.

typescript
// Fresnel is automatic in PBR, no special setup needed
// It's most visible on smooth dielectric surfaces

const glossyPlastic = new THREE.MeshStandardMaterial({
  color: 0x222244,
  roughness: 0.05,
  metalness: 0.0
});

// On metals, Fresnel shifts the reflection color
// rather than adding white reflections
const copper = new THREE.MeshStandardMaterial({
  color: 0xcc6633,
  roughness: 0.15,
  metalness: 1.0
});

Environment Maps

PBR materials look flat without something to reflect. In the real world, objects reflect their surroundings. In Three.js, an environment map provides this ambient reflection and lighting. You can set it on individual materials or on the entire scene.

The demo above shows the same set of spheres with a procedural environment map applied to the scene. Smooth metals now show clear reflections, and even rough surfaces pick up subtle ambient color from the environment.

typescript
// Create a simple procedural environment map using PMREMGenerator
const pmremGenerator = new THREE.PMREMGenerator(renderer);

// Option 1: Use a solid color environment
const envScene = new THREE.Scene();
envScene.background = new THREE.Color(0x88aacc);
const envMap = pmremGenerator.fromScene(envScene).texture;

// Apply to the entire scene
scene.environment = envMap;

// Or apply to individual materials
material.envMap = envMap;
material.envMapIntensity = 1.0; // Scale the reflection strength

pmremGenerator.dispose();

MeshPhysicalMaterial: Beyond Standard PBR

MeshPhysicalMaterial extends the standard metallic-roughness model with several advanced features. Each adds realism at a GPU cost, so use them selectively.

Clearcoat

A clearcoat adds a second, independent reflective layer on top of the base material. Just like the lacquer on a car, the varnish on wood, or the glossy finish on a phone case. The clearcoat has its own roughness, separate from the base layer.

typescript
// Car paint: rough base color + smooth clearcoat
const carPaint = new THREE.MeshPhysicalMaterial({
  color: 0xcc0022,
  roughness: 0.6,    // Base layer is fairly rough
  metalness: 0.0,
  clearcoat: 1.0,          // Full clearcoat
  clearcoatRoughness: 0.05 // Very smooth top layer
});

// Carbon fiber: dark rough base + glossy clear layer
const carbonFiber = new THREE.MeshPhysicalMaterial({
  color: 0x222222,
  roughness: 0.8,
  metalness: 0.0,
  clearcoat: 0.8,
  clearcoatRoughness: 0.1
});

Transmission and Refraction

transmission makes light pass through the material instead of being reflected or absorbed which is essential for glass, water, crystals, and translucent plastics. Combined with ior (index of refraction) and thickness, you can control how much light bends and how deep the object appears.

typescript
// Clear glass
const glass = new THREE.MeshPhysicalMaterial({
  color: 0xffffff,
  roughness: 0.0,
  metalness: 0.0,
  transmission: 1.0,  // Fully transparent
  thickness: 1.0,     // Depth for refraction calculation
  ior: 1.5            // Glass has IOR ~1.5
});

// Frosted glass
const frosted = new THREE.MeshPhysicalMaterial({
  color: 0xffffff,
  roughness: 0.4,     // Roughness blurs the transmission
  metalness: 0.0,
  transmission: 1.0,
  thickness: 0.5,
  ior: 1.5
});

// Tinted water
const water = new THREE.MeshPhysicalMaterial({
  color: 0x88ccff,    // Color tints what you see through it
  roughness: 0.0,
  metalness: 0.0,
  transmission: 0.95,
  thickness: 2.0,
  ior: 1.33           // Water has IOR ~1.33
});

Iridescence

iridescence simulates thin-film interference. You know, like the rainbow sheen on soap bubbles, oil slicks, beetle shells, etc. The effect shifts color based on viewing angle and is controlled by iridescenceIOR and an optional thickness range.

typescript
// Soap bubble
const bubble = new THREE.MeshPhysicalMaterial({
  color: 0xffffff,
  roughness: 0.0,
  metalness: 0.0,
  transmission: 0.9,
  thickness: 0.1,
  ior: 1.3,
  iridescence: 1.0,
  iridescenceIOR: 1.3,
  iridescenceThicknessRange: [100, 400]
});

// Oil slick on metal
const oilSlick = new THREE.MeshPhysicalMaterial({
  color: 0x333333,
  roughness: 0.1,
  metalness: 0.9,
  iridescence: 1.0,
  iridescenceIOR: 1.5,
  iridescenceThicknessRange: [200, 600]
});

Sheen

sheen adds a soft glow at grazing angles, simulating fabrics like velvet, satin, or microfiber. Unlike specular highlights, sheen produces a broad, soft halo rather than a sharp reflection.

typescript
// Velvet fabric
const velvet = new THREE.MeshPhysicalMaterial({
  color: 0x880022,
  roughness: 1.0,
  metalness: 0.0,
  sheen: 1.0,
  sheenRoughness: 0.5,
  sheenColor: new THREE.Color(0xff4466)
});

// Satin
const satin = new THREE.MeshPhysicalMaterial({
  color: 0x224488,
  roughness: 0.8,
  metalness: 0.0,
  sheen: 0.8,
  sheenRoughness: 0.3,
  sheenColor: new THREE.Color(0x6688cc)
});

Putting It All Together

The final demo combines several PBR techniques in one scene: metals, dielectrics, glass, clearcoat, and iridescence. They are all lit by the same environment. This shows how PBR materials respond consistently to the same lighting, producing a coherent, believable scene.

From left to right: gold metal, glossy red clearcoat, clear glass, iridescent sphere, and velvet fabric. All of these are using MeshPhysicalMaterial with different property combinations.

typescript
// Gold metal
const gold = new THREE.MeshPhysicalMaterial({
  color: 0xffcc44,
  roughness: 0.15,
  metalness: 1.0
});

// Red clearcoat (car paint)
const carPaint = new THREE.MeshPhysicalMaterial({
  color: 0xcc0022,
  roughness: 0.5,
  metalness: 0.0,
  clearcoat: 1.0,
  clearcoatRoughness: 0.05
});

// Clear glass
const glass = new THREE.MeshPhysicalMaterial({
  color: 0xffffff,
  roughness: 0.0,
  metalness: 0.0,
  transmission: 1.0,
  thickness: 1.5,
  ior: 1.5
});

// Iridescent metal
const iridescent = new THREE.MeshPhysicalMaterial({
  color: 0x888888,
  roughness: 0.1,
  metalness: 0.8,
  iridescence: 1.0,
  iridescenceIOR: 1.5,
  iridescenceThicknessRange: [200, 600]
});

// Velvet fabric
const velvet = new THREE.MeshPhysicalMaterial({
  color: 0x880022,
  roughness: 1.0,
  metalness: 0.0,
  sheen: 1.0,
  sheenRoughness: 0.5,
  sheenColor: new THREE.Color(0xff4466)
});

// Apply a procedural environment map for reflections
const pmremGenerator = new THREE.PMREMGenerator(renderer);
const envScene = new THREE.Scene();
envScene.background = new THREE.Color(0x88aacc);
scene.environment = pmremGenerator.fromScene(envScene).texture;
pmremGenerator.dispose();
←   The Spectrum of MaterialsAdvanced Mapping Concepts   →