[引擎开发] 全局照明

       全局光照系统一直是实时渲染领域中最备受瞩目的模块,当我们在讨论全局光照时,我们实际上就是在做偏向真实化的渲染,这意味着我们需要以实时的效率去模拟逼近光线追踪的效果,我们所做的一切都是基于物理的,也就是说我们会去考虑光线的直接入射和多次反弹,考虑材质对于光线的真实响应。全局照明主要可以分为直接照明和间接照明部分,直接照明的部分我们应该已经比较熟悉了,比较复杂的是间接照明的部分,也是我们在这一篇章主要阐述的内容。

        我们介绍光照系统之前,首先来回顾在“基于物理的渲染”(PBR)一节中提到的基础概念。

PBR材质回顾

        我们能看见眼前的苹果,是因为光线发射后触碰到苹果,并且反弹到人的眼睛。

        我们能看到“苹果是红色的”,是因为苹果会反射红色,其余的颜色分量则被吸收了,物体出射的颜色是材质的固有属性,我们称为反照率(albedo)。

        我们能够感受到金苹果(金属)能够发出耀眼的光,是因为金苹果会把大部分光线都直接反射出去,光线集中在特定方向附近形成特定的高光形状,而对于普通的红苹果而言,我们之所以觉得它的颜色更加“平坦”,是因为对于红苹果(电介质)会更多地对光线进行漫反射,颜色会近似均匀地出射。

        但是我们也能在特定视角下观察到水面上波光粼粼的效果,即使水并不是金属。这是因为光线反射的比例不仅和材质固有属性有关,也和视角相关。金属的基础反射率高,所以在任意视角下观察都有闪耀的外观,而电介质的基础反射率很低,在视线和法线接近垂直的时候,才会表现出大多数光线反射出去的外观(菲涅尔效应)。

        从光线传播的角度来看,光线照射到物体后,会发生反射(离开物体)或折射(进入物体)的现象。对于折射进入物体的光线,一些被吸收了(不再出射),一些在入射点又重新以任意方向出射(漫反射),一些在任意点以任意方向出射(次表面散射),一些穿过了物体在另一侧出射(透射)。

严格意义上,上述描述的任意方向l:满足dot(n,l)大于0的约束,即分布在法线半球面内

        BRDF描述了材质对于光线的响应,即对于材质上某个位置,有多少光线发生反射,它可以分为漫反射和镜面反射两部分。

        它描述的结果基于微平面理论,也就是将认为物体表面是由许多微小的光学平坦平面组成的,我们使用的材质属性实际上是这些微平面的统计属性。其中,法线分布描述了表面方向和中间向量(微平面法线)的一致性,几何遮蔽项描述了微平面的自遮蔽属性,菲涅尔项描述了反射和折射的比例。

概念引入

        在开始介绍全局光照之前,首先需要介绍两个非常常用的物理量,一个是辐射率(radiance),另一个是辐照度(irradiance),中文比较容易混淆,所以我们通常用英文来描述,这两者是需要牢记的。

        其中radiance我们可以简单的将其理解当前方向的光照输入,比如一张实时捕获的环境贴图,我们可以认为提供了radiance信息;而irradiance我们可以简单的将其理解为每个点接收的光照,比如对实时捕获环境贴图进行卷积,那么得到的就是irradiance。

光源类型

        当我们模拟真实化渲染的光照时,我们考虑两个问题,一个是存在哪些可能的光源,另一个就是材质如何对光源进行响应,其中后者就是我们描述的PBR材质。

        而对于前者来说,我们会考虑现实中的情况。

        首先是会接收来自太阳的直射光,不被阳光直射的地方处于阴影中。处在阴影中的物体也是可见的,是因为它们一方面接收了太阳光经过云层/大气的散射光,这些光源沿着天空球从多个方向入射,我们通常称之为天光;另一方面,它接收了来自周围物体对阳光/天光的一次/多次反弹光线。

       此外,它还可能接收局部光源,比如点光源/聚光灯/面光源等,这些局部灯光的距离是有限的,并且会随着距离平方衰减,同理,局部光源也会产生多次反射的间接光照。

虽然月光/天光本质上属于太阳的间接光照,但如果我们把场景看作一个封闭系统,月光/天光是从系统外部直接输入的光照,而不是从系统内部产生的光照,所以在实时渲染中我们将其认为是直接光

        当我们在室内的时候,会因为房间的遮挡而无法接收天光,这种现象我们称之为天光遮蔽;同时,物体之间的相互遮挡,物体自身的自遮挡,也会阻挡反弹光线,这种现象我们称之为环境光遮蔽。

        我们把上面一些系列直接光和间接光组成的光照系统称之为全局光照。

路径追踪

        一种比较常见的可以计算出真实光照的算法是Path Tracing(路径追踪)。

        这是一种迭代的离线算法,是光线追踪算法中比较常用的一种方案。对这类光线追踪迭代算法,通常我们可以选择从灯光或眼睛位置开始迭代,直到遇到视点/光源后停止迭代,或者从双向出发进行迭代并在中间相遇。

        Path Tracing通常是从视点位置开始迭代。发出的视线在触碰到物体上的某一个点的时候,我们在半球空间随机选择多个方向作为新的射线执行递归渲染,并获取返回的颜色作为当前方向的radiance,然后根据当前材质的BRDF计算反射到眼睛的颜色。

        如果迭次次数不足,那么由于收集到的信息不足(没有命中光源)得到的结果会有较多的噪点,这个现象会随着迭代次数增加而得到改善。Path Tracing无法很快的收敛有一部分原因在于随机采样,这可以通过重要度采样来优化。

        我们假设场景中只有一个平行光源,那么我们的伪代码如下:

Color PathTrace(Ray ray)
{
    if (hit nothing)
    {
        if (ray.direction == Light.direction )
        {
            return Light.color;
        }
        return Black;
    }

    for(...)
    {
        Ray newRay;
        newRay.origin = ray.HitPoint;
        newRay.direction = Random();
    
        Color radiance = PathTrace(newRay);
        result.color += BRDF * radiance * dot(newRay.origin.normal, newRay.direction);
    }
    return result;
}

        假如我们经过了一次迭代就命中了入射光的方向,那么计算的结果是我们通常认为的直接光,如果我们经过多次迭代才命中入射光方向,那么计算的结果是我们通常认为的间接光。

        需要注意的是,这个函数是递归执行的,它可能需要经过成千上万次采样,可以达到一个比较精确的效果,该算法的耗时比较长,通常作为离线渲染的手段。它可以作为实时渲染参考的一个ground-truth对照结果,也可以作为预先烘焙的结果存储在光照贴图中用于实时渲染。

        基于路径追踪,我们可以模拟镜面反射或漫反射的材质,也可以模拟透射(透明物体)和次表面散射以及软阴影效果。

        光线追踪是后续实时渲染的基础,后续讨论的大多数内容都是如何在确保可交互帧率的情况下拟合全局光照的结果,包括直接缓存离线计算的光照结果,或者缓存一些中间计算结果,又或者是基于加速结构进行屏幕空间或世界空间的光线追踪;或者通过减少迭代次数,只去模拟至多两次的光线反弹。

全局光照与材质外观

        在对全局照明有了初步认知后,我们再回来看PBR系统,它描述了材质如何与光照进行交互,只是在前期我们通常以直接平行光的形式来了解PBR。实际上材质应该处于一个非常复杂的光照环境中,不同的光照结合材质的固有属性会产生不同的外观。

        一个非常常见的材质表现就是金属会反射出周围的环境,这实际上就是一种全局光照的表现。我们在讨论直接平行光对金属的镜面反射的时候,会提到由于直接光会更集中的反弹到某些方向上去,这是因为平行光的方向是一致的,才会产生更加具有方向性的外观。而在从四周入射的环境光下,由于光照方向更加均匀,所以反射出来的光线方向也趋于均匀,最后形成了如同镜子一般能够反射场景环境的效果。

        另一个全局光照的表现就是渗色(color bleeding),我们站在红色的砖墙旁,身上会染上红色,站在青草地上,身上会染上绿色,就好像环境的颜色渗透过来了一样,这就是一种物体之间相互照亮的效果。
        以及我们在亮处和暗处去观察物体会有一些不一样的表现,也就是说我们在处理材质的时候会去考虑全局光照对于材质的影响,比如头发会在直接光照明下呈现比较明显的各向异性高光的效果,而在暗部则是多重散射的效果比较明显。

光照方程

        我们后续讨论的绝大部分内容都围绕着光照方程展开:

        L_o(v)=L_e(v)+\int L_i(l)fs(l\rightarrow v)cos(l)dl

        也就是说,最终像素光照相当于周围所有方向的入射radiance和BRDF的积分。

        其中,Le(v)是自发光部分,Li(l)是l方向上的入射radiance,fs(l->v)是从l到v的BRDF公式,cos(l)为方向l和法线的点乘。

数学基础

        我们会在后文中运用到一些比较基础的微积分知识,因为计算环境光照结果本质上就是计算积分,所以在此我们首先要进行介绍。

半球积分

        由于我们需要计算环境光照的贡献,考虑到复杂的入射方向,我们通常会对入射角w0进行积分,对物体表面的某一点在法线方向周围的单位半球邻域进行积分。

        这个积分本质上是一个面积分,它的微元是立体角所对应的一小块曲面,dw可以表示为dxdydz,但这样计算起来比较麻烦,因此我们在计算的时候通常使用极坐标,也就是:

        dw=\sin \theta d\theta d\varphi

        其中,φ是方位角,取值范围在(0,2π),θ是极角,取值范围在(0, π/2),r为半径,目前由于处理的是单位球面,可以直接取1。

        我们在后文中,基本总是用θ指代极角,用φ指代方位角

我们要计算微元曲面的面积,把它近似为矩形,它的两边分别为dθ和sinθdφ
边长的计算是依据弧长公式ds = rdθ,上式中出现的sinθ来自于xy平面上的投影。

        半径从r变换到dr,极角从θ变化到dθ,方位角从φ变换到dφ,各维度的变化为:

        (dr,rd\theta,r \sin \theta d\varphi)

        我们对dw在半球上直接进行积分得到的结果是2\pi,这就是单位半球面的面积。

        我们假设一个直射光均匀的从半球出射,也就是漫反射的情况,由于对于任意方向的光线,只有和法线方向的分量才是有效的,我们还需要考虑dot(n,l)分量,这相当于计算cosθdw的半球积分,最后得到的结果是\pi。反之,如果我们做一个归一化的话,假设入射能量为1,那么出射的每个方向上分到的能量就是\frac{1}{\pi},这个就是我们在计算漫反射的时候经常看到的归一化系数。

        它的推导如下:

                \iint_{\Omega }^{} \cos \theta dw

        = \int_{0}^{2\pi } \int_{0}^{\frac{\pi }{2}} \cos \theta \sin \theta d\theta d\varphi

        = \int_{0}^{2\pi } \int_{0}^{\frac{\pi }{2}} \frac{1}{2}\sin 2\theta d\theta d\varphi

        = \int_{0}^{2\pi } \left ( -\frac{1}{2}\cos 2\theta \right |_{0 }^{2\pi} ) d\varphi

        = \int_{0}^{2\pi } \frac{1}{2} d\varphi

        = \frac{1}{2}|_{0}^{2\pi } = \pi

辐射通量

        我们在物理学中有一个比较常见的概念就是通量,从wikipedia可以看到它的定义:

给定一个三维空间中的向量场A以及一个简单有向曲面Σ,则向量场A通过曲面Σ的通量就是曲面每一点x上的场向量A(x) 在曲面法向方向上的分量的积分:

        \Phi (\Sigma )=\iint_{\Sigma }A \cdot n dS

        

        我们在电磁学中接触过类似的概念,比如磁通量。实际上我们知道光也是一种电磁波,所以我们也有“辐射通量”的概念,它指的是通过某一区域的辐射功率总和。我们也可以称之为incident flux/engergy。

        比如在前文中介绍半球积分的时候,我们实际上就是在计算通量,曲面Σ对应半球,场向量A(x)对应于平行光的方向,A·n就相当于是L· n,也就是cosθ。

        我们再回顾一下之前提到的radiance和irradiance概念,我们现在可以给出radiance和irradiance基于辐射通量的定义。

         irradiance(辐照度):

        E_{e}=\frac{\partial \phi }{\partial x}       

        radiance(辐射率):

        L_{e}=\frac{\partial^2 \phi }{\partial \Omega \partial (Acos\theta)}

        这里的A是面积,Ω是立体角,Acosθ对应投影面积(投影到xy平面)。由此可见irradiance是对辐射通量关于单位面积的一阶偏导,它描述了单位表面上的光照,我们通常用它来描述某个点接收的光照。radiance是辐射通量关于单位面积和单位立体角的混合二阶偏导,它描述了沿着某个方向的光照,我们通常用来描述某个方向入射的光照。

这里要注意的是radiance并不是和入射绑定的,irradiance也不是和接收绑定的,只是我们在实时渲染中通常会用在这种情况上;它们最终能代表的含义完全由其数学表达式给出。

        我们可以这么理解这两者为什么分别是一阶偏导和二阶偏导。irradiance描述的是某个面元上的光照,那么这个光照可以来自多个方向,只要它最终落到这个点上就可以了,所以它是对面元做一阶偏导的情况。而radiance只关心在这个面元中某一方向上的光照,所以还需要对立体角再进行一次偏导。

        ​​​​​​

        由此也就可以解释我们在计算irradiance时总是需要对立体角做计算半球积分,因为我们需要累加所有立体角上的结果(dw或者dΩ)。

        再来看radiance,由于radiance是和立体角和面元都相关的,看起来它会更加复杂一些,但实际上我们通常会给出以下近似,我们认为平行光和点光源的可见立体角都为0,这意味着我们在单位面元上所有立体角上只有一个结果。

        但是现实中的光源是面光源,一个面光源在一个点上贡献的radiance是在可见立体角上积分的结果,这个结果这对面光源来说就是irradiance。也正是因为有了面光源,才会有软阴影的效果,这实际上是在模拟遮挡的边界处只有一部分立体角的光源可见的效果。

积分乘积的近似

        我们在后文中会计算一些非常复杂的积分,通常是乘积的积分形式,如下所示:

        \int_{S}f(x) g(x) dx

        这里f(x)和g(x)都可能受到多个参数的影响,它们组合的数量也是乘积的关系,如果我们想要把不同参数组合对应的结果缓存下来的话数据存储量将是不可接受的。假如能够把这些积分分离计算就能避免这一问题,也就是把上述积分近似成如下形式:

        \frac{1}{\int_{S}dx} \int_{S} f(x) dx \int_{S} g(x) dx

        这个近似在渲染中得到了广泛的运用,主要是为了降低参数维度。我们可以使用中值定理来解释它的数学原理:

        \int_{S}f(x) g(x) dx = \int_{S} f(x) dx \cdot f(\xi ) (\xi \epsilon S)

        我们认为在积分的作用域区间必然存在一点ξ满足上式,如果我们把上式中f(ξ)替换为f(x)的平均值就得到了上述近似公式:

        f(\xi ) \approx \frac{\int_{S}^{}f(x)dx}{\int_{S}^{}dx}

        在f(x)函数变化比较小的时候,我们认为f(ξ)可以被近似于平均值。当f(x)是一个常数的时候,结果是精确的。

