# PBR Materials

> Physically based rendering describes surfaces with base colour, roughness and metalness, so one material looks right under any lighting and environment.

- Category: Three.js & Rendering › Materials & Light
- Level: common
- Tags: pbr, materials, roughness, metalness, fresnel, ibl
- Updated: 2026-11-01
- URL: https://gdcs.coody.app/threejs/materials/pbr-materials

## What it is

**Physically based rendering (PBR)** separates *what a surface is* from *how it is lit*. Instead of tweaking a highlight colour per scene, you describe the material with a few physical parameters, and a lighting model that conserves energy turns them into pixels. The same asset then looks right in a sunny level, a dark cave and a studio turntable.

The common **metal/rough** workflow (glTF 2.0, Three.js, Unity, Unreal) uses three main inputs. **Base colour** (albedo) is the diffuse colour for non-metals and the reflection tint for metals; it contains no lighting or shadows. **Metalness** is 0 for dielectrics (plastic, wood, skin, stone) and 1 for bare metal; values in between are only for transitions such as rust or dust. **Roughness** spreads reflections: 0 is a mirror, 1 is chalk.

Every surface reflects more at grazing angles (**Fresnel**). The head-on reflectance **F0** is about 4% for dielectrics and equals the base colour for metals, so the shader derives it as `mix(0.04, baseColor, metalness)`. **Energy conservation** means light that is reflected specularly is not also scattered diffusely, and metals have no diffuse part at all.

In Three.js, `MeshStandardMaterial` implements this model; `MeshPhysicalMaterial` adds clearcoat, transmission, sheen and iridescence. PBR needs **environment lighting** (`scene.environment`) to look right, because a smooth metal only reflects its surroundings. A procedural `RoomEnvironment` run through `PMREMGenerator` gives good image-based lighting without downloading an HDR file.

## Key formulas

