Shader / PBR
自定义 PBR 与 Unity Lit 的差异拆解
本文记录从 Unity Lit 迁到自定义 PBR 时踩过的坑与差异点,主要关注 BRDF 项、能量守恒、几何可见性函数的差异。
引言
Unity URP 的 Lit shader 用的是简化的金属流 PBR(基于 Disney / GGX),多数场景下够用, 但一旦想做更「物理正确」的渲染管线,自定义 PBR 就躲不开。本文整理我从 Unity Lit 迁到 自写 PBR 时遇到的几个核心差异:
- 法线分布 D 项:URP 用的 GGX/Smith,自己实现时要注意
alpha = roughness^2还是roughness^4。 - 几何遮挡 G 项:Smith vs Schlick-GGX 近似,可见性项
V = G / (4 N·L N·V)的合并写法。 - 菲涅尔 F 项:Schlick 近似的
F0取值,金属流和非金属流的处理。 - 能量守恒:漫反射项要不要乘
(1 - F),金属流要不要清零漫反射。
法线分布 D 项:GGX 与 α 的平方链
GGX(Trowbridge-Reitz)法线分布函数的核心公式:
其中 h 是半角向量,α 是粗糙度相关参数。这里的坑在于 α 与 roughness 的映射关系。
行业里存在两层「平方」:
PerceptualRoughnessToRoughness(p) = p²—— 把感知粗糙度映射到物理粗糙度;- GGX 公式里的
α = roughness²(Disney 的约定)。
合起来,如果用户拖动的滑块是 perceptualRoughness,那么 GGX 里真正用到的 α² = perceptualRoughness⁴。
这就是很多人说的「roughness^4」来源。
Unity URP 的 Lit.hlsl 实际选择:
real roughness2 = roughness * roughness; // roughness 这里已经是物理 roughness
real roughness4 = roughness2 * roughness2;
// GGX D 项里传入的是 roughness4,即 α²
real D_GGX(real NdotH, real roughness4)
{
real a2 = roughness4;
real d = (NdotH * a2 - NdotH) * NdotH + 1.0;
return a2 / (PI * d * d + 1e-7);
}注意 roughness 在 URP 里已经经过 PerceptualRoughnessToRoughness 平方过一次,所以 roughness2 = α,
roughness4 = α²。如果你自写 PBR 时把用户滑块直接当 α 喂进去(少平方一次),高光会明显比 Unity 窄且硬,
这是迁移时最常踩的坑。
我自己实现的版本,把两层平方都显式写出,避免歧义:
real D_GGX(float3 N, float3 H, float perceptualRoughness)
{
float a = perceptualRoughness * perceptualRoughness; // α
float a2 = a * a; // α²
float NdotH = saturate(dot(N, H));
float d = (NdotH * a2 - NdotH) * NdotH + 1.0;
return a2 * rcp(PI * d * d + 1e-7);
}几何遮挡 G 项:Smith-Joint vs Schlick-GGX
几何项描述微表面自遮挡。精确形式是 Smith-GGX:
但精确 Smith 带开方,移动端常用 Schlick-GGX 近似:
k 对直接光和 IBL 取值不同(直接光 k = (α+1)² / 8,IBL k = α²/2),这是 UE4 的约定。
工程上更常见的写法是把 G / (4·N·L·N·V) 合并成可见性项 V,省一次除法。Unity URP 用的是
Smith-Joint GGX 可见性近似:
real Vis_SmithJointGGX(real NdotL, real NdotV, real roughness2)
{
// roughness2 这里是 α
real a = roughness2;
real lambdaV = NdotL * (NdotV * (1.0 - a) + a);
real lambdaL = NdotV * (NdotL * (1.0 - a) + a);
return 0.5 / (lambdaV + lambdaL + 1e-5);
}注意返回的是 V = G/(4·N·L·N·V),所以最终高光只需 D * V * F,不用再除一次。
Schlick-GGX 的 V 形式更便宜:
real Vis_SchlickGGX(real NdotL, real NdotV, real k)
{
real VisV = NdotV * (1.0 - k) + k;
real VisL = NdotL * (1.0 - k) + k;
return 0.25 / (VisV * VisL); // 即 (N·V)(N·L) / ((N·V)(1-k)+k)((N·L)(1-k)+k)
}实测 Smith-Joint 比 Schlick 在掠射角更接近精确 Smith,亮度过渡更自然;Schlick 在低粗糙度时偏高, 会让边缘高光略爆。URP 默认走 Smith-Joint,自己实现时建议对齐。
菲涅尔 F 项:Schlick 近似与 F0
菲涅尔用 Schlick 近似就够了,Marschner 那种精确 Fresnel-Conductor 在实时里没必要:
关键是 F0(垂直入射反射率)的取值,分金属流和非金属流:
- 非金属:
F0 = 0.04(介电质典型值,对应 IOR≈1.5); - 金属:
F0 = baseColor(albedo 直接当反射率,因为金属没有漫反射)。
金属流 PBR 用 metallic 在两者之间插值:
real3 F0 = lerp(0.04, baseColor, metallic);
real3 F_Schlick(real VdotH, real3 F0)
{
real f = Pow5(1.0 - VdotH);
return F0 + (1.0 - F0) * f;
}URP 的 F_Schlick 实现一致。差异点在 VdotH 还是用 LdotH:理论上菲涅尔是观测方向对微表面法线(即半角)的夹角,
所以用 VdotH(或等价的 LdotH)才对,不要用 NdotV,否则掠射角菲涅尔不会正确增强。
能量守恒:漫反射与 (1 − F)
能量守恒的朴素做法是让漫反射乘以 (1 − F),因为被镜面反射走的那部分能量不能再算进漫反射:
real3 kd = (1.0 - F) * (1.0 - metallic);
real3 diffuse = kd * baseColor / PI;(1 - metallic) 是金属流清零漫反射的关键:金属的所有反射都是镜面的,没有次表面散射。
(1 - F) 让漫反射随菲涅尔增强而衰减,保证总能量不爆。
但这里有个细节:Unity URP 的 Lit 用的是 Lambert + (1-F) 的简化,没有用 Disney 的
BurleyDiffuse 或 Hanrahan-Krueger。迁移时如果想升级到更正确的漫反射模型,可以换成
Burley 漫反射:
real3 BurleyDiffuse(real NdotL, real NdotV, real VdotH, real roughness, real3 baseColor)
{
real fd90 = 0.5 + 2.0 * VdotH * VdotH * roughness;
real2 f90 = real2(fd90, fd90);
real2 f0 = real2(1.0, 1.0);
real lightScatter = f0.x + (f90.x - f0.x) * Pow5(1.0 - NdotL);
real viewScatter = f0.y + (f90.y - f0.y) * Pow5(1.0 - NdotV);
return baseColor * rcp(PI) * lightScatter * viewScatter;
}Burley 会让粗糙表面的背光面带一点泛红过渡,比纯 Lambert 自然,但成本高一档。移动端建议保持 Lambert。
总结
迁移过程中踩到的几个关键差异点:
- α 的平方链:
perceptualRoughness → roughness (²) → α (²) → α² (²),少平方一次高光就窄硬。 - V 项合并:URP 用 Smith-Joint 的
V形式,自写时对齐0.5 / (λV + λL),别再额外除4·N·L·N·V。 - F0 金属流:
lerp(0.04, baseColor, metallic),菲涅尔用VdotH不是NdotV。 - 能量守恒:
kd = (1-F)(1-metallic),金属漫反射清零;要更好的过渡可换 Burley 漫反射。
整体上 Unity Lit 的实现是「物理上够用、性能上克制」的取舍,自写 PBR 时把这些项对齐后, 差异主要出现在掠射角和金属高光形态上,正对视角几乎看不出区别。
公式速查
GGX 法线分布 D 项:
Smith 几何可见性 G 项:
Schlick 菲涅尔 F 项:
下面是用默认 shader 展示上述 BRDF 项叠加效果的实时演示:
评论
昵称留言,审核后显示
加载中…