正交性质

        空间中,如果两个向量的内积为0,那么我们认为这两个向量正交。

        同理,如果空间中一组基底两两正交,那么我们认为这是一组正交基;如果其中每个基向量的模长都为1,那么我们认为这是一组标准正交基,正交基这一概念存在于有限维和无限维空间中。

        对于一组多项式fi和对应的权重函数W(x),我们可以定义它的内积:    

        \left \langle f_{m},f_{n} \right \rangle = \int_{a}^{b} f_{m}(x) f_{n}(x) W(x) dx     

        如果对于m≠n,内积的结果为0,即 \left \langle f_{m},f_{n} \right \rangle = 0 ,我们认为这是一组正交多项式。

        此外,对m=n,如果 \left \langle f_{m},f_{n} \right \rangle = 1,我们认为这是一组归一化的正交多项式。

拉普拉斯变换

        拉普拉斯算子可以用于计算多元函数的二阶偏导数。 

        对于一个二元函数,每个维度的变化(一阶偏导数)我们可以使用一个向量来描述,称为梯度。我们把这个向量转换为标量,也就是对每个维度再次求偏导数并求和,称为散度。计算二阶偏导(即梯度的散度)的过程可以称作拉普拉斯变换,对微分方程进行二阶偏导计算的算子被称为拉普拉斯算子,如下所示:

        \Delta f = \frac{\partial^2 f}{\partial x^2} + \frac{\partial^2 f}{\partial y^2} + \frac{\partial^2 f}{\partial z^2}

        对于一些球面对称的微分方程,我们通常会使用球面坐标系下的拉普拉斯方程,接下来我们来推导这一公式。

        我们首先根据之前推导的球面微元,推导球面坐标下的梯度,我们需要在方程中配出球面微元,因此要在乘以一个数的同时除以这个数:

        df(r,\theta ,\varphi ) = \frac{\partial f}{\partial r} dr + \frac{\partial f}{\partial \theta } d\theta + \frac{\partial f}{\partial \varphi } d\varphi

        = \frac{\partial f}{\partial r} dr + \frac{\partial f}{r\partial \theta } rd\theta + \frac{\partial f}{ r\sin \theta \partial \varphi } r\sin \theta d\varphi

        提取出前面的系数,得到梯度向量:

        \left (\frac{\partial f}{\partial r}, \frac{\partial f}{r\partial \theta }, \frac{\partial f}{r \sin \theta \partial \varphi } \right )

        记作A = (A_{r}, A_{\theta }, A_{\varphi }),它是空间中的一个向量场。

        梯度的散度计算会更加复杂一些,相当于分别计算三个维度对体积微元(\sin \theta d\theta d\varphi)的偏导数并求和:

r:\frac{A_{r}(r+dr)\cdot (r+dr)d\theta \cdot (r+dr)\sin \theta d\varphi -A_r(r)\cdot rd\theta \cdot r\sin \theta d\varphi }{dr\cdot rd\theta \cdot r\sin \theta d\varphi }

\theta :\frac{A_{\theta}(\theta+d\theta) \cdot dr\cdot r\sin (\theta+d\theta ) d\varphi -A_\theta (\theta) \cdot dr\cdot r\sin \theta d\varphi }{dr\cdot rd\theta \cdot r\sin \theta d\varphi }

\varphi :\frac{A_{\varphi }(\varphi +d\varphi )\cdot dr\cdot rd\theta - A_{\varphi }(\varphi )\cdot dr\cdot rd\theta }{dr\cdot rd\theta \cdot r\sin \theta d\varphi }

        求解得到球面坐标下的拉普拉斯方程:

        \triangledown ^{2}f = \frac{1}{r^{2}}\frac{\partial }{\partial r}(r^{2}\frac{\partial f}{\partial x}) + \frac{1}{r^{2}\sin \theta }\frac{\partial }{\partial \theta }(\sin \theta \frac{\partial f}{\partial \theta })+\frac{1}{r^{2}\sin ^{2}\theta } \frac{\partial^2 f}{\partial \varphi^2}

        拉普拉斯方程本质上是一个偏微分方程。使用分离变量法求解拉普拉斯方程时,我们得到伴随勒让德多项式,它满足正交性,即:

        \int_{-1}^{1} {P_{l}^{m}(x)}^{2} dx = \frac{(l+m)!}{(l-m)!}\frac{2}{2l+1}

        伴随勒让德多项式可由勒让德多项式求m次导得到:

        P_{l}^{m}(x) = (1-x^2)^\frac{m}{2}P_l^{(m)}(x)

        只考虑球面空坐标下的角度分量,我们得到球谐函数的多项式:

        Y_{l}^{m}(\theta,\varphi ) = \sqrt{\frac{(l-m)!}{(l+m)!}\frac{2l+1}{4\pi }}P_{l}^{m}(\cos \theta )e^{im\phi }

        这是空间中的一组无穷级数的正交基底。

        蒙特卡洛积分

        对于一些复杂的积分,有时候我们难以计算其原函数。因此通常会使用统计中的蒙特卡洛方法去近似这个积分的结果。

        在微积分的理论中,计算一维积分的几何意义即相当于计算曲线围成的面积,而计算曲线围成的面积相当于计算x轴上均分的无限个小矩形面积的和,我们假设分割成n个矩形,那么上述情况可以描述为:

        \int_{a}^{b}f(x)dx=(b-a)\lim_{n \to \infty }\frac{1}{n}\sum_{i=1}^{n}f(x_{i})

        在n趋向于无穷,或者说计算无限个小矩形的和时,我们可以得到精确的结果;换言之,如果我们把面积划分为有限个矩形,我们就可以得到近似精确的结果:

        \int_{a}^{b}f(x)dx\approx (b-a)\frac{1}{n}\sum_{i=1}^{n}f(x_{i})=\frac{1}{n}\sum_{i=1}^{n}\frac{f(x_{i})}{\frac{1}{b-a}}

        上述采样方式是均匀采样。

        如果我们明确知道函数分布的倾向性,那么我们可以考虑带权重的采样,即重要度采样,这通常会让我们在相同的有限数量的样本中取得更好的效果。

        在重要度采样中,我们引入了概率密度函数p(x),我们可以认为均匀采样的概率密度函数均为1/(b-a),是非均匀采样的一种特殊情况:

        \int_{a}^{b}f(x)dx=\int_{a}^{b}\frac{f(x)p(x)}{p(x)}dx\approx \frac{1}{n}\sum_{i=1}^{n}\frac{f(x_{i})}{p(x)}

        因此,我们离散的选取N个点,并分别除以对应的概率密度函数,最后相加求平均,即可无偏的估计蒙特卡洛积分。

        切比雪夫不等式

        切比雪夫不等式是统计学中的一个不等式,它描述了数据在给定区间内的分布概率:

        P(x>a)\leq \frac{\sigma ^{2}}{\sigma^{2}+(a-\mu )^{2}}(a>\mu )

        其中,σ^2是方差,μ是期望值。

        也就是说,如果我们已经得到了整个样本的统计数据,即在确定方差和期望值的情况下,那我们就能够知道取值大于某个特定值(a)的概率的上界。

间接光照

        在实时绘制中,我们有非常多种模拟全局光照的方案,这些方案各有自己的优劣之处,通常我们会根据不同的光源选择不同的方案。这些方案不一定是互斥的,它们可以同时存在,对不同光照使用不同方案组成了整个全局照明系统。

        实际应用

        实时模拟渲染中,我们会把光照分为两个部分进行计算,一个是直接光部分,也就是光源沿着光路的直接照射,我们在学习光照系统的时候会首先了解这一部分。如果我们在应用中只考虑了直接照明,那么通常只有向光面是被照亮的,处在阴影中的部分是全黑的,只有少量材质比如次表面散射在暗处也是有颜色的,这个时候对比度非常高,会带给人非常不真实的感觉,因此还需要考虑间接照明的计算。

        我们会把经过了两次反弹,以及更多次反弹的光照统一归类到间接光照里,从表现上来看它可以为暗处提供补光。一般来说不应该将直接光的计算方式直接应用到间接光上,因为如前所述间接光的表现与直接光是不尽相同的。

        实时渲染的基本思路是尽可能离线生成一些光照信息,根据不同的侧重点这些离线数据记录了不同类型的信息,并且存储为不同形式,我们可以通过直接读取或者通过一些计算得到最终间接照明的结果。需要注意的是,除了直接存储光图这种形式,大多数全局光照方案最多考虑到金属和电介质的差异(即漫反射和镜面反射),对于一些比较包含散射、折射等光线会在物体内形成更加复杂传播路径的材质,我们可能需要一些特别的处理手段,比如使用公式去拟合某些光照分量。

        实时渲染最终的目标是看上去正确,并且兼顾性能,要保证看上去正确,我们至少需要保证贡献度高的光照分量的完整,这一点是非常重要的。我们经常会犯的一个错误是暴力的选择砍掉某一个分量来节约性能,但更佳的选择是简化其它分量的计算方式,或者充分认知到分量移除带来的影响:

        ● 比如使用光泽度(sheen)来近似拟合多重散射分量,而不是直接移除散射效果;

        ● 移除阴影或环境光遮蔽分量时考虑到由此可能导致的漏光,减弱照明或者通过牺牲其他分量的计算复杂度来保留阴影/遮蔽分量;

        ● 移除间接光颜色信息考虑到由此带来的颜色信息丢失,比如一把红色的遮阳伞下的地面通常也会染上红色,我们可以选择移除这个红色的局部信息,替换为全局的环境色调,但不应影响整体的明暗度,这样的画面依然不会显得过于塑料;

        ● 每在场景中添加一个新的光源时要考虑到其产生的间接光,哪怕是提供非常粗糙的补光形式,尤其是在本身就比较暗的室内;如在风格化渲染中给影子加上偏蓝的色调,或是使用Half Lambert/warp diffuse,虽然很简单但实际上也是在模拟间接光;

        总结来说,全局光照虽然看起来是一个非常复杂的话题,但在实际项目开发中,由于性能吃紧,我们几乎不可能用非常复杂的算法,因此大部分计算都是离线完成的,运行时的计算通常都不会过于复杂。很多时候,我们需要在不同分量之间的权衡,保证整个画面整体的和谐性和通透感。

        存储形式

        接下来我们来讨论几种常见的间接光表达形式:

        光照贴图

       如前所述,光照贴图(LightMap)是一个得到了广泛应用的模拟全局照明的方案。由于是静态烘焙的,理论上能够得到非常真实的效果。缺点是烘焙时间长,占用内存/磁盘高,只能作用于静态光源/物体。

        换句话说,只要能够接受内存占用和加载的负载,精度足够,光图可以表达绝大多数光照信息,包括一些高频信息。并且它的存储只和场景物件相关,和光源数量无关。

        光照贴图存储的是最终计算出来的结果,实际上我们可以认为它存储的就是irradiance信息,我们通过一次采样就可以直接应用于渲染,广义上的光图中还可以包含拆离出AO等光照分量的贴图。

        光图本身的使用原理十分简单,我们在将光图应用到项目中时更多需要考虑到的是工程本身的细节,比如我们应该如何实现光图到物体表面像素的映射?在什么情况下光图会发生失真等。

        应用场景

        光图可以用来表达包括直接光和间接光的几乎所有光照分量,在实际应用中我们会根据实际情况来选择存储的分量。假如制作一个固定时段固定光源种类的场景,那么光图就是完全适配的。比如一个摆放了很多局部光源的室内场景,室内意味着它的光照环境非常稳定,一般不会受到外界天气、时间的影响;局部光源多是因为这种室内从设计上一般都会使用大量灯光来保证。如果实时计算的话会因为光照范围重叠而产生性能损耗,间接光部分也需要综合考虑不同方法来模拟,但是如果烘焙到光图中就省掉了所有麻烦。

        但是对于包含昼夜变化的室外场景并不合适,因为环境光是在时刻发生变化的。比如黄昏、清晨、夜晚、午后的天光的色调都有着明显的差异,除非我们为某种情况都去烘焙独立的光图,但这对于内存显然会产生较大的压力。不过在这种情况下,我们依然可以选择存储一些不受影响的分量,比如记录天光遮蔽、环境光遮蔽、来自场景的间接光信息的贴图。

        光图uv

        烘焙光照贴图通常以物体为单位进行存储,因此难以复用。我们通常需要将场景中每个三角形映射到光图上的一些像素,为了实现更合理的映射,我们通常需要为每个模型生成单独的光图uv,而不是复用纹理uv。因为光图的光照和场景三角形是一一映射的关系,所以我们要求无重叠的uv,而纹理uv不一定满足这个需求。

        此外,光图uv通常会基于均匀映射来生成,这样方便引擎统一对贴图进行自动生成。如果我们想要提高某一部分的重要度,才去考虑手动生成非均匀的拉伸uv。为了保证光图uv的无重叠,我们会把网格拆分成多个块,每个块进行独立参数化,生成图表(chart),最终打包到一个纹理里。

        此时光图中相当于存储的是多个图表的集合,或者说是多张图表pack到一起的结果。考虑到双线性采样会访问到邻近像素,那么在采样某个特定的图表时可能访问到隔壁的图表,此时会发生colorblending(颜色溢出/渗色)现象。一个简单的规避方式是在不同uv图表之间保留足够的填充(padding),至少预留2个像素的间隔,确保一个quad里不包含两个图表的内容。

        在拆分图表时,如果uv面积和原三角形面积有最大偏差,那么意味着光照贴图精度不足,会导致光照结果产生不连续的锯齿瑕疵,如果图表对应的分辨率不变,拆分的图表数量变多,精度也会提升。此外,不同图表之间也可能会发生过渡的接缝问题,需要对生成的光照贴图进行后处理或者生成图表时添加约束来处理。

        还有一个值得去注意的一个问题是,通常是每个模型对应一个光图,但我们在制作模型时往往制作多级LOD,理论上我们需要为每个LOD制作一个光图并生成独立的光图uv。

        光图的存储形式和场景物件密度密切相关,我们期望另外一种与物体无关的记录形式,也就是说我们只需要记录某处周围场景的信息,而物件可以从场景中采样所需的光照信息,也就是将一些中间的光照信息缓存下来,然后“注入”到场景。这是一种非常常见的思路,很多GI方案都是基于这个思路展开的,差异在于它们存储的信息所有差异。

