Deep Read

Physically Based Rendering

  PBR 的核心目标不是 100% 物理精确——那是离线渲染的事——而是在物理规律的约束下建立一套可预测的光照模型:美术用真实世界的材质属性(金属度、粗糙度)来创作,材质在任何光照环境下都能表现出一致的效果。整套 PBR 可以拆成三层来理解:先是对现实世界光与物质交互的三个物理观察,然后是把这些观察编码成数学的渲染方程与 BRDF,最后是为了实时跑起来所做的一系列工程近似(尤其是环境光照部分)。

三大物理原理

所有复杂的公式,都是在模拟下面三个现象。

微表面理论 (Microfacet Theory)

  宏观上光滑的表面,在微观尺度下都是由无数朝向随机的微小镜面(Microfacets)组成的。粗糙度 (Roughness) 描述的就是这些微观平面朝向的混乱程度:Roughness ≈ 0 时微观平面朝向基本一致,整体像一面大镜子,高光集中而清晰;Roughness ≈ 1 时朝向混乱,光线被散射到四面八方,高光模糊且范围大。

数学上用法线分布函数 (Normal Distribution Function, NDF) 来描述这种混乱程度——它回答的问题是:有多大比例的微观平面朝向恰好能把光反射进你的眼睛。现代 PBR 最常用的是 GGX (Trowbridge-Reitz),因为它产生的高光带有柔和的长尾,最接近真实观察结果。

能量守恒 (Energy Conservation)

  一个表面反射出去的光,不可能多于它接收到的光。光照到表面时分成两部分:一部分直接被反射(高光),另一部分折射进物体内部。对不透明物体,折射进去的光要么被吸收,要么经过次表面散射后再射出来——后者就是漫反射(Diffuse)。所以永远有:漫反射 + 高光反射 ≤ 入射光。

由此推出一个重要结论:高光反射能力越强的物体,漫反射就越弱。金属是极端情况——折射进内部的光全部被吸收,几乎没有漫反射,只剩高光。

菲涅尔效应 (Fresnel Effect)

  观察角度会影响表面的反射率:几乎所有表面在掠射角(视线接近与表面平行)下反射率都会急剧升高,趋近 100%。看平静的水面就是这个效应——垂直向下看能看清水底,接近水平看过去水面就成了镜子。

精确描述这个效应的是菲涅尔方程,实时渲染中通常用 Schlick 近似。其中的关键参数是 F0:垂直入射时的基础反射率,它是区分物质类别的核心。电介质(塑料、水、木头、皮肤)的 F0 很低,约 0.02–0.05;金属(金、银、铜)的 F0 很高(0.7–1.0)且带颜色。

金属度工作流就建立在这个区别上:Metallic 参数决定如何解读 BaseColor(Albedo)贴图——同一张贴图,非金属时被当作固有色(漫反射颜色),金属时被当作 F0(镜面反射颜色)。

F0(镜面基础反射率)漫反射颜色
金属 (Metallic = 1)BaseColor 贴图(带颜色,0.7–1.0)0(黑)
非金属 (Metallic = 0)0.04(写死的灰度常数)BaseColor 贴图

这张表的每一格都有物理来由:非金属的 F0 几乎都在 4% 左右且不带颜色,所以引擎直接硬编码 0.04,不给美术调;金属的反射自带颜色(金黄、铜红来自反射本身),每种金属不同,所以交给贴图;金属没有漫反射,是因为折射进内部的光被自由电子全部吸收。URP 源码(ShaderLibrary/BRDF.hlslkDielectricSpec 就是那个 0.04)中对应两个 lerp:

brdfSpecular = lerp(kDieletricSpec.rgb, albedo, metallic);  // F0:非金属取0.04,金属取Albedo
brdfDiffuse = albedo * (1.0 - metallic) * ...;              // 漫反射:金属时归零

注意表中的 F0 是"起点"不是"上限":实际高光还要经过菲涅尔随角度放大,掠射角下非金属也能反射接近 100%。Metallic 本身是连续值,中间值就是在两套解读之间插值,但物理上不存在"半金属",中间值只应出现在金属与非金属的过渡像素上(锈迹、灰尘)。

这套"一图两用"也是金属度工作流相对 Specular 工作流省纹理的原因:后者需要一张单独的 RGB 高光颜色贴图,前者用一个单通道灰度图就编码了同样的信息。

渲染方程与 BRDF

图形学的终极目标是解渲染方程,它完整描述了光能在场景中的传播与反射:

image.png

