PBR 从公式到参数:Cook-Torrance 与金属度/粗糙度工作流
上一篇把 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 / Albedo | RGB | albedo(金属区画的是高光色) |
| Metallic | 通常 R(或 mask 的某通道) | metallic |
| Roughness | 常与 AO 打包(glTF:AO-Rough-Metal) | roughness |
| Normal Map | RGB(切线空间) | 替换 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)去掉后画面会出什么问题?