环境贴图

        基于这样的一种思路,一种最为简单的方式是,我们可以像处理天空球那样使用cubemap来存储场景的光照信息。这就是我们熟知的IBL(基于图像的照明),也是我们去了解环境光照计算通常首先会接触到的一种形式。

        我们在求解环境光照的时候,实际上是在求解如下积分:

        \int_{\Omega }L_{i}(l)f(l,v)cos\theta dl

        这里f(l,v)是BRDF的计算公式,L(l)是入射光的颜色,这里我们忽略了可见性函数,我们会在后面的章节中再去讨论可见性分量。基于图像的照明实际上是提供了这种公式中L(l)的输入,也就是我们会把场景周围的radiance信息首先存储到cubemap中,相当于我们实际上使用这个图像来提供间接光照明,对场景进行补光,这种记录radiance信息的贴图我们也可以称之为光照探针(light probe)。

        由此可见这里基于图像的意思就是在计算光照的时候我们已经不关心具体的场景是什么了,我们只需要有这张环境光的radiance信息就足够了。因为我们知道在Path Tracing的时候,需要递归的去求解多次反弹的结果,但是环境贴图的近似相当于忽略了所有的这些过程,我们就认为某个方向对应的贴图结果就是所有间接光照的输入,包括了一次和多次反弹的结果。

        来源

        为了提供间接光照信息,首先需要捕获周围各个方向的场景radiance信息。这种捕获通常是离线准备的HDR全景环境图,针对不同的环境准备多张进行切换;如果考虑到动态场景的效果,也可以实时捕获,兼顾到性能,这可能是在场景发生了较大改变的时候才去触发捕获。

        应用场景

        思考一下,当我们使用cubemap存储光照信息的时候,我们实际上是选择了空间中的某一个点去采集周围的光照,或者我们准备了一张假的光照贴图认为它是空间中某一点周围的光照。这实际上包含了如下信息:

        1)采集到的场景是静态的

        2)cubemap是空间中特定位置采样的结果

        这些特点也就意味着如果我们使用cubemap来描述局部光照信息是十分不准确的,因为实际的cubemap所在的位置离采样点总是有距离上的偏差。为了减少这一误差,根据场景的大小,我们需要在场景中摆放多个cubemap,然后去做一些权重上的混合。

        但有一种比较特别的情况,那就是天光,因为我们可以认为天空球无穷远,那么场景将可以视为质点,因此我们得到的天光数据是更加准确的。

        我们接下来讨论cubemap应用于不同光照分量的情况:

       漫反射

        如果要基于BRDF做环境光拟合,相当于要计算这个图像对每个点的照明,入射光线可以分布在法线的半球区域,所以相当于计算这个区域内所有方向的BRDF并求和,也就是计算BRDF的半球积分。

        f(l,v)=\frac{c_{diffuse}}{\pi}


        由于f(l,v)只与入射方向有关,因此得到漫反射的积分结果是比较简单的,积分结果就是漫反射的irradiance,我们可以离线或实时将其烘焙到cubemap中,在实时计算中只需要依据法线方向通过一次采样即可。

reference《Real Time Rendering》 irradiance漫反射计算:余弦加权半球采样

        我们在半球积分一章中介绍过半球积分的极坐标形式为 ∫∫ cosθsinθ dθdφ,在离线烘焙的时候我们将其转换为黎曼和的形式,每个积分项只需多乘一个从cubemap采样的radiance就可以了。

for(float phi = 0.0; phi < 2.0 * PI; phi += step)
{
    for(float theta = 0.0; theta < 0.5 * PI; theta += step)
    {
        float3 dir = float3(sin(theta) * cos(phi),  sin(theta) * sin(phi), cos(theta));
        irradiance += texture(radiance, dir) * cos(theta) * sin(theta);
        num++;
    }
}
irradiance = PI * irradiance / num;
reference《Real Time Rendering》 radiance贴图与漫反射irradiance贴图
         完美镜面反射

        如果我们把radiance的cubemap作为镜面反射的结果,这就相当于认为光线全部从反射方向出射形成了完美镜面,在这个情况下,我们只需要去计算反射向量,并根据反射向量去采样radiance。

        r=2(n\cdot v)n-v

        上面所说的是完美镜面的情况,也就是所有反射光线都从一个方向出射。但实际上反射光线可能会分布在反射方向邻域的一块区域,形成了一个镜面波瓣,并且随着粗糙度变大,反射方向对应的立体角范围会变大。

reference《Real Time Rendering》完美镜面反射和镜面波瓣

        假设我们认为反射方向是沿着反射向量径向对称分布的,如果我们想要模拟镜面波瓣的反射效果,可以直接这个基础上对图像做预过滤,并且越靠近反射向量的方向权重越高。我们使用的滤波核越大,相当于会取更多方向的radiance参与滤波,那么对应的就是更粗糙的表面。

        并且考虑到立方体到球面并非线性映射,我们对于图像也需要做非线性模糊。

        这个方法有两个缺点,一个是由于数据是预先处理的包含了完整方向的积分结果,因此它无法处理光线被地平线裁剪时的遮蔽,导致掠射角度过亮。

         光泽反射

        根据我们接触的BRDF公式,我们知道镜面波瓣通常不是径向对称的,并且它的形状会受到观察方向的影响,这意味着我们无法通过简单的滤波来模拟它。假如我们需要考虑镜面波瓣的形状,也就是使用BRDF来求解镜面高光的话,我们考虑求解一次完整的光照积分,f(l,v)的计算为:

        f(l,v) = \frac{D(h)F(v,h)G(l,v,h)}{4(n\cdot l)(n\cdot v)}

        和漫反射不同的是,镜面反射的结果并不是在半球上均匀分布的,而是集中分布在反射向量附近,所以我们通常不采用均匀采样,而是使用重要性采样。 

        这个BRDF的公式要比漫反射复杂得多,但这些复杂的计算我们可以使用离线烘焙的方式来完成。真正复杂的地方在于这里的变量包括v(出射方向),l(入射方向),roughness(粗糙度),F0(基础反照率)。也就是说我们需要考虑去为这些变量的不同组合去存储结果,最终得到的数据是一个非常高维的信息,这是不可接受的。

        因此,对于镜面反射来说,最需要去解决的问题是降维。关于这一方面虚幻引擎给出了非常好的解决方案,核心的思想是把原来包含了不同参数的表达式乘积的积分近似为积分的乘积,从而独立去缓存每个参数对应的结果,这样存储的维度就降低了      https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf

        这个方案的近似包含了两个步骤,一个是把radiance信息从积分中独立出来。根据前面数学章节提到的内容,在环境光变化不大时我们可以做出这样的假设。        

式中的p是使用蒙特卡罗方法后,重要度采样的pdf,积分形式没有这一项

        从积分中提取出来的相当于radiance均值滤波的结果,这个结果我们可以缓存到cubemap上,和漫反射的irradiance贴图相比,它少了一个余弦项,并且滤波核会根据镜面波瓣对应的立体角改变大小,也就是我们需要缓存一系列mipmap的结果去对应不同大小的滤波核。

        第二步是处理剩下一部分包含BRDF的积分,由于我们已经把radiance信息提取出去了,所以剩下的部分相当于对一个白色的环境贴图做BRDF镜面反射的积分,但是这个时候我们仍然还有三个变量。

        我们乘上菲涅尔schlick近似项的表达式,再除以菲涅尔项F(v,h)本身,可以把积分项拆成两个,这个时候我们就可以把F0项提取到积分外部了,考虑到F0本身描述了一个颜色信息,能够把它提取出来对我们帮助很大:

        上式可以看作A * F0 + B的线性结果,A是F0的一个缩放系数,B是F0的一个偏移系数。这个时候变量只剩下了roughness和v,这个时候已经足够我们去做预计算了。

        我们可以把A,B的结果存储到一张具有两个通道的2D贴图中(LUT),两个通道分别存储A/B的结果。贴图的uv分别为roughness和cosθ,这两个参数正好和uv一样分布在[0,1]区间。

        自此,我们就可以通过一次预过滤贴图的采样,和一次LUT贴图的采样获得镜面反射的结果。

        遮蔽

        我们也可以存储环境光遮蔽、天光遮蔽等信息。但是由于这部分信息的局部性很强,所以一旦精度或摆放密度不足会有比较大的误差。在采样位置不够精确的情况下,我们只能得到一个大致的遮蔽信息,比如某个地方偏暗,某个地方偏亮,但比较难去表达一些高频的接触遮蔽的效果。

        实际上这个误差也会出现在漫反射和镜面反射上,但是人眼对于颜色信息相对来说没有那么敏感,比如说即使我们提供了一张错误的反射贴图,反射了一些场景中并不存在的东西,这也不是那么容易被发现的。所以即使在环境光照发生突变的交接地带,这种错误通常也能被容忍。但是假如是在室内与室外交界的地方,我们在室内采样到了室外的信息,就会发生漏光的现象,这是更容易被发现的瑕疵。

球谐函数

        我们在前文漫反射信息则是均匀滤波的结果,它的光照信息非常平滑,相当于它非常低频,所以我们可以用比较低的分辨率去存储环境光的irradiance贴图。

       换个角度来说,我们也可以用更简单的形式去存储漫反射的结果而不是使用cubemap。我们知道环境光漫反射信息本质上是定义在球面上的一个二维函数,因此我们可以使用球面上的球谐函数作为基函数去表达它,那么irradiance信息可以转换为使用一组球谐系数表示的向量。

       我们通常使用球谐函数的频域表达形式,在这种形式下,球谐函数具有无穷的阶数。每一阶基函数表达了不同频率的信息,依次为常值函数,线性函数、二次函数等等,越往后次数越高,表达的频率越高,数量也越多。