Lo(p,ωo)=Le(p,ωo)+Ωfr(p,ωi,ωo)Li(p,ωi)(nωi)dωiL_o(p, \omega_o) = L_e(p, \omega_o) + \int_\Omega f_r(p, \omega_i, \omega_o) \, L_i(p, \omega_i) \, (n \cdot \omega_i) \, d\omega_i
  • LoL_o 是出射辐射度(最终看到的颜色),LeL_e 是自发光;
  • 积分对半球上所有方向的入射光 LiL_i 求和,(nωi)(n \cdot \omega_i) 是兰伯特余弦项;
  • frf_r 就是双向反射分布函数 (BRDF)——输入入射方向、观察方向、法线和材质属性,输出表面对光的反射比例。

这个方程无法实时精确求解,PBR 的核心就是用一个物理上可信的 BRDF 去近似 frf_r。URP 的 Lit 材质所用的 BRDF 拆成两项:

fr=kdflambert+ksfcook-torrance\Large f_r = k_d \, f_{lambert} + k_s \, f_{cook\text{-}torrance}
两项分别对应光与表面交互的两条路径:
  • 折射进内部再散射出来的漫反射
  • 在界面上被立即反弹的镜面反射

漫反射项:Lambert

漫反射模拟的是光折射(Refraction)进入物体内部、经过多次次表面散射(Subsurface Scattering)后再从表面射出的部分。Lambert 是最经典的完美漫反射模型:它假设光一旦进入材质内部就被均匀散射,从任何方向射出的亮度都相同,与观察角度无关。

flambert=calbedoπ\Large f_{lambert} = \frac{c_{albedo}}{\pi}

Albedo:固有色,材质的基础色,物理上代表光进入材质内部散射后最终"幸存"射出的颜色和比例。

除以 π\pi 是能量守恒的要求:一个反射率为 1(全白)的朗伯表面,接收来自整个半球的光后,反射的总能量不能超过入射总能量。对 BRDF 乘余弦项在半球上积分,朗伯余弦项的积分结果正好是 π\pi,所以 BRDF 本身必须除以 π\pi 才能归一化。

镜面反射项:Cook-Torrance

镜面反射模拟光在物体表面(界面)被立即反弹的部分。Cook-Torrance 是基于微表面理论的 BRDF 模型,三大物理原理在这里全部落地:

fcook-torrance=DFG4(nl)(nv)\Large f_{cook\text{-}torrance} = \frac{D \cdot F \cdot G}{4 \, (n \cdot l)(n \cdot v)}
  • D(法线分布函数):决定高光的大小和形状
    • 描述微表面法线的分布:有多少微表面的朝向恰好能把光源反射进你眼睛。分布越集中(粗糙度低)高光越小越锐,越分散(粗糙度高)高光越大越糊。由 Roughness 控制。
- **F(菲涅尔项)**:决定反射比例随角度的变化 - 描述不同观察角度下被反射的光所占的比率,由 F0 和视角共同决定。
- **G(几何遮蔽函数)**:遮挡打折,防止掠射角超亮 - 描述微平面之间的自遮挡和自阴影,即朝向正确($m = h$)却未被遮挡的微表面所占的百分比。它的作用是保证粗糙表面在掠射角下不会因大量微表面同时反射而异常变亮——即守住能量守恒。
- **分母 $4(n \cdot l)(n \cdot v)$** :校正因子,处理微观几何局部空间与宏观表面局部空间之间变换的微平面量校正。
  工程上注意分母的点积不仅要避免负值,也要避免零值,通常在 clamp 或取绝对值之后再加一个很小的正数。还要清楚这个模型的边界:它只对单层微表面上的单次散射建模,不考虑多次散射、分层材质和衍射。被 G 挡掉的光实际上并没有消失,会在微表面间继续反弹,单次散射模型把它直接丢掉了,代价是粗糙度越高表面越暗(粗糙金属尤其明显),近年的多次散射补偿(Kulla & Conty 2017)就是在补这部分能量。

下面分别看 D、F、G 三项的具体选型。

Specular D

法线分布函数的常见模型有:

  • Beckmann [1963]、
  • Blinn-Phong [1977]、
  • GGX [2007](即 Trowbridge-Reitz [1975])、
  • GTR [2012],
  • 以及各向异性版本的 Beckmann [2012] 和 GGX [2015]。

业界主流是 GGX,胜在高光长尾更真实:

DGGX(m)=α2π((nm)2(α21)+1)2D_{GGX}(\mathbf{m}) = \frac{\alpha^2}{\pi \left( (\mathbf{n} \cdot \mathbf{m})^2 (\alpha^2 - 1) + 1 \right)^2}

