🎨 Computer Graphics · Shaders
📅 Березень 2026 ⏱ ≈ 11 хв читання 🟡 Середній

Physically Based Rendering (PBR)

PBR is the standard shading model for modern real-time graphics. Instead of arbitrary "diffuse + specular + shininess" sliders, it models light the way physics does — microfacets, Fresnel reflectance, and energy conservation — using just metalness and roughness as inputs.

1. The Rendering Equation

James Kajiya (1986) formulated the master equation that every physically correct renderer must satisfy:

Rendering Equation (Kajiya 1986) L_o(x, ωo) = L_e(x, ωo) + ∫_Ω f_r(x, ωi, ωo) · L_i(x, ωi) · (ωi · n̂) dωi

L_o — outgoing radiance in direction ωo
L_e — emitted radiance
f_r — BRDF: bidirectional reflectance distribution function
L_i — incoming radiance from direction ωi
ωi · n̂ = cos θ (Lambert cosine term)

Solving this integral exactly requires path tracing. Real-time PBR approximates it with: (1) direct punctual lights, and (2) precomputed environment maps for the diffuse and specular integrals.

2. Microfacet Theory

At a microscopic level, no surface is a perfect mirror. Every surface is a collection of tiny flat microfacets, each a perfect mirror, but randomly oriented. The distribution of microfacet orientations defines the macroscopic appearance.

A rough surface has many facets pointing in random directions → large, diffuse-looking highlights. A smooth surface has facets nearly all aligned with the surface normal → tiny, sharp specular "hotspots".

Roughness α² = roughness², where roughness ∈ [0, 1] is the artist input. Two effects reduce the visible reflecting facets:

3. The Cook-Torrance BRDF Terms

The microfacet specular BRDF (Cook-Torrance) has three terms:

Cook-Torrance Specular BRDF f_cook = D(h) · G(l, v) · F(v, h) / (4 (n·l)(n·v))

D — Normal Distribution Function (how many facets face h)
G — Geometry/Masking term (self-shadowing)
F — Fresnel term (reflectance vs angle)
D — GGX / Trowbridge-Reitz

D(h, α) = α² / (π · ((n·h)²(α²−1) + 1)²). Most popular: looks great, fast to sample.

G — Smith / Schlick-GGX

G(n,v,k) = (n·v) / ((n·v)(1−k)+k), k = α²/2 (direct light). Applied separately for view + light dirs.

F — Fresnel-Schlick

F = F₀ + (1 − F₀)(1 − cosθ)⁵. Simple, accurate. F₀ is reflectance at normal incidence.

The full BRDF = specular Cook-Torrance + diffuse Lambert, weighted by the Fresnel term: diffuse contribution is (1 − F) × albedo / π. Metals absorb all refracted light → no diffuse term.

4. Fresnel and F₀

F₀ is the reflectance at normal incidence (0° angle of incidence). This is the "base reflectance" of the surface:

At glancing angles (near 90°), everything is highly reflective — you can see sky reflections on a flat lake even though water has low F₀. This is the Fresnel effect, and the (1 − cosθ)⁵ power law is a remarkably good approximation for most materials.

F₀ from IOR: For dielectrics, F₀ = ((n−1)/(n+1))². Glass with IOR = 1.5 → F₀ = ((1.5-1)/(1.5+1))² = 0.04 (4% at normal incidence).

5. Metalness–Roughness Workflow

Artists use three texture maps. This is the "metalness workflow" used by Three.js, Unreal Engine, Unity, Blender, and glTF:

Additional optional maps: Normal, Ambient Occlusion, Emissive, Height.

Specular–Glossiness workflow: Older alternative used by some Blinn–Phong-era pipelines (Specular stored explicitly as RGB). Less artist-friendly and more prone to non-energy-conserving values. MetalnessRoughness is now the industry standard (glTF 2.0 core).

6. Image-Based Lighting (IBL)

Punctual lights (point, directional, spot) only cover a fraction of real-world lighting. IBL uses an HDR environment map as an infinite sphere of area lights. The rendering equation integral is split:

All three textures are computed once offline. Real-time IBL from static environments is essentially "free" in terms of per-frame cost.

7. GLSL PBR Shader

Core Cook-Torrance BRDF functions in GLSL ES 3.0:

const float PI = 3.14159265359;

// GGX Normal Distribution Function
float D_GGX(float NdotH, float roughness) {
  float a  = roughness * roughness;
  float a2 = a * a;
  float d  = (NdotH * NdotH) * (a2 - 1.0) + 1.0;
  return a2 / (PI * d * d);
}

// Smith-Schlick Geometry Function
float G_SchlickGGX(float NdotV, float roughness) {
  float r = roughness + 1.0;
  float k = (r * r) / 8.0;
  return NdotV / (NdotV * (1.0 - k) + k);
}
float G_Smith(float NdotV, float NdotL, float roughness) {
  return G_SchlickGGX(NdotV, roughness)
       * G_SchlickGGX(NdotL, roughness);
}

// Fresnel-Schlick
vec3 F_Schlick(float cosTheta, vec3 F0) {
  return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

// Full Cook-Torrance BRDF (single directional light)
vec3 PBR(vec3 N, vec3 V, vec3 L,
         vec3 albedo, float roughness, float metalness) {

  vec3 H = normalize(V + L);
  float NdotV = max(dot(N, V), 0.0001);
  float NdotL = max(dot(N, L), 0.0);
  float NdotH = max(dot(N, H), 0.0);
  float VdotH = max(dot(V, H), 0.0);

  // F0: dielectric = 0.04, metal = albedo
  vec3 F0 = mix(vec3(0.04), albedo, metalness);

  float D = D_GGX(NdotH, roughness);
  float G = G_Smith(NdotV, NdotL, roughness);
  vec3  F = F_Schlick(VdotH, F0);

  vec3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001);

  vec3 kD = (1.0 - F) * (1.0 - metalness);  // metals: no diffuse
  vec3 diffuse = kD * albedo / PI;

  return (diffuse + specular) * NdotL;
}
Three.js MeshStandardMaterial uses exactly this PBR model. You can mix your own shader with THREE.ShaderMaterial and include Three's built-in GLSL chunks via #include <lights_pars_begin> to get scene lights automatically.