PBR 从公式到参数:Cook-Torrance 与金属度/粗糙度工作流

Unity 知识 · Shader 区域 · 光照与材质 · 第 3 篇 更新于 2026-10-15

上一篇把 D、G、F 三项推了出来,这篇把它们写成可跑的代码,再回答两个实际问题:材质面板上的金属度/粗糙度在物理上到底是什么,以及「能量守恒去哪了」。

直接光 Cook-Torrance:完整片段

输入:N、V、L(归一化)、albedo、metallic、roughness、光源色。三项公式的出处见 /notes/unity/lighting-2/。

const float PI = 3.14159265;

float D_GGX(float ndh, float a) {
    float a2 = a * a;
    float d  = ndh * ndh * (a2 - 1.0) + 1.0;
    return a2 / (PI * d * d);
}

float G_Smith(float ndl, float ndv, float a) {
    // 直接光 Schlick-GGX:k = (roughness^2)/2 的常见形式
    float k = a * a * 0.5;
    float gl = ndl / (ndl * (1.0 - k) + k);
    float gv = ndv / (ndv * (1.0 - k) + k);
    return gl * gv;
}

float3 F_Schlick(float uv, float3 f0) {
    return f0 + (1.0 - f0) * pow(1.0 - uv, 5.0);
}

float3 PBR_Direct(float3 N, float3 V, float3 L, float3 lightColor,
                  float3 albedo, float metallic, float roughness)
{
    float3 H = normalize(L + V);
    float ndl = max(1e-4, dot(N, L));
    float ndv = max(1e-4, dot(N, V));
    float ndh = max(0.0,  dot(N, H));

    float a = roughness * roughness;          // 感知粗糙度 → 线性 α,见下文
    float3 f0 = lerp(float3(0.04, 0.04, 0.04), albedo, metallic);

    float  D = D_GGX(ndh, a);
    float  G = G_Smith(ndl, ndv, a);
    float3 F = F_Schlick(ndv, f0);

    float3 spec = D * G * F / (4.0 * ndl * ndv);
    float3 kD   = (1.0 - F) * (1.0 - metallic);   // 能量守恒,见下文

    return (kD * albedo / PI + spec) * lightColor * ndl;
}

这就是 URP SimpleLit/Lit 内部逻辑的骨架——引擎版本多了 IBL、多灯循环、优化近似,核心就是这段。

能量守恒去哪了

F 项说「入射光里反射了 F 比例」。剩下的 1 - F 才能进漫反射——所以漫反射系数乘 (1-F)。Blinn-Phong 的高光和漫反射各自独立调,可以同时很亮,能量上就是错的;PBR 里高光强的地方漫反射自动变暗,材质不会「发光」。这就是「能量守恒去哪了」的答案:它变成了 kD = (1-F)(1-metallic) 这一行。

金属度:一个参数为什么够

物理事实:

  • 电介质(塑料、陶瓷、水、木头):F0 ≈ 0.04(灰白高光),漫反射强且带 albedo 颜色。
  • 导体(金属):F0 高且带颜色(金偏黄、铜偏红);光子进入金属立即被自由电子吸收,漫反射 ≈ 0。

一个 metallic ∈ [0,1] 恰好在这两端插值:

  • f0 = lerp(0.04, albedo, metallic):金属的高光色 = 基色。
  • kD *= (1 - metallic):金属没有漫反射。

所以金属度不是「像不像金属」的主观滑条,而是「电子行为」的两端插值。中间值(0.5)物理上不存在,工程上用来做脏污、磨损过渡。

粗糙度:为什么代码里要平方一次

美术直觉的粗糙度是感知线性的(0.5 看起来该是「中等」),但 GGX 的 α 是微观斜率的方差类参数,感知上偏陡。工程约定:感知粗糙度平方后作为 α(a = r²),Unity/glTF/Frostbite 都是这个约定。面板上同一个 0.5,直接当 α 用会明显比参考图更亮更锐——对不上时先查这里。

工作流:三张贴图怎么喂

贴图通道进公式的位置
Base Color / AlbedoRGBalbedo(金属区画的是高光色)
Metallic通常 R(或 mask 的某通道)metallic
Roughness常与 AO 打包(glTF:AO-Rough-Metal)roughness
Normal MapRGB(切线空间)替换 N,见 /notes/unity/effects-1/

参数一律走 Properties → CBUFFER,见 /notes/unity/structure-4/。

什么时候不该手写这段

生产项目优先用 URP Lit 或 Shader Graph——手写版缺 IBL、多灯、阴影集成,全补上是几千行工程(URP 源码就是答案,见 /notes/unity/engineering-1/)。手写这段的价值是理解与风格化改造:风格化高光截断、各向异性、清漆层,都是在这个骨架上动刀。

自检三问

  • 为什么金属的 albedo 贴图颜色会同时影响高光和漫反射消失?
  • 面板粗糙度 0.5,代码里 α 应该是多少?为什么?
  • kD = (1-F)(1-metallic) 去掉后画面会出什么问题?