Physically Based Rendering & IBL
Energy conservation, microfacet theory, Fresnel at grazing angles, metallic/roughness workflow (glTF 2.0, Unreal, Unity URP/HDRP): albedo, metallic, roughness, ao, normal, emissive.
29.1 Principles
Energy conservation, microfacet theory, Fresnel at grazing angles, metallic/roughness workflow (glTF 2.0, Unreal, Unity URP/HDRP): albedo, metallic, roughness, ao, normal, emissive. Dielectric F0 ≈ 0.04; metals use the albedo as F0 and have no diffuse.
29.2 Cook–Torrance BRDF
f = kd · albedo/π + D·F·G / (4 (N·L)(N·V))
static const float PI = 3.14159265359;
float D_GGX(float NdotH, float roughness)
{
float a = roughness * roughness; // perceptual roughness → alpha
float a2 = a * a;
float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
return a2 / (PI * d * d);
}
float V_SmithGGXCorrelated(float NdotV, float NdotL, float roughness)
{
float a2 = pow(roughness, 4.0);
float gv = NdotL * sqrt(NdotV * NdotV * (1.0 - a2) + a2);
float gl = NdotV * sqrt(NdotL * NdotL * (1.0 - a2) + a2);
return 0.5 / max(gv + gl, 1e-5); // includes the 1/(4 NdotL NdotV) term
}
float3 F_Schlick(float VdotH, float3 F0)
{
return F0 + (1.0 - F0) * pow(1.0 - VdotH, 5.0);
}
float3 CookTorrance(float3 N, float3 V, float3 L, float3 albedo, float metallic, float roughness)
{
float3 H = normalize(V + L);
float NdotL = saturate(dot(N, L));
float NdotV = max(dot(N, V), 1e-4);
float NdotH = saturate(dot(N, H));
float VdotH = saturate(dot(V, H));
float3 F0 = lerp(0.04, albedo, metallic);
float3 F = F_Schlick(VdotH, F0);
float D = D_GGX(NdotH, roughness);
float Vis = V_SmithGGXCorrelated(NdotV, NdotL, roughness);
float3 specular = D * Vis * F;
…29.3 Image-based lighting (split-sum approximation)
Precompute (usually in compute shaders):
Irradiance map (diffuse): cosine-convolved environment, or 9 spherical-harmonic coefficients.
Prefiltered specular map: GGX-convolved cube map, one roughness per mip.
BRDF LUT: 2-D texture of (scale, bias) for F0 indexed by (N·V, roughness).
Importance sampling for prefiltering uses the Hammersley sequence:
Extensions: multiple-scattering energy compensation, clear coat, sheen, anisotropy, subsurface scattering, transmission (glTF extensions; Disney/OpenPBR models).
TextureCube<float3> gIrradiance : register(t10);
TextureCube<float3> gPrefiltered : register(t11);
Texture2D<float2> gBRDFLUT : register(t12);
SamplerState gLinearClampIBL : register(s3);
cbuffer IBL : register(b5) { float gPrefilteredMips; float gIBLIntensity; };
float3 F_SchlickRoughness(float NdotV, float3 F0, float roughness)
{
return F0 + (max(1.0 - roughness, F0) - F0) * pow(1.0 - NdotV, 5.0);
}
float3 AmbientIBL(float3 N, float3 V, float3 albedo, float metallic, float roughness, float ao)
{
float NdotV = max(dot(N, V), 1e-4);
float3 F0 = lerp(0.04, albedo, metallic);
float3 F = F_SchlickRoughness(NdotV, F0, roughness);
float3 kd = (1.0 - F) * (1.0 - metallic);
float3 irradiance = gIrradiance.SampleLevel(gLinearClampIBL, N, 0);
float3 R = reflect(-V, N);
float3 prefiltered = gPrefiltered.SampleLevel(gLinearClampIBL, R, roughness * (gPrefilteredMips - 1));
float2 brdf = gBRDFLUT.SampleLevel(gLinearClampIBL, float2(NdotV, roughness), 0);
float3 diffuse = irradiance * albedo;
float3 specular = prefiltered * (F0 * brdf.x + brdf.y);
return (kd * diffuse + specular) * ao * gIBLIntensity;
}float2 Hammersley(uint i, uint n) { return float2(float(i) / n, reversebits(i) * 2.3283064365386963e-10); }
float3 ImportanceSampleGGX(float2 Xi, float3 N, float roughness)
{
float a = roughness * roughness;
float phi = 2 * PI * Xi.x;
float cosT = sqrt((1 - Xi.y) / (1 + (a * a - 1) * Xi.y));
float sinT = sqrt(1 - cosT * cosT);
float3 H = float3(sinT * cos(phi), sinT * sin(phi), cosT);
float3 up = abs(N.z) < 0.999 ? float3(0, 0, 1) : float3(1, 0, 0);
float3 T = normalize(cross(up, N));
float3 B = cross(N, T);
return normalize(T * H.x + B * H.y + N * H.z);
}