- **Metal/rough F0:** $F_0 = \operatorname{lerp}(0.04,\; \textit{baseColor},\; m)$
- **Fresnel (Schlick):** $\textcolor{#f5d547}{F} = F_0 + (1 - F_0)\,(1 - \hat v \cdot \hat h)^5$
- **Energy split:** $k_d = (1 - \textcolor{#f5d547}{F})(1 - m), \qquad L_o = \left(k_d \frac{\textit{baseColor}}{\pi} + f_{spec}\right) L_i \,(\textcolor{#3ee09a}{\hat n}\cdot \hat l)$ — m = metalness. Metals (m = 1) have no diffuse term at all.
- **Cook–Torrance specular:** $f_{spec} = \frac{D(\alpha)\, \textcolor{#f5d547}{F}\, G}{4\,(\textcolor{#3ee09a}{\hat n}\cdot\hat v)(\textcolor{#3ee09a}{\hat n}\cdot\hat l)}, \quad \alpha = \textit{roughness}^2$ — D: microfacet distribution (GGX), G: self-shadowing.

## Rules of thumb

- Base colour stays within about sRGB 30–240 for non-metals (charcoal ≈ 50, fresh snow ≈ 240). Pure black or white albedo doesn't occur in nature.
- Metalness is 0 or 1 for almost every pixel. Use in-between values only in thin transition areas.
- Raw metal base colours are bright, around sRGB 180–255 (gold ≈ (255, 195, 86), iron ≈ (196, 199, 199)).
- Keep roughness ≥ 0.04. A perfect mirror leaves only aliased, pin-point highlights.
- Pack textures the glTF way: roughness in G, metalness in B of one `metalnessRoughnessMap`; base colour textures are sRGB, data maps are linear.

## Implementation

Procedural environment lighting + PBR materials in Three.js (no downloads):

```ts
import * as THREE from "three"
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js"

renderer.toneMapping = THREE.ACESFilmicToneMapping
const pmrem = new THREE.PMREMGenerator(renderer)
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture
pmrem.dispose()

const gold = new THREE.MeshStandardMaterial({
  color: 0xf3c35b, metalness: 1, roughness: 0.25,
})
const carPaint = new THREE.MeshPhysicalMaterial({
  color: 0xc8312a, metalness: 0, roughness: 0.5,
  clearcoat: 1, clearcoatRoughness: 0.05,     // glossy lacquer over a rough base
})
const glass = new THREE.MeshPhysicalMaterial({
  roughness: 0, transmission: 1, ior: 1.5, thickness: 0.5,
})
```

The core of a metal/rough shader:

```glsl
vec3  F0 = mix(vec3(0.04), baseColor, metalness);
float a  = roughness * roughness;
vec3  F  = F0 + (1.0 - F0) * pow(1.0 - max(dot(V, H), 0.0), 5.0);
vec3  kd = (1.0 - F) * (1.0 - metalness);          // energy conservation
vec3  spec = D_GGX(N, H, a) * G_Smith(N, V, L, a) * F
           / max(4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0), 1e-4);
vec3  Lo = (kd * baseColor / PI + spec) * lightColor * max(dot(N, L), 0.0);
```

## Common mistakes

- No `scene.environment`. Metals render almost black because there is nothing for them to reflect.
- Baking lighting, shadows or ambient occlusion into the base colour texture. Put AO in `aoMap` and keep albedo flat.
- Loading roughness or metalness maps as sRGB. They are linear data, and gamma-decoding them skews every value.
- Mid-grey metalness (0.5) everywhere to get a "semi-metal" look. It is physically meaningless; vary roughness instead.
- Fetching an HDR from a CDN at runtime. It breaks offline and under strict CSP; generate or bundle the environment.

## Interactive visual

Left grid: roughness grows to the right and metalness grows upward, all lit by a procedural room environment that slowly turns. The white ring marks the cell nearest the sliders, and the large sphere on the right is the exact selected material. Turn on clear coat to add a glossy lacquer layer.

## Prompt

```text
Set up physically based materials in a Three.js scene with TypeScript, without loading any remote files. Generate image-based lighting from `RoomEnvironment` (three/examples/jsm/environments) through `PMREMGenerator.fromScene(room, 0.04)` and assign it to `scene.environment`; dispose the generator and texture on teardown. Set `renderer.toneMapping = THREE.ACESFilmicToneMapping` and keep output colour space sRGB. Build a 5×5 grid of spheres with `MeshStandardMaterial`, roughness 0→1 along x and metalness 0→1 along y, all sharing one geometry, and one large `MeshPhysicalMaterial` sphere driven by UI sliders for roughness, metalness and clearcoat. Clamp roughness to at least 0.04. Also export a pure function `f0(baseColor, metalness)` = lerp(0.04, baseColor, metalness) per channel, with a unit test for metalness 0 and 1.
```

## Related

- [Reflection and Refraction](https://gdcs.coody.app/threejs/materials/reflection-and-refraction.md) — Light hitting a surface partly reflects and partly bends through. Snell's law sets the bend, Fresnel the split; past the critical angle all of it reflects.
- [Surface Normals](https://gdcs.coody.app/threejs/rendering/normals.md) — A normal is a unit vector perpendicular to a surface. Lighting, slope checks, bounces and decals all start from it, so it must be right.
- [Fragment Shaders](https://gdcs.coody.app/threejs/rendering/fragment-shader.md) — A fragment shader computes the colour of each pixel a triangle covers. Feed it UVs, normals and a time uniform and it paints anything.
- [Texture Atlas](https://gdcs.coody.app/performance/gpu/texture-atlas.md) — Pack many sprites into one texture and address each by its UV rectangle, so objects share a material and batch into few draw calls. Pad to stop bleeding.
- [Lerp and Smoothstep](https://gdcs.coody.app/mathematics/interpolation/lerp-and-smoothstep.md) — Lerp blends two values by t, inverse lerp finds t, remap chains both. Smoothstep softens the ends, and exponential damping chases a target at any frame rate.

## Further reading

- [Khronos — glTF 2.0 specification (metallic-roughness material)](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html)
- [LearnOpenGL — PBR theory](https://learnopengl.com/PBR/Theory)
- [Three.js docs — MeshStandardMaterial](https://threejs.org/docs/#api/en/materials/MeshStandardMaterial)

---
Source: Game Development Cheat Sheet (https://gdcs.coody.app)