image.png

Specular F

菲涅尔项的常见模型有

  • Cook-Torrance [1982]、
  • Schlick [1994]、
  • Gotanda [2014]。

业界基本都用 Schlick 近似——计算成本低,精度足够:

FSchlick(v,h)=F0+(1F0)(1(vh))5F_{Schlick}(\mathbf{v}, \mathbf{h}) = F_0 + (1 - F_0)\left(1 - (v \cdot h)\right)^5

Specular G

几何项的常见模型有

  • Smith [1967]、
  • Cook-Torrance [1982]、
  • Neumann [1999]、
  • Kelemen [2001]、
  • Implicit [2013]。

Eric Heitz 在 [Heitz14] 中论证了 Smith 几何阴影函数是正确且更准确的 G 项,并将其拓展为 Smith 联合遮蔽阴影函数(Smith Joint Masking-Shadowing Function),共四种形式:分离型、高度相关型、方向相关型、高度-方向相关型。目前较常用的是其中最简单的分离遮蔽阴影型 (Separable Masking and Shadowing):把 G 拆成光线方向和视线方向两个独立的部分,用相同的分布函数分别计算再相乘。

按这个思想把 Smith 框架和不同的 NDF / 近似组合,就得到 Smith-GGX、Smith-Beckmann、Smith-Schlick、Schlick-Beckmann、Schlick-GGX 等变体。UE4 用的是 Schlick-GGX:以 Schlick 近似为基础,通过重新映射 kk 去匹配 GGX 的 Smith 方程——解析光源用 k=(Roughness+1)28k = \frac{(Roughness + 1)^2}{8},IBL 用 k=α22k = \frac{\alpha^2}{2}

GSchlickGGX(n,v,k)=nv(nv)(1k)+kG_{SchlickGGX}(n, v, k) = \frac{n \cdot v}{(n \cdot v)(1 - k) + k}

kdk_dksk_s:能量守恒把两项绑在一起

kdk_d(漫反射系数)和 ksk_s(镜面反射系数)不是两个独立的参数,它们被菲涅尔项 FF 绑定。菲涅尔项已经精确给出了"有 FF 比例的光在界面被立即反射",那么根据能量守恒,剩下 1F1 - F 的光必然折射进了材质内部——而这部分折射光是漫反射的唯一能量来源。所以:

ks=F,kd=1Fk_s = F, \qquad k_d = 1 - F

这个绑定关系直接解释了一个日常现象:斜着看塑料桌面时只见反光不见桌面自己的颜色,因为掠射角下 FF 趋近 1.0,ks1k_s \to 1kd0k_d \to 0,漫反射颜色(Albedo)被挤没了,剩下的全是镜面反射。

基于物理的环境光照

以上处理的是直接光。环境光照(IBL)要解决的是渲染方程中来自整个环境的积分,同样拆成漫反射和镜面反射两半,各自预计算。

image.png

漫反射:SH 球谐光照

  要解决的问题:一个点接收到的环境光是"每个方向多亮"的球面函数,直接存就是一张 Cubemap,太贵。

SH 的原理:把这个球面函数分解成一组固定形状"积木"的加权和,只存权重系数,用的时候代入方向重新加权求和还原光照——相当于球面上的傅里叶变换,存频率系数,不存原始信号。

积木一共 9 块,按复杂度分三组:

  • 1 块均匀的球:管平均亮度(环境光底色);
  • 3 块哑铃(沿 x/y/z 轴,一头亮一头暗):管方向性,比如"上面比下面亮";
  • 5 块花瓣:管更细一档的明暗起伏。

为什么 9 块就够?因为漫反射本来就把光"揉"得极度平滑——它只有低频没有高频,更复杂的积木给了也用不上(Ramamoorthi 2001 证明了误差在 1% 以内)。

于是间接漫反射的流程变成:

  1. 烘焙时:把环境 Cubemap 按漫反射 BRDF 卷积(重度模糊),再分解到 9 块积木上,算出权重——每通道 9 个、RGB 共 27 个浮点数。一张 Cubemap 就这样被压成了 27 个数。
  2. 渲染时:Shader 拿法线方向代入 9 块积木求值,乘上权重加起来,就得到该方向的环境漫反射颜色。全程只是几次乘加,不采样任何贴图,比读纹理还便宜。

