接下来,就让我们正式进入整个 IBL 的数学原理的旅程。请注意,前方高能!
首先让我们先来完整的复习下整个反射方程(渲染方程):
Lo(p⃗,ωo⃗)=∫Ω(kdcπ+ksDFG4(ωo⃗⋅n⃗)(ωi⃗⋅n⃗))Li(p⃗,ωi⃗)n⃗⋅ωi⃗dωi⃗其中:D=NDFGGXTR(n⃗,h⃗,α)=α2π((n⃗⋅h⃗)2(α2−1)+1)2F=FSchlick(h⃗,ωo⃗,F0)=F0+(1−F0)(1−(h⃗⋅ωo⃗))5GSchlickGGX(n⃗,ωo⃗,κ)=n⃗⋅ωo⃗(n⃗⋅ωo⃗)(1−κ)+κκdirect=(α+1)28κIBL=α22G(n⃗,ωo⃗,ωi⃗,κ)=GSchlickGGX(n⃗,ωo⃗,κ)GSchlickGGX(n⃗,ωi⃗,κ)上列式子中:α=roughness2roughness∈[0.0,1.0](粗糙度系数)h⃗=ωo⃗+ωi⃗∣ωo⃗+ωi⃗∣即出射方向与入射方向的中间向量L_o(\vec{p},\vec{\omega_o}) = \int\limits_{\Omega} (k_d \cfrac{c}{\pi} + k_s \cfrac{ D F G }{ 4 (\vec{\omega_o} \cdot \vec{n} )(\vec{\omega_i} \cdot \vec{n})}) L_i(\vec{p},\vec{\omega_i}) \vec{n} \cdot \vec{\omega_i} \mathrm{d} \vec{\omega_i} \\[2ex] 其中: \\[2ex] D = NDF_{GGXTR}(\vec{n},\vec{h},\alpha) = \frac{ \alpha^2 }{\pi (( \vec{n} \cdot \vec{h} )^2(\alpha^2 - 1) + 1) ^ 2} \\[2ex] F = F_{Schlick}(\vec{h},\vec{\omega_o},F_0) = F_0 + ( 1 - F_0 )(1 - (\vec{h} \cdot \vec{\omega_o}))^5 \\[2ex] G_{SchlickGGX}(\vec{n},\vec{\omega_o},\kappa) = \frac{\vec{n} \cdot \vec{\omega_o}}{(\vec{n} \cdot \vec{\omega_o})(1-\kappa) + \kappa } \\[2ex] \kappa_{direct} = \frac{(\alpha + 1)^2}{8} \\[2ex] \kappa_{IBL} = \frac{\alpha^2}{2} \\[2ex] G(\vec{n},\vec{\omega_o},\vec{\omega_i},\kappa) = G_{SchlickGGX}(\vec{n},\vec{\omega_o},\kappa) G_{SchlickGGX}(\vec{n},\vec{\omega_i},\kappa) \\[2ex] 上列式子中:\alpha = roughness^2 \qquad roughness \in [ \ 0.0,1.0 \ ] (粗糙度系数) \\[2ex] \vec{h} = \cfrac{ \vec{\omega_o} + \vec{\omega_i} }{ \left| \vec{\omega_o} + \vec{\omega_i} \right| } \qquad 即出射方向与入射方向的中间向量 Lo(p,ωo)=Ω∫(kdπc+ks4(ωo⋅n)(ωi⋅n)DFG)Li(p,ωi)n⋅ωidωi其中:D=NDFGGXTR(n,h,α)=π((n⋅h)2(α2−1)+1)2α2F=FSchlick(h,ωo,F0)=F0+(1−F0)(1−(h⋅ωo))5GSchlickGGX(n,ωo,κ)=(n⋅ωo)(1−κ)+κn⋅ωoκdirect=8(α+1)2κIBL=2α2G(n,ωo,ωi,κ)=GSchlickGGX(n,ωo,κ)GSchlickGGX(n,ωi,κ)上列式子中:α=roughness2roughness∈[ 0.0,1.0 ](粗糙度系数)h=∣ωo+ωi∣ωo+ωi即出射方向与入射方向的中间向量
仔细观察上述方程积分部分,其中的 DFG 部分以及其分母部分,还有入射光函数都与入射点 p⃗\vec{p}p 、入射光 ωi⃗\vec{\omega_i}ωi 、出射光 ωo⃗\vec{\omega_o}ωo 以及隐含的光源颜色和物体表面的漫反射颜色 ccc 等变量相关联,并且这些变量基本都是矢量形式,所以其计算也是非常复杂的。
当然计算复杂主要是从其计算量来说的,即时间复杂度和空间复杂度。但是这些变量以及表达式至少都是可知和可计算的。如果各位有兴趣可以直接将这个积分方程按照其原始表达式按照黎曼和的形式翻译为 Shader 代码,只是每个点可能都需要海量的计算。当然你也可以通过控制黎曼和的数量来使计算量进一步减小,只是最终效果可能会有些惨不忍睹。这也是实时图形学中最永恒的一个话题——在渲染质量与效率之间进行折中。
所以这个方程虽然理论上已经可以编程进行计算了,因为 BRDF 部分的计算我们在之前的教程中已经交代清楚了,而到这里我们又知道了 IBL 的方法,使用环境映射贴图来采样的到 入射辐照度函数 Li(p⃗)L_i(\vec{p})Li(p) ,而立体角 dω\mathrm{d} \omegadω 根据前面的教程中的方法也可以拆分为积分半球上关于天顶角 θ\thetaθ 和方位角 ϕ\phiϕ 的二重积分。所以编码已经不是问题了。只是对于物体表面上每个点来说,实际都需要成千上万次的计算,期间还夹杂着大量的采样操作,而且最终效果还不一定能达到令人接受的程度。显然这样的粗暴编码的方法,因为缺乏效率,就显得非常不“计算机科学”了。因此整个渲染方程就需要更进一步的解析和优化,直到整个过程的计算量能达到可接受的程度,或者说达到当前计算机硬件条件下可计算的程度。
首先在 DirectX12(D3D12)基础教程(十九)—— 多实例渲染 的第4节中,特意补充了之前被忽略的一个重要知识点,即渲染方程中有 “ ks=Fk_s = Fks=F ”, 所以最终正确的渲染方程表达式是:
Lo(p⃗,ωo⃗)=∫Ω(kdcπ+DFG4(ωo⃗⋅n⃗)(ωi⃗⋅n⃗))Li(p⃗,ωi⃗)n⃗⋅ωi⃗dωi⃗L_o(\vec{p},\vec{\omega_o}) = \int\limits_{\Omega} (k_d \cfrac{c}{\pi} + \cfrac{ D F G }{ 4 (\vec{\omega_o} \cdot \vec{n} )(\vec{\omega_i} \cdot \vec{n})}) L_i(\vec{p},\vec{\omega_i}) \vec{n} \cdot \vec{\omega_i} \mathrm{d} \vec{\omega_i} Lo(p,ωo)=Ω∫(kdπc+4(ωo⋅n)(ωi⋅n)DFG)Li(p,ωi)n⋅ωidωi
也即 DFG 部分中已经包含了 ksk_sks 项,所以不用再乘一遍。这本质上说明菲涅尔反射其实就是所谓的镜面反射的重要系数。只是镜面反射是关于光线的纯几何近似,而菲涅尔反射则是更加“物理”的近似,并且也更“真实”。所以一般在 PBR 中再说镜面反射和漫反射时基本就不再是传统光照模型中的纯几何化的含义了,这点大家一定要注意区分,不要简单的觉得 PBR 是故弄玄虚的将光照问题复杂化了。
本质上说 PBR 渲染已经跟传统光照模型没有任何关系了,二者区别极大。当然有一种说法认为传统光照模式是物理光照模型的一种极简近似,但这是针对真实的物理光照本身说的,而不是针对 PBR 说的。请注意这些说法中的本质含义和区别。
如果你有一点关于积分运算的知识的话,那么明显的就可以发现渲染方程可以进一步拆分为两个部分:
Lo(p⃗,ωo⃗)=∫Ω(κdcπ)Li(p⃗,ωi⃗)n⃗⋅ωi⃗dωi⃗+∫Ω(DFG4(ωo⃗⋅n⃗)(ωi⃗⋅n⃗))Li(p⃗,ωi⃗)n⃗⋅ωi⃗dωi⃗=κdcπ∫ΩLi(p⃗,ωi⃗)n⃗⋅ωi⃗dωi⃗⏟漫反射项−Diffuse+∫Ω(DFG4(ωo⃗⋅n⃗)(ωi⃗⋅n⃗))Li(p⃗,ωi⃗)n⃗⋅ωi⃗dωi⃗⏟镜面反射项−Specular\mathrm{L}_{o}(\vec{p},\vec{\omega_{o}}) = \mathop{\int}_{\Omega} ( \kappa_d \frac{c}{\pi})\mathrm{L}_i(\vec{p},\vec{\omega_i}) \vec{n} \cdot \vec{\omega_i} d\vec{\omega_i} \quad \\[2ex] + \quad \mathop{\int}_{\Omega}( \frac{DFG}{4 ( \vec{\omega_o} \cdot \vec{n} ) (\vec{\omega_i} \cdot \vec{n} ) } ) \mathrm{L}_i(\vec{p},\vec{\omega_i}) \vec{n} \cdot \vec{\omega_i} d\vec{\omega_i} \\[2ex] = \underbrace{ \kappa_d \frac{c}{\pi} \mathop{\int}_{\Omega} \mathrm{L}_i(\vec{p},\vec{\omega_i}) \vec{n} \cdot \vec{\omega_i} \mathrm{d} \vec{\omega_i} }_{漫反射项-Diffuse} \quad \\[2ex] + \quad \underbrace{ \mathop{\int}_{\Omega}( \frac{DFG}{4 ( \vec{\omega_o} \cdot \vec{n} ) (\vec{\omega_i} \cdot \vec{n} ) } ) \mathrm{L}_i(\vec{p},\vec{\omega_i}) \vec{n} \cdot \vec{\omega_i} d\vec{\omega_i} }_{镜面反射项-Specular} Lo(p,ωo)=∫Ω(κdπc)Li(p,ωi)n⋅ωidωi+∫Ω(4(ωo⋅n)(ωi⋅n)DFG)Li(p,ωi)n⋅ωidωi=漫反射项−Diffuseκdπc∫ΩLi(p,ωi)n⋅ωidωi+镜面反射项−Specular∫Ω(4(ωo⋅n)(ωi⋅n)DFG)Li(p,ωi)n⋅ωidωi
这样反射积分就变成了两个部分积分的和。这样显然是有好处的,因为这种形式两个积分项可以分开计算,最后再来求和,所以可以被方便的用来并行计算。