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);
}