URP 用的是 L2 球谐(取到 l=2l=2,就是上面这 9 块;有人叫二阶、有人叫三阶,同一个东西,写 L2 最不含糊)。那 27 个权重装在 7 个 Shader 常量里:unity_SHAr/SHAg/SHAb(常数项 + 哑铃项)、unity_SHBr/SHBg/SHBbunity_SHC(花瓣项),采样入口是 SampleSH()

间接漫反射在 Unity 中的存储

烘焙时要回答的问题是:一个点接收到的间接光怎么存?Unity 按物体是否移动分成两条路,关键区别在于烘焙时法线是否已知

  • 静态物体 → 光照贴图 (Lightmap)。表面固定,每个纹素的法线烘焙时就已知,可以直接把"来自所有方向的入射光对着这个法线积分"的结果算出来,存一个颜色值,方向维度在烘焙时就消掉了。运行时用第二套 UV 采样即可,它不需要 SH。
  • 动态物体 → 光照探针 (Light Probes)。物体会动,烘焙时不知道将来站在这个位置的表面法线朝哪,只能把整个球面的光照都保留下来,运行时再按实际法线解码。"高效存储一个球面函数"正是 SH 要解决的问题,每个探针存 27 个 SH 系数。
  • 两者都没有时的兜底是 Ambient Probe:天空盒环境光的一组全局 SH 系数。
存储方式存的是什么为什么
静态物体Lightmap每纹素一个颜色(不是 SH)法线已知,方向可预先积分掉
动态物体Light Probes每探针 27 个 SH 系数法线未知,必须保留球面信息
兜底Ambient Probe一组全局 SH 系数同上,取自天空盒

在 Unity 里可以这么记:SH 是"探针"的数学载体,凡是探针(Light Probe / Ambient Probe / 新一代的 APV),里面装的都是 SH 系数;间接漫反射 = 探针(SH)+ Lightmap(非 SH)两条腿。SH 也只管漫反射这半边,间接高光是高频信号,9 块积木拼不出清晰的倒影,只能走贴图,也就是下一节的 Reflection Probe。

镜面反射:Split Sum

环境光镜面反射要求解的是渲染方程中关于环境的镜面积分:

Lo(v)=Ωfspecular(l,v,h)Li(l)(nl)dlL_o(v) = \int_\Omega f_{specular}(l, v, h) \cdot L_i(l) \cdot (n \cdot l) \, dl

其中 LiL_i 是来自环境贴图的入射光,fspecularf_{specular} 是镜面 BRDF。这个积分无法实时计算,现代 PBR 管线的行业标准是分离求和近似 (Split Sum Approximation):把积分拆成两个相对独立的部分分别预计算,运行时在 Shader 中采样两个结果组合起来。这两部分是:

  1. 预滤波环境贴图 (Pre-filtered Environment Map) —— 负责光照项 LiL_i
  2. 环境 BRDF (Environment BRDF) —— 负责 BRDF 项。

预滤波环境贴图

  粗糙表面的反射是无数微观平面反射结果的平均,实时渲染不可能对每个像素采样 Cubemap 上成百上千个点。解法是把这个平均预先算好:Cubemap 生成后,引擎对它做卷积,模拟不同粗糙度下的反射效果,结果存进 Mipmap 链——Mip 0 是最清晰的原图,对应 Roughness = 0 的完美镜面;Mip 级别越高图像越模糊,对应越高的粗糙度。

运行时,Shader 根据材质的 Roughness 算出对应的 Mip 级别,采样一次就得到近似正确的模糊反射。

环境 BRDF

材质的"反射效率表"—— 给定粗糙度和观察角度,它告诉你从整个半球打来的环境光中,有百分之几会被反射进你的眼睛。预滤波环境贴图给"环境有什么光",环境 BRDF 给"材质吃下多少",两者相乘就是间接高光。

"Environment BRDF" 是个概念性的昵称,更准确的叫法是半球-方向反射率:对于给定的观察方向 vv 和粗糙度 α\alpha,当来自整个半球的所有光线(假设强度均为 1.0)照射到表面时,BRDF 总共会向 vv 方向反射多少比例的能量。它只依赖 Roughness 和 nvn \cdot v 两个变量,因此可以预计算。实现上有两条路线:

  • 2D LUT:UE4 的经典方案。把结果烘焙成一张 2D 查找表(BRDF Integration Map),输入 Roughness 和 NdotV,输出一个缩放因子和一个偏移,在 Shader 中作用于 F0 完成修正。
  • 解析拟合:URP 的方案。不用查找表,而是用一个拟合出的数学解析式直接计算,整个过程在 GPU 的 ALU 中完成,省掉一次纹理采样的内存访问,对移动端这类带宽受限的设备更友好。