reference《Real Time Rendering》按照频带进行排列的球谐基函数,每行分别为常数、线性函数、二次函数等,越往后变化越快,表达的频率也越高,数量也越多
 

        我们对以上球谐函数做了特殊编码,其中不同行使用不同的 l 值,代表了不同的频带,称作频带指数,每行有2l + 1个球谐基底,它们从左到右的编码m分别为 -l 到 +l。

        其中,球谐多项式为:

        B_{l.m}(\theta,\varphi )=A_{l,m}P_{l}^{m}(\cos \theta )e^{im\varphi }

        A_{l,m}为归一化系数,它使得\int B^{2}dw = 1,即:

        \int_{\Omega }^{} B_{p} B_{q} dw = \left\{\begin{matrix} 0,p\neq q\\ 1,p=q\end{matrix}\right.

        上式说明球谐基底具有正交性。

        归一化系数

        由\int B^{2}dw = 1知:

        1 = \int {B_{l,m}(\theta,\varphi )}^{2}dw = \int_{0}^{\pi}\int_{0}^{2\pi}{B_{l,m}(\theta,\varphi )}^{2}sin\theta d\theta d\varphi

        = {A_{l,m}}^{2}\int_{-1}^{1} {P_{l}^{m}(cos\theta)}^{2} d(cos\theta) \int_{0}^{2\pi} {e^{im\varphi}}^{2}d\varphi

        = 2\pi{A_{l,m}}^{2}\int_{-1}^{1} {P_{l}^{m}(u)}^{2} du ( u =cos\theta)

        = 2\pi{A_{l,m}}^{2}{A^{'}_{l,m}}^{2}

        自此,我们得到球谐多项式归一化系数和伴随勒让德多项式归一化系数之间的关系为:

        A_{l,m} = \frac{1}{\sqrt{2\pi} A_{l,m}^{'}}

        因此球谐归一化系数为:

        A_{l,m}=\sqrt{\frac{2l+1}{4\pi}\frac{(l-m)!}{(l+m)!}}

        环境光漫反射以低频信息为主,因此我们可以只使用前几阶描述低频的系数来表达,一般在实时渲染会用到2~3阶信息就足够了,我们在运行时可以使用前几阶的系数重建原来的结果,能够得到非常近似的结果。

        投影与重建

        球谐函数虽然本身理论比较复杂,但它的实际计算是非常简单的。我们对其做的主要操作就是预处理的时候投影到球谐基函数并丢弃高频分量,运行时重建回原始数据。

        正如我们在笛卡尔坐标系中,把三维分量分别投影到x轴、y轴、z轴上,我们分别会得到一个系数,当我们把光照信息投影到球谐函数上时,也会得到每个频带上的系数,这样一组球谐系数存储下来就可以表达光照信息,比如漫反射的irradiance。我们可以认为投影的过程就是球谐编码的过程。

        计算投影的方式如下,本质上是计算这两个函数的内积。其中f(w)是我们需要编码的光照函数,Bi(w)是球谐基底,ci是球谐系数:

        ci=\int\mathit{f(w)}\mathit{B(w)}\mathit{dw}

        我们认为f(w)可以写成Σci B(w)的形式,我们从这个形式出发开始推导:

        计算球谐系数(投影)是我们比较熟悉的一重积分过程,我们依然可以使用蒙特卡罗方法进行均匀采样。注意到球谐是定义在整个球面上的,所以我们还需要除以球面的归一化系数4PI。

        重建也就是解码的过程,它的计算正是我们推导投影时所用的形式:

        f(w)=\sum c_{i}B_{i}\left ( w \right )

        由于我们在实际计算中只会使用到前二阶(4个系数)或者前三阶(9个系数)的球谐基底,所以实际上编码和解码的复杂度都不算太高。

        球谐卷积

        我们在上文中介绍了如何用球谐基函数去表达任意函数,以及编码和解码的过程。

        接下来我们考虑另外一个问题,假如我们计算两个函数乘积的积分,那么这个操作就非常像在两个图像中做卷积的过程,我们计算函数每个值两两相乘的结果,并乘以归一化系数再求和。

        如果我们把这两个函数使用球面坐标来表示,那么我们可以利用球面的正交性来简化这一结果。也就是说在常规的卷积中我们需要考虑任意两个值相乘的结果,包括同下标和不同下标的组合,最终可能有n * n次乘积计算,而在球谐函数不同基底之间是正交的,它们相乘的结果为0,因此相当于我们只需要考虑相同下标的组合,最终只有n次乘积计算。

        

        如上图所示,我们认为两个函数乘积的积分在转换到球谐基底上后,相当于两个函数球谐系数的点积,再乘以归一化系数。通过这样的方式我们就把复杂的积分计算转换为简单的点积计算,我们认为这是球谐函数卷积的一个比较好的性质。

        不幸的是,这种性质最多只能支持两个函数的乘积,对于三个以及更多的乘积因子,为了应用卷积性质,我们需要将这些乘积因子以任意形式组合成两个乘积的形式,这对我们数据存储的形式也有很大的影响。

        编码radiance

        我们接下来来讨论我们可以用球谐函数来缓存哪些分量。 

        首先正如我们在前面提及的,环境光的漫反射信息本身是一个低频信息,因此我们可以直接将irradiance信息用球谐函数来存储。相比起我们之前存储到立方体贴图的做法,这种形式足够的省,并且重建数据也只是一个简单的点乘计算。

        此外,考虑到刚刚介绍的球面函数乘积积分的性质,我们可以不用球谐来编码irradiance信息,而是直接编码radiance信息,通过radiance球谐系数和clamped余弦球谐系数相乘实现快速irradiance滤波。这也为我们快速应用动态捕获的radiance贴图提供了可能性。

        这里我们提到的clamped余弦的球谐系数是固定的,并且clamped余弦函数有一个特殊的性质,那就是它绕着球体z轴旋转对称,因此它是与参数φ无关的函数。

        这种旋转对称的函数投影到球谐基底后,每个频带中只有一个非零系数(m = 0),也就是说对于二阶球谐只有两个有效值(A0,A1,A1,A1),对于三阶球谐只有三个有效值(A0,A1,A1,A1,A2,A2,A2,A2,A2),我们称之为球带谐波(zonal harmonic)。

        球带谐波的计算如下:

        A_{l} = \int_{0}^{2\pi }\int_{0}^{\frac{\pi }{2} }\cos\theta ^{+}B_{l}^{0}(\theta_{i })\sin \theta _{i}d\theta d\varphi

        由于带状谐波与参数φ无关,上式可简化为:

         A_{l} = 2\pi \int_{0}^{\frac{\pi }{2} }\cos\theta B_{l}^{0}(\theta_{i })\sin \theta _{i}d\theta

        代入勒让德多项式,得到(由于m=0,阶乘项被消除):

        A_{l} = 2\pi \sqrt{\frac{2l+1}{4 \pi}} \int_{0}^{\frac{\pi }{2} }\cos\theta P_{l}^{0}(\theta_{i })\sin \theta _{i}d\theta        

        = -2\pi \sqrt{\frac{2l+1}{4 \pi}} \int_{0}^{\frac{\pi }{2} }\cos\theta P_{l}^{0}(\theta_{i })\ dcos\theta

        已知球谐多项式P_{1}^{0} = cos\theta,令u = cos\theta,我们通过交换积分上下限消除负号,并把cosθ替换为P(1,0),得到球带谐波的表达式:

        A_{l} = 2\pi \sqrt{\frac{2l+1}{4 \pi}} \int_{0}^{1} P_{l}^{0}(u) P_{1}^{0}(u) du= \sqrt{(2l+1)\pi} \int_{0}^{1} P_{l}^{0}(u) P_{1}^{0}(u) du

        举例来说,若l = 1,我们得到:

        A_{1}=2\pi\sqrt{\frac{3}{4\pi}}\int_{0}^{1}{​{P_0^{1}(u)}^{2}}du=2\pi\sqrt{\frac{3}{4\pi}}\int_{0}^{1}{u}^{2}du=2\pi\sqrt{\frac{3}{4\pi}}\frac{1}{3}=\sqrt{\frac{\pi}{3}}

        因此我们根据上式通过数值积分求得clamped余弦函数的带状谐波,并在运行时查表获得这个值,用于radiance的快速卷积计算。

        此外,和常规卷积一样,我们也需要考虑这里乘积积分的归一化系数,我们称为频带常数,它是球谐系数归一化系数的导数:

       w = \sqrt{\frac{4\pi}{2l+1}}

        我们同时考虑球带谐波和频带常数,可得:

        \hat{A}_{0}=\sqrt{4\pi} \sqrt{\frac{\pi}{4}}=\pi

        \hat{A}_{1}=\sqrt{\frac{4\pi}{3}} \sqrt{\frac{\pi}{3}}=2.094395    

         \hat{A}_{2}=\sqrt{\frac{4\pi}{5}} \sqrt{\frac{5\pi}{4}}\frac{1}{4}=0.785398

        最终irradiance的球谐系数为radiance球谐系数和\hat{A}的乘积:

        E_{l}^{m} = \hat{A}_{l}L_{l}^{m}

        振铃现象
reference《Real Time Rendering》 函数:clamped余弦函数 蓝色:球谐近似 红色:原始函数 

        如上图,分别是原始信号cos^{+}和球谐拟合的cos^{+}信号,可见原始余弦函数在\left [ \frac{\pi }{2},\pi \right ]区间值恒定为0,而拟合的球谐函数却是轻微“振荡”的,并且可能会得到小于0的非法值,这种现象被称为振铃现象。

        在\pi /2处把函数clamped到0带来了原始函数图像的突变,意味着这里具有一个较为高频的信息,但由于高频分段被舍弃,最终的拟合在此处会具有较大的误差。

        振铃现象在我们用少量低频基函数近似高频信息的时候会发生,比如在物体阴影附近可能会形成异常的亮斑。

        我们通常采用“窗口化”的做法缓解这一现象。

其它基底函数

        除了常见的二阶和三阶球谐函数,出于对性能或者对效果的考虑,不同项目可能会选择类似球谐函数的其它球面或非球面函数作为基底函数(reference:《Real Time Rendering》)

reference:《HEMISPHERICAL LIGHTING INSIGHTS》

        以AHD基底为例,它将照明分解为环境光和平行光,能够简单有效地拟合光照结果:

        I(w)=C_{a}+max(n\cdot d)C_d

        其中,Ca是环境光颜色,Cd是平行光颜色,d是光照方向。Ca对应了一个纯色,主要用于近似环境光,而Cd则考虑了余弦项,主要用于近似高光,d对应高光方向,表现为上图中的两个不同球体。

        AHD存在的问题是,它不是线性光照模型,因此在混合的时候可能会产生错误。

        为了优化这一点,我们使用了改进的AHD基底,它存储irradiance(I_z),权重(w_a)和高光方向(d),这些值对于插值更友好,并且存储空间更小。而环境光颜色和平行光颜色可通过这些值求出:

        C_a = I_z w_a

        C_d=\frac{I_z(1-w_a)}{d_z}

        使用环境光拟合得到会得到过平的结果,因此人们又提出了使用半球光来代替环境光的做法,称之为HHD基底,其计算最终光照的公式为:

        I(w)=\frac{1+n_v\cdot n}{2}C_a+max(n\cdot d)C_d

        可见半球光考虑了法线的影响,其中nv是顶点法线,相当于顶点法线和像素法线差距越大,半球光的贡献就越小,这相当于拟合了一个遮蔽项。

环境光遮蔽

概念引入

        遮蔽的概念非常接近于阴影的概念,阴影描述的是直接光无法照射/遮挡的地方,而遮蔽描述的是环境光/间接光无法照射/遮挡的地方。

        我们在介绍光照信息的时候,我们主要讨论的是光照的颜色信息。如果我们想要描述物体之间相互遮蔽,或者物体的自遮蔽效果,我们可以在环境光的半球积分中添加可见性项V,可见性项返回非0即1的结果,它在当前方向可见时返回1,不可见时返回0:

        L_{o}(v)=\int_{\Omega }f(l,v)L_{i}(l)v(l)(n\cdot l)^{+}dl


        其中l是光线方向,v是视角方向,n是法线方向,f(l,v)是材质的BRDF,上述公式描述了环境光受到遮挡时,需要将可见性项也作为积分的乘积之一。

        我们假设表面是Lambertian,我们将漫反射公式代入,并把L(i)和f(l,v)项从积分中提取出来,那么我们得到:

        f(l,v)=\frac{\rho _{ss}}{\pi}

        L(v)=\int f(l,v)L(l)v(l)(n\cdot l)^{+}dl

        =\int \frac{\rho _{ss}}{\pi}L(l)v(l)(n\cdot l)^{+}dl

        \approx \rho _{ss}\frac{\int L(l)dl}{\int dl} \cdot \frac{\int v(l)(n\cdot l)^{+}dl}{\pi}

        相当于我们把环境光遮蔽和平均照明分别提取出来,此时环境光遮蔽的定义如下:

        K_{A}=\frac{1}{\pi}\int_{l\epsilon \Omega }v(p,l)(n\cdot l)^{+}dl

        这意味着我们通常所说的环境光遮蔽是一个近似分量,它的物理意义相当于所有可能光线入射方向中,未被遮挡的光线方向占总方向的比例

深入理解

        正如我们所说,环境光遮蔽是一个近似物理分量,它是我们通过积分近似抽象出来的一个概念。

        实际上回忆之前介绍的Path Tracing算法,我们并没有特别的去计算可见性,我们只是不断地向周围发射光线,并收集累加光照信息。

        这是因为在定义光照方程中的“可见性”时,我们需要明确环境光的来源,如果我们认为可见性为0,说明该环境光无法直接照射到当前位置,光线与当前点的连线之间存在障碍物。

        但是在光线追踪中,光线可以经过非常复杂的路径进入眼睛,所以我们无论朝哪个方向发射,我们总能捕获到对应的radiance,但它们的来源大不相同。

        也就是说,虽然某处环境光对该点来说是直接不可见的,但其反弹的光线却可能是可见的,因此我们这里所说的可见性是针对具体的某条光线。

        在这类全局光照算法中,我们通常不直接描述可见性,实际上我们需要的是世界空间中物体的几何位置信息,有了场景信息,我们就能知道光线如何在其中传播,并且我们也能从其中推导出我们所需要的“可见性”信息。

        因此,如果我们使用环境光遮蔽分量,可能带来一些效果的瑕疵,这需要我们引入一些物理不正确的分量去规避这些问题,我们在接下来会详细介绍这部分。

遮蔽距离

        在考虑环境光遮蔽的时候,我们首先需要考虑环境光的来源。在上述给出的环境光遮蔽公式中,我们实际上认为环境光来自无穷远的地方,比如天光,那么山体、大型建筑都会对天光进行遮挡,那么按照光照方程,在室内计算得到的结果几乎是全黑的。

        但在现实中,室内环境通常不是完全暗的,因为对直接光线(天光、太阳光)的多次反弹,物体之间存在相互照亮的效果,我们认为这种来自于物体相互照亮的环境光的来源是比较近的。

        考虑这一部分近距离环境光产生的遮蔽,我们可以限定在一个有限的距离中来获取这一信息,也就是假设环境光只在这个有限距离里产生:

        K_{A}=\frac{1}{\pi}\int_{\Omega }\rho (l)(n\cdot l)^{+}dl

        和可见性函数v(l)非0即1不一样,ρ(l)是一个取值范围在[0,1]的连续函数。相交的距离越远,ρ(l)的取值越大。超过最大距离后,不再考虑计算。

        这样的话我们可以实现比较近距离遮挡表现,比如物体的自遮挡,比如墙角会偏暗,或者比较近的物体产生的遮挡,比如摆放在地面上的物体下方会偏暗。

        在实际工程中,为了区分这两种不同的遮蔽,我们使用两种不同的分量来描述:

        1.对于远距离的天光,我们使用Sky Occlusion(SO,天光遮蔽)来描述;

        2.对于特定距离内的反弹光,我们使用Ambient Occlusion(AO,环境光遮蔽)来描述;

不足

        1.方向性

         K_A直接和环境光的半球积分相乘,相当于只考虑了遮挡的占比,而完全忽略了方向性,也就是它认为半球方向上的所有光线都会对最终结果产生贡献。

        也就是说,当环境光变化越平滑的时候,AO和真实的结果就越接近,而当环境光随方向变化比较剧烈时,AO的近似效果就会比较差。

        举例而言,假设我们在一个左半边是红色墙面,右半边是绿色墙面的环境中摆放一个方块,该方块受到遮挡只能接收左半边的间接光照。如果我们使用AO的方式来计算,得到它的AO系数约为0.5,最后会得到一个红色和绿色混合的结果,而实际上我们知道它的结果应该仅是红色的,这正是因为AO的模拟忽略了不同方向上光照的贡献,造成了方向性信息的拟合不足。

        在另外一个例子中,假设空间中某个位置,左侧有小的遮挡物,而右侧是平坦的,那么理论上会产生略微向右拉长的清晰的遮挡,而在环境光中,则会生成看不清楚方向的,比较模糊的遮蔽。

reference:《HEMISPHERICAL LIGHTING INSIGHTS》
无方向性(左)和有方向性(右)的效果对比

        2.偏暗

        之前在描述AO的时候,在渲染方程中,我们认为如果某一方向的可见性为0,说明在这一方向存在物体遮挡,那么这一方向对应的环境光照就不产生任何贡献。

        在光线追踪中,我们计算光线”碰撞点“反射出来的颜色作为光照输入,但是同样的情况在AO中,我们却认为它是不可见的,并使其不产生贡献。

        这是因为在AO模拟的时候,我们定义了AO产生的“距离”,也就相当于忽略了在该距离内的物体产生的间接光照,而这部分光照通常是多次反弹的结果。我们认为与视点相距特定距离的所有位置会构成一个空间中的半球面,这相当于我们假设环境光来自半球面外,并且假设能够对环境光产生遮蔽的物体来自半球面内,因此我们最终得到的结果会偏暗一些。

        考虑到我们的近似没有考虑多次反弹的分量,我们可以对这部分能量损失进行补偿。比如之前提到的距离函数,它在特定距离内大部分情况下会返回非0的结果,这相比起直接使用非0即1的可见性结果整体就会更亮一些。或者我们可以对环境光遮蔽KA进行重映射,把它映射到一个更亮的范围中,作为多次反弹的近似模拟。

        更进一步,如果我们想要使用AO分量更正确地模拟间接照明效果,我们原本的计算为:

        L = K_A C_{light}

       这样的计算遗漏了相互反射部分,我们需要加上这部分的结果:

        L = K_A C_{light}+(1-K_A)C_{multiboundlight}

        这里的多次反弹照明是累加的结果,可能会通过离线计算或实时有限次反弹次数拟合获得。

定向遮蔽
        Bent Normal

        为了解决AO存在的方向性问题,我们可以使用Bent Normal来替换常规的法线来计算间接光照明。需要注意的是我们只用它来计算间接光,而不是直接光。

        Bent Normal又称为环境法线,它的定义是未遮挡方向的余弦加权结果,描述了平均的未遮挡方向,我们可以按照它的定义预计算该数据:

        n_{bent}=\frac{\int L(l)(n\cdot l)^{+}dl }{\left \| \int L(l)(n\cdot l)^{+}dl \right \|}

        如上图所示,当我们使用了bent normal之后,相当于将法线旋转了特定角度,计算旋转后的半球积分,相当于采样的环境光信息会往平均未遮挡方向偏移,它会影响到我们最终采样的环境光颜色。

        但是bent normal并没有真正地在未遮挡的角度上进行积分,而仍然在半球上进行积分,因为它只是修改了计算间接光照时的法线方向,这意味着bent normal只模拟了有限的方向性。它的好处是我们只需要额外存储一个bent normal,环境光的半球积分依旧可以预计算。

reference《Real Time Rendering》三种不同的方法下,a和b的颜色

        如上图所示,在场景几何结构复杂,且环境光变化较大时,如果使用传统的ambient occlusion,会得到两点的颜色完全一致,而使用了bent normal后,它们将分别在偏左的半球和偏右的半球做积分,更加接近于定向遮蔽的效果。

reference:《bent normals and cones in screen space》(a) 使用光线追踪 (b) 使用SSAO  (c) 屏幕空间bent normal  (d) AO (e)bent normal

        同样,如上图所示,可见在实际场景的渲染中,使用bent normal渲染的场景,相比起使用传统ao方式渲染的场景,更加接近光线追踪的结果,并且提供了暗处更准确的颜色信息。

reference:《bent normals and cones in screen space》
        Bent Cone

        如果我们想要进一步提升环境光遮蔽的准确性,我可以考虑额外存储环境光可见的张角信息,即如图所示的bent cone,或者称为visibility cone。

        在我们获得了可见性锥开口大小的情况下,我们需要获取对应的不同张角对应的积分结果,一种预过滤的方式是按照锥形开口角度的大小把不同预过滤的结果存储在纹理的mipmap中,并假设可见性锥方向对齐于法线方向。因为较大开口变化更加平滑,可以不使用较高分辨率来存储。

        另外一种方式是结合屏幕空间算法,使用可见性锥来做重要度采样的加速,因为可见性锥更好的反应了可见光线的分布情况,这样能够得到更加正确的结果。

屏幕空间AO

        我们在前文中提到了计算遮蔽时考虑距离参数,也就是我们只考虑某一段距离内环境光产生的遮蔽,相当于我们处理的是一个比较局部的信息,这一类信息如果我们用布点来实现很容易因为精度不够而不准确,但是它非常适合在屏幕空间实现。因为我们只需要在一个比较小的范围内去做查询,实现类似接触阴影的效果。

        SSAO

        一个非常朴素的思想是,既然AO的定义是未被遮挡光线的比例,以及我们只需要考虑有限距离的环境光产生的遮蔽,我们只需要在像素点周围随机选取采样点。Crytek提出了根据采样点对应的屏幕像素深度来判断该点是否被遮挡,并统计未被遮挡采样点的比例作为环境光遮蔽的结果。

reference《Real Time Rendering》Crytek的屏幕空间AO算法

        比较理想的情况是在法线半球去采样,并使用cosθ项作为每个采样点的系数,但在早期无法获得屏幕空间法线信息的时候,会选择在完整的球体内进行采样,并且忽略了余弦项,由于另一半球通常不受光,这种近似通常会让画面变得更暗。 

        判断点是否被遮挡使用了类似阴影贴图的做法,我们比较场景深度和采样点深度来判断眼睛能否看见这个采样点,如果不能,我们认为它被遮挡。

        这种方法的缺陷是无法获得物体之间的相对位置信息,也就是它可能会在屏幕像素上邻近,但实际上有一定距离的物体之间产生错误的遮蔽效果,使得画面看起来“变脏”,我们可以通过判断深度差来确定两个像素是否有一定距离来规避。

       HBAO

        在常规的SSAO中,我们通过随机采样点的可见性来估算AO,这并不够准确,更加可信的估算方式是计算AO的可见或不可见角度。基于这个思想,Nvidia提出了可以在屏幕空间查询遮挡的角度范围,该方法被称为HBAO(Horizon Based Ambient Occlusion)。  

        1.确定角度

reference:《Image-Space Horizon-Based Ambient Occlusion》

        要执行这个算法,我们首先需要了解两个角度,一个是切平面的角度,称为切线角(tangent angle),记作t(\phi),另一个是遮挡部分的夹角,称为视界角(horizon angle),记作h(\phi)\phi是相对于XY平面的夹角。

        XY平面即屏幕平面(也称视线平面),Z为视线方向。        

reference:《Image-Space Horizon-Based Ambient Occlusion》
在屏幕空间中,对于点P,我们采样周围的四个方向

        我们首先需要确定视界角:

        如上图所示,我们首先在屏幕空间中选取四个方向,其中每个方向上,随机添加一个角度的旋转; 接下来,沿着每个方向,我们得到多个射线经过的像素,每个像素对应一个深度,我们比较这些深度信息,取得其中落在范围内的最大值,并计算该点对应的视界角;屏幕上的这些深度,对应于3D空间沿着z轴的平面中,每个角度相交的距离。

reference:《Image-Space Horizon-Based Ambient Occlusion》
如图,我们在采样方向对应的切面上,可以直接通过屏幕空间深度采样得到对应的交点,我们找到找到半径范围内最远的交点(S3),将该射线与视线平面夹角作为视界角

        R是我们预先指定的环境光遮蔽半径,我们只会在这个区域内搜索交点,如果超过了这个区域,我们认为不存在遮蔽。

        理解这个算法的关键是理解上面几个截图中,对应了哪个坐标空间;

        整个故事是从屏幕空间开始,因此我们实际上是以屏幕上某个点作为圆心,并在屏幕上“切了一刀”,在该切平面计算视界角的,要注意这里的切并非法线切线中的"切线“;

        该切平面的纵轴为屏幕空间的z轴,横轴落在屏幕空间上,因此每个点对应的高度是已知的(即屏幕上该点的深度);

        我们会围绕该点截取多个“切平面”去收集。

        2.积分计算

        我们计算最终的环境光遮蔽:

        K_{A}=1-\frac{1}{2\pi }\int_{\phi =-\pi}^{\pi}\int_{\theta =t}^{h}W(w)cos(\theta)d\theta d\phi

        上式中计算可见度即使用1减去被遮挡部分的积分,其中2\pi为归一化系数。上式为一个二重积分,由于积分上下限确定且内函数形式简单,我们可直接计算原函数,可将其转换为一重积分。

        计算一重积分时,我们仅使用四个采样方向,如在前一章节中提到的那样。我们在四个方向执行以上的计算sin(h)-sin(t),并计算它们的平均AO值作为最终的AO值。

        \approx \int_{\phi=-\pi}^{\pi}(sin({h}')-sin(t))W(\phi) 

        上式中包含了距离衰减函数W(w)=1-r^{2},它是和θ相关的值,不同θ对应着不同的相交距离,相交的距离越远,产生的贡献越小。其中r是交点位置的归一化距离,若我们计算P点发出的射线,并相交于S点,则r=\frac{\left \| S - P\right \|}{R}

        为了考虑多个角度对应的权重,NVIDIA使用了逐步累加的算法:

        WAO = W(s1)AO(s1)+W(s2)(AO(s2)-AO(s1)) (\phi (s2)>(\phi (s1))

        需要注意的是,此处积分的θ并不是仰角,因此cosθ也并不是我们的所熟知的余弦项,它只是为了获取投影到图像平面的结果。因此,相比起常规的SSAO,HBAO虽然能够得到更加准确的结果,但是并不物理正确的衰减函数以及余弦项的缺失依然为其引入了误差。

      3.问题

       这个算法在落地中仍然存在一些需要处理的工程问题,以下是NVIDIA在原paper中指出的几个问题:

        ● 法线选取      

        上式计算sin(h)-sin(t)时,我们需要的法线是面法线,而非插值后的法线。也就是说我们并不是从GBuffer去采样计算光照的法线,而是用ddx/ddy计算得到的,因为插值法线会产生一些错误遮挡。如当插值法线相比起面法线,向未遮挡方向偏移,遮挡范围会比实际的要小一些。

        ● 伪影

reference:《Image-Space Horizon-Based Ambient Occlusion》

        如上图,我们在建模曲面的时候,会使用较少的平面去拟合连续光滑的曲面,如果我们使用不连续的面法线来计算AO,就会导致在平面的交界处产生“黑线”。

        为了解决这个问题,原文提出了对切线方向添加一个角度偏移,这样就可以避免把这部分遮挡算入最终的结果。

        ● 像素不连续

         由于屏幕的像素是离散的,即它在空间中不连续,因此两个相邻的像素可能会得到”跳变“的结果,这表现为画面上的突变;问题我们可以通过前文中的衰减函数来减弱这个现象。 

        ● 噪点

        和大部分屏幕空间算法一样,需要使用算法降噪,原文中使用了高斯模糊的方式,一部分噪点由采样精度引入,且计算屏幕空间AO时可能会使用半分分辨率来计算。

        GTAO

        基于HBAO的优点,以及对其不足之处的改善(引入了物理不正确的经验参数W,缺少余弦项),Activision设计了一种更加接近于光线追踪效果的屏幕空间AO算法,即GTAO(Ground-Truth Ambient Occlusion),它的定义同样基于视界角,并且考虑了余弦项,对可见角度进行积分:

        K_{A}=\frac{1}{\pi}\int_{0}^{\pi}\int_{\theta1}^{\theta2} cos(\theta-\gamma )^{+}|sin\theta|d\theta d\phi

        上式中,\theta1(\phi)\theta2(\phi)分别是视界前后可见的起始范围,\gamma是法线和视线之间的夹角。

        1.与HBAO的差异

        注意到这里和HBAO有几个主要的差距:

        (1)多了余弦项,即cos(\theta -\gamma )^{+}

        相当于认为越接近法线的角度对环境光贡献的权重越高,这更接近于真实情况;

        (2)cos\theta变成了|sin\theta|

        这里无论是HBAO的cosθ还是GTAO的sinθ描述的都是到屏幕空间的投影结果,只是两个算法中定义的视界角θ不一样,HBAO是相对于视线平面的夹角,而GTAO是相对于视线的夹角,两者正好互补,这种定义下,θ可能是负数,因此要加上绝对值;

        (3)Φ积分范围
        此外,由于GTAO考虑的视界角需要考虑正负两个方向(+φ和-φ),因此它关于Φ的积分范围转换为[0,\pi ]

        (4)φ积分范围

        GTAO最大的差异在于它的采样范围从(t,θ(h1))变成了(θ(h1),θ(h2)),相当于考虑了完整的球体范围,而不是忽略了切线值下的遮挡情况,并且也规避了在切线处采样可能存在的各种瑕疵,比如法线选取问题和伪影问题;

        (5)不包含衰减函数

        GTAO只计算了近距离的遮挡,不包含任何衰减,而对于远处低频的遮挡使用其它方式获取,在超过近距离遮挡区域后才逐渐淡出;

reference:《Practical Real Time Strategies for Accurate Indirect Occlusion》
计算基于水平的环境遮挡时的参考框架示意图。视界角θ1和θ2用红色表示,方位角φ用绿色表示,视界方向w0和法向n用黑色表示,w0和n之间的角度y用蓝色表示

        GTAO整体的计算方式和HBAO类似,都需要计算出可见视界角,只是计算积分有所不同。       

        为了能够计算这个积分,我们需要计算出两个视界角来确定积分范围,这两个视界角是方位角φ方向为\hat{t}(\phi)时,对应的半球面产生的视界角中,最大的两个角度:

         \theta _{1}(\phi)=arccos(max_{r\epsilon [0,1]}(\left \langle w_{s}(r),w_{0} \right \rangle^{+} ))

        其中w_{s}(r)是特定方向φ上对应的多个θ。

        此处视界角的计算和HBAO类似,也是在屏幕空间过点P选定方向后,通过在该方向上采样深度值来判断距离,只不过要同时搜索得到其相反方向的视界角,得到两个视界角。

        2.积分求解

        我们依然可以通过求解原函数的方式将积分转换为普通计算,但由于参考坐标系的变化和余弦项的添加,使得原函数的推导更加复杂。

        我们先假设法线n位于视界向量定义的平面P上,并分别求解w0左右两部分的积分(通过交换上下限去除绝对值):

        \hat{a}(\theta_{1}(\phi),\theta_{2}(\phi),\gamma )=\int_{0}^{\theta_{1}}cos(\theta-\gamma ) sin\theta d\theta+\int_{0}^{\theta_{2}}cos(\theta-\gamma )sin\theta d\theta

        =\frac{1}{2}\int_{0}^{\theta_{1}}(sin(2\theta-\gamma)+sin\gamma) d\theta+\frac{1}{2}\int_{0}^{\theta_{2}}(sin(2\theta-\gamma)+sin\gamma) d\theta

        =\frac{1}{4}(-cos(2\theta-\gamma)+2\theta sin\gamma)|_{0}^{\theta_{1}}+\frac{1}{4}(-cos(2\theta-\gamma)+2\theta sin\gamma)|_{0}^{\theta_{2}}

        =\frac{1}{4}(-cos(2\theta_{1} -\gamma)+cos\gamma + 2\theta_{1} sin\gamma)+\frac{1}{4}(-cos(2\theta_{2} -\gamma)+cos\gamma + 2\theta_{2} sin\gamma)

        但通常情况下法线n都不可能恰好落在视界向量的平面上,因此我们需要从原式中分离出法线在该平面上的投影结果:

        \left \langle n,w_{i} \right \rangle ^{+}=\left \| \bar{n} \right \|\left \langle \frac{\bar{n}}{\left \| \bar{n} \right \|},w_{i} \right \rangle^{+}

        \frac{\bar{n}}{\left \| \bar{n} \right \|}为投影法线,在该平面上与法线的夹角变成了与投影法线之间夹角,因此,我们得到最终的积分求解结果:

        A(x)=\frac{1}{\pi}\int _0^{\pi}\left \| \bar{n} \right \| \hat{a}(\theta_{1}(\phi),\theta_{2}(\phi),\gamma^{'} )d\phi

        其中:

        y^{'}=arccos(\left \langle \frac{\bar{n} }{\left \| \bar{n} \right \|},w_{0}\right \rangle)

        3.间接光照

        我们在前面提到的AO参数是通过把可见性项直接从积分中提取出来得到的,因此我们计算的最终间接光照为AO * Irradiance。

        GTAO算法给出的公式是基于某个位置的邻域只遮挡光,而不会反射光,相当于忽略了相互反射。在这个假设的前提下,该公式是基本符合真实情况的,也因此会导致能量损失。

        一些算法通过给AO添加距离衰减项来避免暗部过黑,而GTAO恰好了移除了衰减项。

        原算法中,先后给出了两种算法来提供间接照明,其中一个是根据观测结果的三次多项式拟合公式:

        G(A,\rho )=a(\rho)A^{3}-b(\rho)A^{2}+c(\rho)A

        其中,ρ为反照率,A为环境光遮蔽(AO),a(ρ),b(ρ),c(ρ)定义如下:

        a(\rho)=2.0404\rho-0.3324

        b(\rho)=4.7951\rho-0.6417

        c(\rho)=2.7552\rho-0.6903

        如果我们假设邻域S(x)具有恒定的反照率ρ并为均匀的漫反射,我们可以将多次反弹的结果表示为一个Neumann series,它收敛解如下:

        L_{r}(x,w_{0})=L_{i}\frac{\rho }{\pi }\frac{A(x)}{1-\rho (1-A(x))}

        此外,既然我们已经有办法计算出视界角,实际上我们也有办法得到bent normal和bent cone:

        b=\int _{0}^{\pi}\int_{\theta_{1}}^{\theta_{2}}w_{i}(\theta,\phi)cos(\theta-\gamma)^{+}\left | sin(\theta) \right | d\theta d\phi

        \alpha_{v}(x)=arccos(\sqrt{1-A(x)})
        有了bent normal等,我们就可以使其作为输入,参与包含定向遮蔽的其它间接光计算算法。

        尽管GTAO已经号称是Ground Truth的实现方案,但它依然会受限于屏幕空间,也就是说,它无法规避和屏幕空间相关的缺陷——它无法正确地提供屏幕上不存在的信息,比如视锥体之外的物体产生的遮蔽,或者被前方的角色挡住的物体产生的遮蔽;此外它只提供近距离的遮蔽,像天光这种长距离的遮蔽是无法得到的;以及,它的准确只建立在采样数量足够多的情况下。   

reference:《Advanced Ambient Occlusion Methods for Modern Games》
屏幕空间AO算法的缺陷:由于无法识别相对位置关系,场景会被角色遮挡,导致错误的采样到角色上(红色的采样点),而不是场景物件本身,导致遮挡计算出错;一旦有角色“经过”某处的场景,这处场景的遮挡就会有问题,表现为角色周围的“光圈”;我们也可以通过把动态物体和静态物体分别处理来解决

        这会使得仅应用屏幕空间的AO算法会让整个画面变得不稳定,并且会丢失很多遮挡信息。因此在实际工程中,屏幕空间算法通常作为补充,和其它技术结合在一起使用。

全局光照

        我们在前文讨论了一些模拟间接光照的方案,但是我们尚未系统的讨论全局光照相关的内容。通常我们可以把全局光照的求解划分为两个部分,一个是如何收集场景中的光照信息,包括一次或多次反弹的结果;另一部分是如何将已知的间接光照应用到场景物件中。

        我们在实时渲染的绝大部分算法都遵循这样的思路。在基于图像的照明中,我们跳过了场景光照收集的过程,因此它只是提供了环境照明作为补光,但不足以称之为全局光照。全局光照需要我们提供间接照明的完整解决方案,包括光照信息的获取,以及场景物件的作用。

        光照的来源

        在开始全局光照的具体算法之前,我们先来考虑一个问题,那就是光从哪里来?

        在路径追踪算法中,我们实际是在做一个迭代的过程,从视点发出射线后,不断地模拟光线行进的路径,并把光照的结果累加。而在实时运算中,我们无法做到多次迭代的过程,因为这个操作的时间复杂度太高了,所以我们往往只在运行时模拟一次迭代的结果,那么我们需要预计算多次迭代的光照结果;或者我们可能会把多次迭代分摊到多帧进行。

        以天光为例,如果我们在运行时读取预计算的光照信息,假如我们从光源方向出发来考虑这个问题,那么场景物件要么被天光直接照射(未被遮挡的室外),要么无法被天光直接照射(室内或被遮挡的室外),这个时候我们认为光源来自无限远的方向,场景物件接收的是直接照射的结果。

        假如我们从视点方向出发来考虑这个问题,物件在某个方向上接收到的光照来自于这个方向上物件提供的间接照明,这个时候我们认为光源来自比较近的方向,场景物件接收的是天光多次反弹的结果,这类信息通常比较高频。

        这两种方式实际上对应了两种不同特点的光照信息,一种是无限远的照明信息(长距离),另一种是经过远距离传播后的局部照明信息(短距离)。回到开头的问题:光从哪里来?假如我们仅考虑光源的输入,我们现在可以回答这个问题:光来自任何地方!它不仅来自四面八方,在任意方向上,远处的天空在“发光”,近处的物件也在“发光”。

        由于这两者侧重点不同,不仅是光照信息不同,对应的场景信息也不同,我们在实时全局光照计算中通常区分来处理这两种情况。另一方面,室内和室外的间接光照环境也有较大差异,室外以天光直接照明和天光反弹为主;而室内通常是天光遮挡的,以天光反弹以及局部光源照明为主,因此有时候我们也会分开处理室内外光照。

        虚拟点光源(VPL)

        我们先以简单的局部光源开始展开介绍。

        假如我们在场景中添加了一个聚光灯或点光源,我们除了要计算它的直接照明,还需要模拟它产生的间接光照,不然我们最终得到的效果对比度将会比较失真。

        聚光灯或点光源通常被称为局部光源,因为它们的影响范围在有限的距离内,因此我们可以比较容易的去跟踪它们的光线路径。以点光源为例,它的光线会向四周发射,如果我们使用一个cubemap去捕获它周围的场景深度信息,那么我们就可以通过深度比较去获得直接光的遮挡信息。

        同样的,如果我们同时去捕获场景的法线、albedo、光照等信息,记录为RSM(Reflected Shadow Map)形式,那么我们可以将场景中每个像素点视为一个提供一次反弹间接照明的虚拟点光源(Virtual Point Light),这个信息可以被视为radiance信息。

        自此,我们得到了一次反弹的radiance结果,如果我们假设点光源照到的物体都是非金属,即它们都会以漫反射的形式对光线进行反射,我们就可以随机选取一些采样点去计算每个位置的irradiance信息。

        这类算法非常朴素的模拟了光线一次弹射的间接照明结果,它的radiance来源是局部光源并且是实时生成的,这也就意味着它可以匹配动态的实时光源,并且对于性能非常敏感,因此在生成cubemap的时候我们不会使用太高的分辨率。并且由于我们把一个像素点视为一个次级光源,因此我们提供多大分辨率的cubemap,就意味着我们生成了多少个次级点光源。

        注意到在这个算法中我们会把第一次光线打到的位置视为可用的次级光源,这说明我们只考虑了间接照明,而没有考虑间接光的遮蔽。假如一个物体与点光源的连线之间存在遮挡,那么理论上这个物体就不会受到这部分的间接照明,但好在我们还可以生成阴影贴图去模拟这一点。

        即便如此,这个算法的思想依然是值得借鉴的,因为它为在有限的硬件条件下,模拟光线追踪,实时的生成间接照明提供了一种可行的思路。

       光线传播体积(LPV)

        在局部光源中,由于光线只在有限的距离内传播,所以它产生的间接光照的影响范围也是有限的,对其进行模拟比较简单;相比起局部光源,天光的间接光照会更加复杂,因为它是一个全局传播的过程,并且传播路径复杂且漫长。

        考虑到性能问题,我们无法在运行时模拟完整的传播路径,即使是局部光源,我们也仅仅模拟了一次反弹的效果。如果我们确实想要做到这一点,我们就需要考虑把长距离传播转换为短距离传播的方法——就像接力中每个人跑一小段距离,就能把接力棒传送到终点一样,同样,我们可以让每个像素仅考虑邻域像素的光照结果一样,用类似卷积的方式把颜色信息传播到较远的像素,这就是Light Propagation Volume的思想(LPV)。

        在上一节中,我们介绍了虚拟点光源的思想,我们可以认为光线照到物体上,物体反弹出来的光线,就相当于在物体表面放置了一个虚拟点光源,这个光源会对周围物体产生贡献。我们可以认为在天光在场景物体中的无数次反射中,整个三维空间中的物体表面上密密麻麻分布着多个虚拟点光源,这些点光源作为“次级光源”照亮了整个场景。

        如果我们在场景中构建三维网格体(体素),我们可以认为每个网格内都包含一个(或不包含)虚拟点光源,此外,我们还需要考虑以某种形式记录网格之间能否传输的可见性信息。完成了虚拟点光源和场景可见性信息的空间建模后,我们就可以开始实现传播的模拟,每次迭代会向上下前后左右六个方向进行传播,并进行一定的衰减。

reference《Real Time Rendering》光线传播的多次迭代过程

        经过多轮传播达到稳定后,我们得到空间中的一个均匀三维网格,每个网格存储了对应位置的最终间接光照信息,在实时运算中,我们取周围的radiance计算最终的irradiance即可,比如对于漫反射就是取周围的radiance计算加权,而光泽反射则直接采样最终的照明结果。由此也可见我们较难控制光泽反射的粗糙度和质量。

        这类算法由于仅考虑邻域,非常适合在计算着色器中实现,并且内存访问跨度也很小,并且这还是一个实时算法,我们会在每一帧都去执行传播这个步骤。

        不过我们也能很快意识到其中存在的问题:

        整个算法的精确度和格子的密度有较大关系,如果场景的复杂度远远高于一个格子的大小,我们的拟合就会丢失这部分遮挡信息,也会产生漏光/颜色溢出;而格子密度较高时,又会显著的增加内存占用和迭代耗时。

        另一方面,整个算法可以说是基于VPL的全局照明版本,那么它也具有一样的问题,那就是仅模拟了一次反弹的间接照明结果。此外,整个算法只是重点阐述了光线传播的迭代,对可见性信息的获取并没有提供标准的解法。

        虽然这个算法因为执行效率和画面瑕疵本身并没有得到广泛的应用,但光线传播仍然是一个非常有趣的思想,可以在全局光照模拟的时候参考这种思路。

        基于体素的GI(VXGI)

        类似于LPV算法,VXGI也是实时的基于体素的GI算法,需要构建体素化场景和收集光照,与之不同的是它最终的irradiance收集方式有所差异。

        体素化场景

        要实现VXGI,首先要对场景进行体素化,这件事情是在运行时以较低的更新频率去实现的。首先需要确定体素的范围,并收集包含在这个范围内的所有场景物件。体素化可以通过保守光栅化来实现,通过判断光栅化后的像素落入到哪个voxel进行收集。

reference: 《Voxel-Cone-Tracing-Octree-Real-Time-Illumination》
光栅化体素的方式

        我们会统计每个voxel是否被物件填充,以及填充的百分占比,我们称之为遮挡因子

        由于光栅化是2D的,而场景是3D的,所以我们需要选择合适的角度进行投影。也就是说,我们需要选定一个“主轴”,它可以产生最大的投影面积(三个轴向投影最大面积,包括xy轴投影,yz轴投影和xz轴投影)。

        光照注入

        得到了空间中体素及其遮挡比例后,我们开始计算每个体素的光照信息。

        我们遍历场景中的所有光源,计算每个光源对每个体素的贡献,影响因子包含可见性、余弦项、光源颜色和强度等。

        通过这些操作,我们得到记录了场景光照和遮挡因子数据,这些数据可以被组织为以下形式之一:

        (1)GPU端的八叉树结构;

        (2)clipmap的三维纹理;

        这部分数据的更新可以增量实现。

        渲染场景

        接下来我们在后处理屏幕空间计算全局光照。

        对屏幕上的每个像素我们在半球范围内收集反弹的光照信息,基于Cone Tracing进行RayMarching。

        与普通的RayMarch不同的是,我们发出“锥体”而不是射线进行查询,这样就可以用较少的样本数量覆盖整个半球区间,如下图(step3)所示:

reference: 《Interactive Indirect Illumination Using Voxel Cone Tracing》
VXGI的三个步骤:
① 从光源方向渲染,把入射radiance和光源方向烘焙到八叉树中
② 在八叉树中滤波irradiance值和光源方向
③ 从相机方向渲染,基于体素进行锥形追踪,采样漫反射和镜面反射BRDF

        发出少量的“锥体”进行查询,随着不断前进,我们增加孔径,也就是在层级八叉树或层级纹理中,使用更底层的节点/更高的mipmap进行查询/采样。

        

reference: 《Interactive Indirect Illumination Using Voxel Cone Tracing》
如图,不同的孔径对应不同级别的mipmap

        根据表面是漫反射或是镜面反射材质,我们在半球区域发射锥体或是在glossy高光根据粗糙度发射锥体:

        ① 对于diffuse BRDF,使用几个比较大的锥体去收集;

        ② 对于glossy BRDF,根据粗糙度确定镜面锥的孔径,并沿着反射方向来分布锥体;

        每次读取到一个光照值和遮挡因子,我们按照透明度计算的方式,使用遮挡因子作为alpha。我们累加遮挡因子,并且将其用于减弱来自后续点的光照强度,不断前进直到遮挡因子累加到1,或者行进长度达到某个设定的阈值。

        c=\alpha c + (1-\alpha)\alpha_{2} c_{2}

        \alpha = \alpha + (1-\alpha)\alpha_{2}

        这种透明度累加的形式并不那么正确,如果圆锥体被两个不同的物体遮挡,分别挡住50%,那么累加起来应该是完全遮挡,而在透明度累加中则会计算得到0.5 + (1-0.5) * 0.5 = 0.75的光照结果。

        由上图可见,发射的“锥体”之间存在缝隙,所以可能会导致漏光;并且和其他体素化方法一样,如果遮挡物小于体素也会产生漏光。

        动态漫反射全局光照(DDGI)

        我们在前文提到Cone Tracing可能产生的漏光问题,这也是它为了节省追踪性能所付出的代价;如果我们选择走实时光线追踪就不会存在这个问题,DDGI就是使用实时光线追踪的一套方案,因此它可以获得更高质量的画面效果。但是正如名字所示,它仅仅处理了漫反射效果。

        它可以看作是基于Irradiance Volume实现的一种改进方案。

        probe摆放和生成

        在场景建模上,使用了probe的形式来记录场景信息,每个probe记录了对应点位的irradiance信息,这和常规的irradiance volume做法类似。比较特别的地方是,DDGI在摆放probe的时候还额外存储了两个信息:

        1)Probe每个方向到最近物体距离的均值

        2)Probe每个方向到最近物体平方距离的均值

        probe存储为八面体的形式,因此会从球面映射到八面体,这种存储方式参数化具有更少的失真,并且有更简单的边界管理;这些probe均匀的摆放在3D空间轴对齐的网格顶点上,可以被打包到纹理图集中,以便于双线性插值。

        一切涉及到probe摆放的全局光照技术方案都可能会遇到由于probe产生的漏光问题,比如摆放在室内的物件可能会采样到室外的probe,而实际上由于墙的遮挡理论上不应该影响室内的物件。此时假如我们知道probe到遮挡物的距离,就可以通过与probe到物体的距离进行比较来判断物体是否能够受到特定probe的影响。

        但是,获取probe在各个方向到遮挡物的距离意味着我们需要存储大量的信息,因此需要考虑近似的方案,DDGI使用了统计学中的切比雪夫不等式来描述在物体和probe之间是否存在遮挡,并直接使用取不等式的上界将其转化为等式。换言之,就是在已知多个方向的距离均值和方差的情况下,计算物体被遮挡的概率作为权重。

        probe更新

        场景中的probe存储的是漫反射irradiance信息,这些信息是动态捕获的,使用GPU进行世界空间的光线追踪计算(光线追踪API或基于探针的行进方法)。

        对每一个probe,发射多条光线,记录交点的位置、法线、albedo信息,存入场景的光照缓冲区中,纹理的分辨率为采样光线数 * Probe数量。

        如果我们使用之前的光线反射来更新光照缓冲区,那么我们就可以递归地计算跨帧的多次间接光照反射,获得多重反射的全局照明。

        probe是动态计算的,我们为了减少采样数,采用了时间超采样的形式,把probe的irradiance计算分摊到多帧完成,并通过对当前帧和上一帧进行插值得到最终结果,计算公式如下:

reference:《Dynamic Diffuse Global Illumination with Ray-Traced Irradiance Fields》

        动态照明

        我们记录了场景radiance等信息后,再用其计算Probe中的irradiance,即将某方向对应的半球面上的radiance使用cosθ进行混合。

        接下来,我们根据每个probe的位置和方向来计算irradiance probe的插值权重。

reference:《Dynamic Diffuse Global Illumination with Ray-Traced Irradiance Fields》

        如图所示,我们使用世界空间中表面的法线来对周围八个探针计算光照的权重并进行采样;在进行可见性查询的时候,我们使用前面记录的均值和方差应用切比雪夫计算遮挡期望,考虑到probe的密度不足,细节可见性由屏幕空间环境光遮蔽来补充。

        这里存在的问题是,我们使用切比雪夫不等式来近似模拟遮挡,但实际情况不一定满足我们所取得上界,在实际情况和拟合差距较大时,依然会产生漏光。

        我们在前文中介绍了多种全动态的实时全局照明方案,如果我们能把一部分信息预计算的话,这就可以使用更加复杂的算法去收集信息,而不需要考虑实时性能,但由于这涉及到了内存占用,所以我们需要使用尽可能压缩的数据结构。

        预计算辐射传输(PRT)

        球谐函数就是其中一个常用的内存占用少的数据结构。

        我们在前文中推导投影计算球谐系数的时候,推导出了一个性质,那就是两个球面函数乘积的积分(内积)可以转换为球谐系数的点积。

        我们发现光照方程正好就对应了这种情况:

        

        如上图所示,整个光照方程可以拆解成四部分乘积的积分,分别是光照项(radiance)、可见性项(visibility),BRDF项和余弦项。我们组合其中几项就可以把它转换为两部分的乘积,这样的话我们就可以仅通过球谐系数的点乘得到最终的结果。

        这里蕴含这一个重要的信息,那就是我们把对应分量投影到球谐基底之后,我们不需要去做重建的操作,因为我们关心的是积分的结果而不是积分的单项,因此我们只需要一步到位还原积分的结果即可,而这个计算过程依然也是一个简单的点乘,我们把这个过程称作relighting。

        如果我们认为场景中只有光照会发生变化,而场景本身是静态的,那么我们就可以把光照(radiance)信息拆分出来,剩下的部分(可见性,BRDF,余弦)描述了场景的几何信息以及如何对光照进行响应,我们称为传输项(transfer),因此这种方法被称为预计算辐射传输(Precomputed Radiance Transfer),简称PRT。

        

        相当于我们把积分拆成了两部分,一部分是radiance,一部分是transfer。因此我们所需要做的就是离线的把radiance和transfer分别投影到球谐基底上,得到两组球谐系数向量,并在运行时通过向量点积计算得到最终的irradiance。

        这里有一个值得考虑的事情,那就是我们之前提到的我们使用球谐函数的主要原因是信号是低频的,但在这个式子中radiance本身是高频的,但我们仍然使用二阶或三阶的信息去表达它,这件事情仍然成立的原因是transfer项是低频的,一个低频项和高频项乘积的结果仍然是低频的,而我们只关心最终的积分结果。

        另一方面,虽然说我们可以利用球谐函数的性质来简化整个计算过程,但预计算辐射传输的思想并不完全依赖于球谐函数,它的本质是把场景信息和光照信息分离存储,并实时计算光照,而不是像lightmap一样直接存储最终的计算结果。比如我们可以使用球谐来存储低频的传输项,并仍然使用cubemap或其他基底来存储radiance信息,只是最终relighting的计算有所差异。

        通过将光照信息和光照传输信息分离,我们就可以很好地处理静态场景动态光源的情况,比如场景中的灯光动态开关、颜色变化、方向变化等,我们都能快速的让场景响应实时光照的改变,并且相比起完全离线烘焙的方案,所付出的额外运行时开销是有限的。

        PRT也存在一些缺陷,首先一个非常显然的问题就是它模拟的场景是静态的,因为一旦有物体位置形态等发生变化,都会引起传输项的改变,预计算的结果就会失效。

        另一方面,我们认为transfer项是低频的,相当我可能难以较好地模拟一些高频的信息,比如突变的环境光颜色、明暗交界面、高精度法线贴图等结果。

        漫反射

        漫反射的公式在积分项中是一个常数,我们将其代入上式中的BRDF项,并将其从积分中提取出来,得到以下结果:

ρ:albedo

        我们把漫反射的计算转化为radiance球谐系数和transfer球谐系数这两个向量的点积。

        光泽反射

        对于不同的观察方向,镜面反射会得到不同的结果,相当于不同的观察方向会对应一组不同的球谐系数向量,也就是说此时每个传输系数是关于观察方向(v)的一个函数,而不像漫反射一样是一个常值。

        既然传输系数本身也是一个函数,那么它自然也可以投影到球谐基底上,这相当于我们投影了两次,因此我们得到了一组嵌套的系数:

如图,我们经过两次投影得到最终结果

        这就相当于在计算高光的时候,我们不再使用传输向量(transfer vector),而是使用传输矩阵(transfer matrix),比如原本是一个1x9的向量,现在就会变成一个9x9的矩阵。

        最终计算irradiance也不再是向量的点乘,而是向量(光照球谐系数)和矩阵(传输球谐系数)相乘,相乘后我们得到一组向量,根据视线方向求和取得对应的结果。

        这里可以看到,假如我们要去渲染具有光泽度的物体,那么所需的存储数据和运行时的计算复杂度都会上升不少。除此之外它还有一个非常大的局限性,那就是它只能用来表达粗糙度较高的光泽反射,因为整体的频率相对较低。对于非常光滑的镜面反射,它是非常高频的,我们没有办法通过球谐函数前几阶来表达,反而使用cubemap直接采样是更加合适的。

        另一方面,传输矩阵的数据量过大,因此人们想到了压缩这类数据的方法。考虑到传输系数并不是在空间中均匀分布的,而是具有一定的趋向性,因此我们可以使用PCA主成分分析进行降维。

reference:《Precomputed Radiance Transfer for Real-Time Rendering in Dynamic, Low-Frequency Lighting Environments》
上图中,我们使用红色表示正的SH系数,用蓝色表示负的SH系数;对于漫反射而言,我们使把光照信息投影到SH上,得到一组光照向量系数,它和传输向量点积得到最终的光照结果;对于光泽反射而言,我们使用传输矩阵和光照系数相乘;我们还可以选择把BRDF核分离存储,而不是预先一起烘焙到传输函数中,这样可以提供更高的自由度,这也需要运行时再去与BRDF核卷积。
        可见性信息

        我们在球谐函数一章中介绍了clamped余弦函数的编码,它也可以被视为传输函数的一种。更一般的来说,PRT的传输函数中不仅可以编码余弦函数,还可以编码任何和场景相关的信息,比如前文介绍的可见性信息的编码。

        可见性描述了来自哪些角度的环境光照是可见的,以及哪些角度的环境光照被遮挡。

        我们会在后面介绍到,通常计算环境光遮蔽都是独立提取AO分量并和环境光相乘。而在PRT中,我们把AO视为场景信息的一部分,而不将其独立到积分外部。

        相比起把AO信息提取出来的近似做法,把可见性信息编码到传输函数中会使得我们获得更加精确的结果——比如获得更好的方向性,这表现为该暗的方向暗,该亮的方向亮,以及间接光照颜色以可见角度对应的颜色为主。

        内部相互反射

        在我们模拟可见性的时候,如果某一处不可见,我们简单的认为该处的间接光不产生任何影响,而实际上我们应该受到该点反弹出来的光照。

        如果仅考虑环境光遮蔽的话,场景将会偏暗,尤其在环境光遮蔽系数为0的角度,将会是全黑的。也就是说如果场景把可见性信息烘焙到传输函数中,但不考虑相互反射的话,我们会多的比提取环境光遮蔽分量单独计算的方式更暗的结果,整体画面的观感可能会更差。

       因此我们应该在原有的基础上额外考虑多次相互反射的结果。在实际计算中,我们先去计算环境光直接可见(即不被遮蔽)部分的光照,再去计算环境光间接可见(即多次反弹入射)部分的光照,再把两者相加。

        可见部分的系数为Vp,不可见部分的系数为(1-Vp):

        T_{I}=T_{S}+\frac{\rho}{\pi} \int {L}_{p}^{'}cos\theta ^{+}(1-V_{p})ds

        =\frac{\rho}{\pi} \int {L}_{p}cos\theta ^{+}V_{p}ds+\frac{\rho}{\pi} \int {L}_{p}^{'}cos\theta ^{+}(1-V_{p})ds

        如上所示,相当于我们分别需要去收集间接光照直接照射和多次反弹的球谐系数,并收集直接可见部分和不直接可见部分的球谐系数。

        内部相互反射的运行时计算部分比较简单,但由于要考虑到多次反弹的情况,它的离线烘焙相比起直接可见的环境光要更加复杂。

reference:《Precomputed Radiance Transfer for Real-Time Rendering in Dynamic, Low-Frequency Lighting Environments》

        

        如上图所示,在第0个迭代中,我们首先计算每个点受到的直接环境光对应的radiance。

        接下来,在第1个迭代中,以点p为例,我们遍历它不可见的方向,如Sd方向,相交于q点,我们读取点q上(在第0次迭代中计算出来并存储的)-Sd方向上出射的radiance(假设在p点观察q点);这样我们就得到了环境光一次反弹的结果。

        同理,我们在第n次迭代中可以得到环境光n次反弹的结果。

        光源的旋转

        在昼夜变换系统中,我们的灯光方向是在不断变化的。当我们的光源发生了旋转,那么我们可能就需要重新投影,投影的过程是求解积分的过程,这件事情是比较麻烦的。

        但对于球谐基函数来说,它具备一些比较良好的性质,旋转光源,相当于旋转基函数,旋转后的基函数可以表达为同阶基函数的线性组合。那么我们如果把旋转的结果缓存到表格中,我们就可以直接通过查表的形式完成relighting,而不需要做重新投影这样复杂的操作。

        该性质被称为旋转不变性,这也意味着,我们先投影信号再旋转,和先旋转再投影是一样的。

        

        传输函数

        L(v)=\frac{\rho _{ss}}{\pi }\int L(i)v(i)(n\cdot l)^{+}

        漫反射光照方程实际上是一个三重积分,它计算了光照、可见性和余弦项三者乘积的积分,但如前所述,在球谐函数中,我们仅能支持二重积分的快速卷积计算,因此我们只能将其转换为两项乘积的积分。

        在常规的PRT算法中,我们选择了光照和场景信息分离存储,也就是将L(i)和v(i)(n·l)分别映射到球面函数上,然后在运行时通过卷积计算得到最终irradiance。此处由于L(i)是分离存储的,所以我们能够很方便的修改同一个场景的光照环境。

        实际上我们可以存在多种转换方式,我们可以选择把光照分离存储,也可以选择把可见性分离存储:

        \bar{L(l)}=L(l)(n\cdot l)^{+}

        \bar{v(l)}=v(l)(n\cdot l)^{+}

        分离可见性存储的方式使得我们如果想要修改光照将会受到限制,但我们有办法通过准备多套预计算的L(l)(n·l)来模拟细节法线的结果,具体的拆分形式可以根据实际用途来决定。

        全局和局部PRT

        原始PRT算法认为光照是无限远的,它取得的光照是全局的光照信息,因而比较适合开放的室外场景,它能够很好地拟合天光以及天光遮蔽这种远距离的间接光信息。

        换言之,我们在前面介绍了两种光照的来源,一种是远处长距离的光照,另一种是近处短距离的光照,PRT通常拟合的是前一种情况;这意味着PRT对室内环境的拟合限制比较大。

        当然,PRT也可以推广作用于局部光源,但是这通常需要更高的布点精度来获得较好的效果。

        假如在场景中同时存在无限远的光线和局部光源,我们在工程实现中也可以考虑对光线进行分类,并且取距离较近的光照输入集合进行插值进而模拟光照。

        在PRT中,我们预计算了辐射和传输两个部分,如前所示,由于这种方法需要我们明确光源的来源,更常用于带有昼夜变化的大世界场景中。

        如果我们把场景中所有的光源产生的radiance收集到特定的结构中,而不关心光源来源,并根据预计算radiance在运行时生成irradiance,这也是一种可行的方案。我们每收集一次就相当于完成了一次反弹,如果我们把前一帧的输出作为下一帧的输入,就可以实现间接光照的多次反弹。

        在这个思想的基础上,可以发展出非常多种光照算法的变体,比如以怎样的形式存储场景中的radiance和可见性信息,以怎样的形式进行radiance收集等等。

Enlighten(辐射度算法)

        Enlighten是一套非开源的全局照明解决方案,它的原理就是辐射度(radiosity)算法。

        辐射度算法会假设场景中所有的间接光都是由漫反射表面产生的,且场景中的物体是由多个面片组成的,Enlighten称之为Cluster,全局光照的计算将以Cluster为基础单位进行计算。

        由于我们会将Cluster视为理想的漫反射曲面,因此它应该足够小以满足这一条件。

        我们会离线生成每个物体的Cluster,并预计算两两之间的形状因子,它描述了几何信息:

        F_{ij}=\frac{1}{A_{i}}\int_{A_{i}}\int_{A_{j}}V(i,j)\frac{cos \theta _{i}cos \theta _{j}}{\pi d_{ij}^{2}} da_{i}da_{j}

reference:《Real Time Rendering》

        其中V(i,j)是面片之间的可见性,θ是面片法线和面片中心点连线的夹角,A是面片的面积,d是中心点连线的距离。

        接下来,我们就可以基于形状因子在运行时计算每个Cluster的平均辐射度:

        B_{i}=B_{i}^{e}+\rho _{ss}\sum_{j}F_{ij}B_{j}

        其中,B_{i}是面片的辐射度,B_{i}^{e}表示了面片辐射出度,F_{ij}是形状因子,\rho _{ss}是次表面反照率。

        Enlighten在运行时把计算得到的光照信息存储为lightmap形式,并应用到场景物件上。光图可随着光源更新而更新。

Lumen(Radiance Cache)

        Lumen是ue5中推出的全新全局光照方案,它整合了各种技术的优点。Lumen使用Mesh Card作为物件代理来提供物件光照信息,使用基于SDF加速的多种光线追踪的方式收集radiance信息并缓存,在运行时插值和积分得到最终irradiance。

        在下文中,我们首先介绍Lumen中引用的一些辅助数据结构,如SDF、Mesh Card、Surface Cache和Voxel Light,然后再去介绍我们是怎么去计算最终的irradiance的,ue5把这套计算irradiance的方式称为radiance cache。

        距离场

        一种能够很好地表达场景信息的存储形式是有向3D距离场,如名字所示,它是分布在三维空间中的场,存储了每个点(沿着指定方向)到最近表面的距离。

        这意味着我们输入任意一个三维坐标(x,y,z),都能查询到当前坐标点对应的最近表面距离。同时,它还是有向的,符号代表了相对物体的位置,正号表示在区域外部,负号表示在区域内部,而0表示正好在表面上。

        给定距离场的3D纹理,我们就能在一定程度上还原场景的几何信息,它通常用于各种光线追踪或光线步进算法的加速,因为我们可以查询表面距离并跳过空距离。

        在Lumen中,使用了多种不同的SDF:

        ● 逐物体的SDF

        ● 全场景的SDF

        全场景SDF存储占用空间大,但查询速度快,因此我们选择了存储低精度的全场景SDF;逐物体SDF查询计算复杂度较高,但结果更为准确,这两者在运行时混合使用,先使用全局SDF找到大致位置,再对附近的逐物件SDF进行查询。

        对于逐物体的SDF而言,由于我们会在场景中使用实例化复用相同模型,因此只需要存储同一份数据,并应用旋转缩放等即可。

        Mesh Card

        虽然已经使用了SDF来描述场景信息,但是它仅仅包含了位置信息,对于场景中的物件而言,Lumen没有选择把物体属性也存储在SDF中,而是使用了Mesh Card来描述物体属性。

        Mesh Card在模型导入时离线生成,它是带朝向的box,可以提供表面的光照采样的位置和方向。

        Surface Cache

        接下来,基于离线生成的Mesh Card和SDF,我们可以开始收集场景信息。

        Mesh Card只记录了位置方向等几何信息,我们还需要额外收集材质信息,这部分信息记录在Surface Cache中。

        Surface Cache是运行时收集的,因此我们会以有限的预算去捕获相机附近的Mesh Card,且离相机近的Card分配的分辨率高,离相机远的Card分配的分辨率低。

        Surface Cache数据类似于多张GBuffer的纹理,包含了albedo,opacity,depth等信息,以Atlas的形式pack在一起,包含了多个不同分辨率的Mesh Card的信息。      

reference:《unreal-engine-5-goes-all-in-on-dynamic-global-illumination-with-lumen》
Surface Cache贴图
        
        Voxel Light

        现在,我们可以把光照信息注入到相机附近的层级体素里,此处的体素为世界空间的3D纹理,以clipmap的形式存储。

        我们从每个点出发向六个方向发射光线,捕获到对应的Surface Cache,记录Radiance。

        完成捕获后,我们按照如下步骤来填充voxel中的radiance:

        1.直接计算Surface Cache的直接光照信息,并将捕获的Surface Cache的直接光照信息注入到Voxel中;

       2.基于包含了直接光的voxel,采样得到一次反弹的光照,并将其和直接光加到一起;

       3.经过多次迭代,得到光照多次反弹的结果;

        Irradiance光照计算

        Lumen的光照计算以软件光线追踪为主,它综合了多种查询方案来获得更高的效果:

        ① 屏幕空间(Sreen Space Radiance Cache)

        ● 放置probe

        我们把屏幕空间划分为多个tile(1/16分辨率),并在每个tile的角落处均匀放置screen probe,probe使用八面体结构存储,每个probe使用8x8分辨率存储捕获到的信息(共64根光线),这些数据按照屏幕上的顺序pack到一张大图上。

        我们后续会使用这些probe进行插值,如果某处像素无法插值(比如深度差异过大不连续),我们就会在此处增加一个probe,通过这种自适应的方式来布点。

reference:《Radiance Caching for real-time Global Illumination》
自适应probe放置,上图红色部分为无法插值的像素,白色的点为probe在屏幕空间放置的位置,从左到右分别为16个像素间隔、8个像素间隔、4个像素间隔,以及1像素间隔的情况;我们重复这个迭代,直到屏幕上几乎没有无法插值的像素

        ● 捕获radiance

        每个probe基于HZB进行多次cone tracing捕获周围的RadianceHitDistance

        为了降低计算量,我们会将这个计算缓存下来,称为screen space radiance cache,每帧我们仅计算第N个像素,并在帧与帧之间抖动,通过时间超采样把这些结果累加到一起。

         ● 重要度采样

        常规的采样会带来一些噪点,因此ue5采用了重要度采样优化,我们更倾向于在光照更亮的地方,或者说,我们期望在BRDF和光照贡献更高的地方进行采样。

        可以通过查询最后一帧的屏幕亮度缓存,估计入射光线的来源,我们将当前位置重投影到上一帧去查询,如果重投影失败(上一帧不在屏幕中或者在屏幕外),我们从world radiance cache查询。

reference:《Radiance Caching for real-time Global Illumination》
上图从左到右依次为,八面体对应的重投影BRDF图像,八面体对应的重投影Lighting图像,以及八面体8x8的图像;如果光线交点在两者贡献更高的位置,在此处执行光线的超采样,而在没有贡献的方向剔除光线,我们会把超采样的结果混合后存储到probe贴图中

        ● 过滤

        为了降低噪点,我们需要在radiance cache中,对传入的光照进行过滤。

        对邻域的probe而言,虽然它们存储了相同的采样方向的结果,但由于probe的世界坐标位置有所差异,所以可能采样到差异较大的对象,通常我们会通过深度的差值来量化。

reference:《Radiance Caching for real-time Global Illumination》
如上图,左边白色的竖线表示屏幕,两个黄色的圈是屏幕上的两个probe,可见它们发出了相同方向的两条光线,上面蓝色的光线命中了球体,而下面白色的光线没有命中球体,这是通常会出现的情况
实际上我们期望的是下面的光线偏转一个角度到红色的光线处,以命中同一位置​​​​​;这个白线和红线之间的夹角被称为角度误差,如果超过了10度,我们认为这个邻域probe该方向是不合适的;右图中,绿线和白线的夹角小于10度,所以会参与最终的计算

        对于每一个probe,我们将它和邻域的八个probe(3x3)进行对比,过滤掉(对于该probe而言)不合格的光线方向,这些方向将不参与后续与该probe的插值计算:

        1.角度相差过大

        2.命中深度相差过大

reference:《Radiance Caching for real-time Global Illumination》
将降采样的间接光照整合到全分辨率的场景上

        如上图所示,在捕获的降分辨率的Radiance Cache Probe上,我们执行全分辨率的插值和积分,以及时域降噪。

        ● 插值

        对屏幕上的每个像素,使用所在tile附近的四个probe按照权重进行插值,并且跳过上一步中过滤掉的方向。

        我们使用到当前像素平面的距离作为权重(使用法线和深度计算),降低距离较远的probe的权重,这样可以避免前景的光照泄漏到背景,如果距离权重超过某个阈值,我们会直接舍弃这个probe。

        ● 积分

        我们把插值后的结果转换存储为三阶SH形式,以便进行快速Irradiance积分计算,并且转换为SH相当于做了低通滤波,也可以消除其中一部分噪点。

        对于镜面反射,我们沿着GGX波瓣采样对应方向的screen space radiance cache。

        ② 世界空间(World Space Radiance Cache)

        屏幕空间只会去处理近距离的采样点,如果采样点不在屏幕内,或者采样点虽然在屏幕内但距离比较远(意味着需要步进较远距离),我们都会跳转到使用世界空间的radiance cache,这是通过设置屏幕空间查询的半径实现的(比如2米)。

        world space radiance cache的分布密度仅为1/64分辨率,它同样以probe的形式记录了Radiance和TraceDistance,每个probe存储32x32分辨率纹理的信息。我们记录的是相机周围的world space radiance cache,因此它可以用更低的频率随着相机移动增量更新。

        需要注意的是,我们最终的光照计算依然在屏幕空间,只是在捕获屏幕空间probe的radiance cache时,我们不再在从screen space开始查询,而是去读取或更新world space radiance cache,然后把结果记录回screen space radiance cache。

reference:《Radiance Caching for real-time Global Illumination》
我们从ue5分享的这个ppt就能看出这一点,我们所有的计算都是基于Screen Space Radiance Cache的,只是我们需要查询的信息如果在屏幕空间上找不到,或者说需要ray过远的距离,对性能不太友好,我们才会到提前准备好的World Radiance Cache去找这个信息,查询到的结果会写回到Screen Space Radiance Cache。所以我们可以把World Radiance Cache看作是一个fallback。

 

        具体的查询方式是,输入特定位置和方向的光线,我们基于逐物件的SDF进行trace,并从命中物体对应位置的Surface Cache获取radiance;如果距离更远,我们从Global SDF进行trace,并从对应位置的Voxel Light中获取radiance;对于无限远的距离,我们从Cubemap获取Radiance。

        和相邻的screen probe一样,screen probe和world probe在位置上也会有偏差,一个screen probe会落入周围8个world probe之间。world probe查询会跳过screen覆盖的半径,又由于两者距离存在偏差,world probe可能会恰好跳过了近距离的遮挡物,导致漏光,因此我们通常使用修正后的方向在世界查询,即世界空间probe和屏幕射线终点的连线。

总结
        场景表示

        计算场景的全局光照,很重要的一点是确认光的传播路径,我们有非常多形式去表达这一点,比如在前文提到的:

        ① 环境光遮蔽(AO)/ 环境法线(Bent Normal)

        描述光线从视点出发的遮蔽情况;

        ② 天光遮蔽(SO)

        描述光线从光源出发的遮蔽情况;

        ③ 反射阴影贴图

        通过渲染局部光的反射阴影贴图,基于可见点生成一次反弹的次级光源;

        ④ 传输向量(transfer vector) / 传输矩阵(transfer matrix)

        以球谐形式记录某一点处多个方向上可见性和余弦的乘积;

        ⑤ 距离(Distance) / 距离平方(Distance Square)

        通过记录probe周围的平均距离来判断几何关系;

        ⑥ 体素(Voxel)

        把场景体素化,并记录每个体素格子的遮挡因子;

        ⑦ Cluster

        把场景建模成cluster,并预计算形状因子;

        ⑧ 距离场(SDF)

        把场景建模成3D距离场,用于加速光线追踪;

        不管以怎样的形式,要么以其它的数学形式建模来转换描述整个场景,要么描述局部的相对拓扑关系,我们只要有办法获得光线在某些地方能否传输这样的信息即可。

        光照表示

        主流的全局光照算法可以分为两种形式,一种是记录radiance信息的,这意味着我们需要经过运行时的积分转换为irradiance。

        对于Radiance而言,我们也有多种方式进行收集:

        ① 通过RSM生成次级光源;

        ② 通过体素传播radiance;

        ③ Cone Tracing收集radiance;

        ④ 软光追收集radiance;

        ⑤ Radiance Volume(Probe);

        对于Radiance而言,我们可以认为它分为环境光直接入射radiance,第一次反弹的radiance(次级光源),以及第n次反弹的radiance。

        在这一部分的处理上,我们可能会:

        ① 预计算多次反弹的结果,存储在Radiance Volume中;

        ② 运行时收集多次反弹的结果,可分摊到多帧完成;

        为了将Radiance转换为Irradiance,我们可能会:

        ① 使用随机采样/重要度采样进行蒙特卡洛积分;

        ② 利用球谐函数的快速卷积性质积分;

        另一种是记录Irradiance信息的,这意味着我们可以直接应用,比如:

        ① Irradiance Volume / Probe;

        ② IBL diffuse map;

        ③ Irradiance光图;

        对于收集的Irradiance,我们也有多种应用的形式,比如:

        ① 取周围最近的四个probe作为四面体进行插值

        ② 填充到Irradiance Volume,采样三维纹理 (比如ue称之为Volumetric Light Map)

        漏光处理

        漏光是全局照明中非常常见的一个问题,它本质上是由于精度不足导致的,更为具体的说,是描述光照的尺度大于实际几何尺度。

        从信号学角度来看,相当于对连续信号进行离散采样,但由于采样密度不足,两次间隔的采样都没有捕获到这个信号,有一些信息被遗漏了。

        对于类体素的做法,由于体素的间距是固定的,所以间距较大跳过了小物体时,基本上很难避免漏光现象,唯一的方式是增大体素点。

      对于类probe的做法,我们可以通过probe的摆放,比如在物体旁或容易发生漏光的薄墙壁旁摆放更多的probe来缓解漏光的现象,但这也意味着我们需要一些层级结构来组织这些数据,而不能使用单一的三维纹理。

reference:《The lighting technology of Detroit : Become Human》
重要区域摆放更多的probe

        优化布点的一种方式是虚拟偏移,即将靠近墙壁的物体内部probe偏移到墙壁附近,且该偏移仅应用于查找,这样可以减弱漏光的现象。

        我们也可以通过预先烘焙的方法,比如烘焙距离信息、标记probe所述的区域,标记室内外等方式规避一部分漏光。

        我们可以通过法线和到probe的方向之间的点乘作为权重,并丢弃一些背面的probe。

        类RayMarching/RayTracing的方式可以计算遮蔽的方向性来规避漏光问题:        

reference:《Large-scale global illumination at activision》

        除此之外,还有一种比较特别的漏光现象,是由法线产生的漏光。

结束语

        全局光照在工业界发展得非常迅速,这也得益于硬件的发展。从早期的光图/IBL,和光图uv做斗争,再到probe类体素类算法,研究如何摆放和生成数据,再到后来得益于高效卷积,PRT开始占据引领地位,再到逐渐有厂商开始尝试软件或硬件光线追踪的落地,这种发展可能让人经历从怀疑到尝试,再到认可的心态,或者是在熟悉的领域中固步自封。

        在这个过程中,始终应该保持一种开放的心态——辐射学的理论,光线运动的形式是作为客观事实存在的,真实感图形学本质上是工程科学而非自然科学,背后可能会涉及到高等数学、信号学、统计学、光学等,它所在做的事情是在尽可能拟合真实世界,而在不同的硬件环境下,总会有更适合的算法,工程上的一些“妥协”也常常受限于具体的条件,这也注定了这个领域是一个不断发展、充满活力的领域。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值