# Advanced Topics in Rendering(渲染中的高级主题)

# Advanced Light Transport

image-20221015102020490

在高级的光线传播里,我们会说到无偏的和有偏的光线传播方法。无偏光线传播里的代表有双向路径追踪(BDPT)以及 Metropolis 光线传播(MLT)。有偏光线传播会提到光子映射(Photon mapping)以及 VCM。我们还会提到实时辐射度算法,相当于把间接光表示成很多很小的光源。我们先来看看什么是无偏和有偏。

image-20221015102039788

我们做光线追踪,很多方法都用到了蒙特卡洛的估计方法。蒙特卡洛的估计方法有些是无偏的,也就是说蒙特卡洛估计出来的结果如果无论用多少个样本,它的期望永远都是我们要的真实值,那么这个估计方法就是无偏的。相应的其它的情况那么就都是有偏的了,也就是说估计出来的期望和我们最后要的值不一样。在有偏里面有一个特殊情况,就是在一个极限的定义下,如果用的样本的数量足够多,这种情况下得到的期望值会收敛到正确值,我们称这种情况为 consistent。

image-20221015102104065

我们之前说路径追踪,是利用了光路的可逆性从相机开始的路径追踪,这条路径连接了相机和光源。而对于双向路径追踪,它会生成两个不同的半路径,从光源会打出一些从光源出发的半路径,从摄像机出发也可以生成一系列的半路径,双向路径追踪会把半路径直接的端点给连起来,由此形成一条完整的路径。这个思想是非常简单的,但是实现起来却十分困难。

image-20221015102125072

双向路径追踪在有些条件下效果非常好,例如在上面场景中,左边是路径追踪,右边是双向路径追踪,而且每个像素都是用的 32 个样本,为什么双向路径追踪比单向路径追踪效果好这么多呢?这是因为如果我们想要找到一条带有大量能量的路径对于这个场景来说不太容易,场景中有一盏光源,这个光源仅仅只朝着上方向打光,也就是说整个场景实际上都是被间接光给照亮的。对于这种情况来说,如果我们做路径追踪从摄像机出发射出光线,打到墙面之后由于是漫反射表面所以很难弹射到光源位置,也就是说,当光线传播在光源这半边比较容易算的时候,那么 BDPT 的效果就会更好,但是它会慢很多。

image-20221015102148513

Metropolis 光线传播用到了统计学上一个采样的工具,这个工具叫作马尔科夫链,假如当前有一个样本,马尔科夫链可以告诉我们根据当前的样本生成的跟它靠近的下一个样本。它可以做到给定足够的时间的情况下,马克可夫链的蒙特卡罗方法可以生成一系列以任意函数的形状为 PDF 生成的样本。我们之前说过蒙特卡罗方法可以用任意的 PDF 来采样一个函数,但我们并没有说用什么样的 PDF 采样一个函数是最合适的,结论是当我们采样的 PDF 和我们要积分的函数形状一致的时候,这个时候得到的 Variance 是最小的。任何未知的函数都可以通过马尔科夫链的方法生成一系列的样本,使得这些样本的分布它就是和被积函数的形状一致。既然是有一个样本可以变出更多的样本出来,反映到路径追踪就是在已经有一个路径的情况下,可以在它周围产生更多跟它相似的路径,例如在上图中我们已经找到了一条蓝色的光路,现在对这条光路做一些微小的扰动,改变一下各个交点的位置就能形成一条新的路径。我们通过这种在一个 path 周围产生更多新的 path 的方法,最后我们就可以把所有的 path 的都找到,这就是 Metropolis 方法的实现原理。

注:Metropolis 是人名。

image-20221015102214630

Metropolis 方法在很多情况下效果都是不错的,它非常适合做困难的光路传播,因为我们只要能找到其中的一条作为种子,那么在它的周围就能不断的找到更多的路径。在上图中左边半开的门,光线只能通过门缝进来,右边是泳池底下波纹的形状,这个现象叫作 Caustics,也就是说光线通过聚焦打在泳池底部,然后被我们看见,其间经过了两次折射,一次漫反射,这种路径我们简写为 SDS(Specular-Diffuse-Specular),路径追踪解这种 Path 非常困难,这是因为当路径打到水底的时候由于 Diffuse 根本不知道光线被反射到哪里去,只有打到很少的一部分才可以在水面和光源连线,形成一条有效的光路。但只要我们能找到一些光路,MLT 可以在周围找到更多的光路,所以它的效果看上去就很好。

image-20221015102236016

但 MLT 也存在它的问题。Metroplis 方法很难在理论上分析它最后会收敛的速度,如果是用 Path Tracing 大概是能有一个估计的,我们能知道它会以什么样的一个速度进行收敛,简单的蒙特卡洛方法是可以分析 Variance 的,例如如果要提高一倍的 Sample 的数量,是不是能够降低一倍的 Variance,是有这么一系列的关系的。但是 Metroplis 方法很难估计这样的关系,所以我们不知道什么时候能够收敛,或者说什么时候能变得没有噪声。它还有一个问题就是它所有的操作都是局部的,也就是每个像素都自己做自己的,那么有些像素它就收敛的快,有些像素收敛的慢,那么最后我们图像看上去就比较赃,就像是上面这副图我们所看到的情况一样。因为我们根本不知道一张图什么时候收敛,那就更不可能作为渲染动画的方法,因为每帧对应的画面各自有些地方收敛有些地方没收敛,最后看上去动画就抖得非常厉害。

image-20221015102257087

光子映射是一个有偏的估计。它特别适合用来渲染所谓的 Caustics,它还适合处理 SDS 路径,在上图中我们可以比较直观的看到渲染的结果。

image-20221015102318424

image-20221015102333762

光子映射可以分为两个步骤进行。其中的一种实现方法是:第一步从光源出发往外辐射光子,光子往各个不同的方向打,如果碰到一些物体该反射的反射,该折射的折射,直到光子打到 Diffuse 的物体上就停下来。这一步做完之后,就把所有光子给整理起来,我们就直到所有光子在哪里。第二步从摄像机开始同样打出一些路径,它们同样该反射的反射,该折射的折射,直到打到 Diffuse 的物体上就停下来。之后我们可以把这两步给合起来做一个计算,我们要做一个局部的密度估计。这个计算建立在一个观察上,一开始我们辐射了一大堆的光子分布在物体表面上,现在又有一大堆从摄像机出发的光线也打在物体表面上了,要想判断物体表面哪些地方应该亮,哪些地方不应该亮,就是光子分布越集中的地方就越亮,光子分布越不集中的地方就越不亮,所以在第二趟打到各种不同的 Diffuse 物体表面的时候就要做局部的密度估计,也就是对任意一个着色点,我们取它周围 N 个最近的光子,这需要把所有的光子组织成一个自上而下的加速结构模式,这样我们可以迅速地定位到一个着色点周围有多少个光子,之后我们还需要计算这 N 个光子所占据的着色点周围的面积,然后计算光子的密度。

image-20221015102355855

既然要求局部光子的密度,自然就会涉及到 N 取多大的问题。如果用很少的光子数量,自然就会得到很有噪声的一张图,但如果我们找特别多的光子数量,我们法线最后生成的图会变模糊,为什么会这样呢?我们在做局部密度估计的时候,应该算的是当前这个点的密度是多少,也就是dN/dAdN/dA,但我们是给了一定的光子数去算它实际的面积,也就是ΔN/ΔA\Delta{N}/\Delta{A},这两个结果正常来说应该挺接近的,但是它们绝对不相等,也就是说,我们对密度的估计结果并不准确,这也是这个方法是有偏估计的原因。但是假如ΔA\Delta{A} 足够小的时候,那么我们的结果就接近正确,也就是说,如果我们能打出更多的光子并且我们不改变 N 的大小,那么这个 N 个光子一定会覆盖一个更小的区域,那么ΔA\Delta{A} 也就会更接近于dAdA。这些说明,当我们有无限多的光子的情况下,我们这么做就能得到一个正确的结果,这也就是我们为什么又说它是一个 consistent(一致的)方法。但是只要我们光子打出的不是无限多,那么结果多少都会有点糊,所有这个方法是有偏的但是一致的方法。

image-20221015102413796

到这里我们已经可以很好的理解有偏和无偏,只要我们得到的结果有任何一点模糊,那么就是有偏的,而一致指的是在样本无限多的情况下,就能让结果收敛到正确的结果。我们之前是找一个着色点周围 N 个光子求面积,那能不能固定一个面积数里面有多少个光子来求密度呢?但是这样做的话,我们最后得到的结果一定是有偏的并且不一致的,这是因为ΔA\Delta{A} 永远都不可能和dAdA 相同。

image-20221015102436898

VCM 把双向的路径追踪和光子映射结合了起来。也就是在我们做 BDPT 的过程中,如果有的 path 满足这样一种性质,例如在上图中,左边红色的 path 和右边绿色的 path 它们最后的端点非常接近(在同一个局部的面上),这个时候就认为其中一半过来的就类似于光子,这个时候再用光子映射把这部分的光路它们两个 Sub-path 的贡献给结合在一起。

image-20221015102455149

还有一种跟之前的完全不一样的方法,叫作实时辐射度算法(Instant Radiosity),有时也被称之为 many-light 方法。我们之前分析光线传播的时候,提到过并不区分光线是反射来的还是自己发出来的,它们都是 Radiance,实时辐射度算法就用到了这种思想。已经被照亮的面,我们都可以认为它们是光源,然后用它们来照亮别人。一开始会先从光源打出很多 light sub-paths,这些路径会停在某些地方,我们认为停住的那些地方就成为了新的光源,它们成为了新的光源之后,当我们再去看一个着色点的时候就用所有这些新的光源去照亮这个着色点,这样的话就相当于我们考虑了光源弹射两次的情况。总结起来就是,首先生成一系列的 VPL(Virtual Point Light),然后在摄像机看向这个场景的时候用这些 VPL 去照亮着色点。

image-20221015102518739

这个方法的有点就是非常快,就单说 many-light rendering,也是平时研究很活跃的一个话题。但它也存在一些问题,例如在右边的这个用 VPL 渲染的场景中,我们发现有些地方会莫名其妙的发光,特别是在一些窄的缝隙会弧线问题,这个和我们之前推出的距离平方项有关系,我们在推 Light Sampling 的时候把对立体角的采样改成了对面积的采样,如果光源和着色点之间的距离极近,那么就相当于除以了一个接近零的数,所以得到的结果会非常大。还有一点需要注意的就是,VPL 不能处理 Glossy 材质的物体。

# Advanced Appearance Modeling

image-20221015102549911

我们接下来来说一下更高级的外观建模,我们之前说过外观就是材质,材质就是 BRDF(对于表面模型来说)。我们接下来会提到一些非表面的模型,例如散射介质(参与介质)、毛发、颗粒状物体等等。表面模型里面也有些复杂一些的,例如半透明材质、布料、细节比较多的模型等等。最后我们还会提到一些程序化生成的模型。

# Non-Surface Models

image-20221015102611686

image-20221015102628995

image-20221015102655537

当一根光线在行进的过程中,如果穿进了一个参与介质,这里我们假设有一根光线穿进了云层,那么会有若干事情发生。第一,如果云中间有光源那么它会发光,我们先不考虑这种情况;第二,如果光线行进的过程中打到了云里的小晶体,它会把这条光线随机的打到其他方向上去;第三,光线在传播过程很有可能会接收到从其他方向散射过来的光;第四,传播过程中有些能量可能逐渐就被吸收了,可以想象一下乌云。总结起来就是,光线在传播过程中会发生两样事情,要么被吸收,要么被反射。

image-20221015102726123

那么光线是怎么散射的呢?是不是说光线会被均匀的散射到不同的方向上去呢?我们类比一下物体表面的散射,只有当物体表面是 Diffuse 材质的时候才认为光线是均匀的散射到各个方向上去,同样的道理对于参与介质也是一样,在任何一个点都会往各个方向散射,怎么散射由一个函数来定义,这个函数叫作相位函数(Phase Function),相位函数决定了是均匀的往各个方向散射,还是主要往一个特定的方向散射。也就是说我们在规定不同散射的方法,这就很像 BRDF,BRDF 决定光线如何反射,而 Phase Function 是在决定光线应该如何散射。

image-20221015102740795

那么我们该如何渲染参与介质呢?假设我们现在已经在参与介质中的某个地方,光线往某一方向走,能走多远说明了介质的吸收能力有多强,停下来的时候就要重新考虑要把光线往某个方向散射,直到找到一条 path,之后光源和各个弹射的地方相连计算整个路径的贡献。中间有很多数学的计算,我们这里不再考虑渲染方程,因为渲染方程告诉我们的是光线如何与物体表面作用,而没有说怎么和体积作用。

image-20221015102753428

image-20221015102810455

image-20221015102822725

image-20221015102841523

另外还有一些材质也是不定义在物体表面上,例如头发。头发是一根一根的,所以要想描述头发的光学属性,我们就要考虑光线和这种圆柱形成的曲线去作用,而不再是和一个面去作用。通过上面这张图,我们发现人的头发会形成两种不同类型的高光,一种是无色的高光,另外一种是有色的高光。

image-20221015102856232

从一开始有人研究头发开始,人们就广泛的采用一个简单的模型,也就是 Kajiya-Kay Model。有一根光线打到圆柱上,光线打到圆柱上会往四面八方散射,对于头发来说我们可以把它考虑成会产生漏斗形的散射,同时也会有一些光被散射到四面八方去,这就好像是 Diffuse+Specular 一样,和之前我们求表面很像。

image-20221015102910937

用上面的方法我们会得到上面这种头发材质,但是效果看上去不怎么好。

image-20221015102937860

一个被广泛应用的模型叫作 Marschner Model。当光线打到一根圆柱的时候,会存在一部分能量被反射掉(R),也有一部分能量折射会穿进去然后再穿出去(TT),还有一种是光线进入到头发里面,在头发的内壁上发生反射再往回走穿出去(TRT),通过这种方式就能定义各种不同的光线传播。我们之前说过,描述一个材质就是描述它应该和光线如何作用。

image-20221015103000250

Marschner Model 把头发当成是一个玻璃的圆柱,并且把外层称为 cuticle,内层称为 cortex,并且头发内部有色素,如果有光线穿进头发,在传播过程中会被部分吸收,然后再穿出去。Marschner Model 考虑了我们之前说的三种光线和圆柱的作用,也就是 R、TT、TRT。

image-20221015103024963

把这些综合考虑在一块儿,就能得到非常好的头发渲染效果。但我们上面定义的只是光线如何和一根头发进行作用,如果要考虑光线和多根头发作用,那么就要考虑光线打到一根头发出来之后又会打到第二根头发,以此类推,直到从无数根头发里面弹射出光线到我们的眼中。

image-20221015103044443

image-20221015103100208

image-20221015103128140

那么人的头发能不能用来描述动物的毛发呢?通过上面的图,我们发现如果把人的头发的模型应用到动物身上,会发现不对,右边的图才是我们想要的结果。这也说明,人的头发的模型不足以描述光线如何与动物毛发作用。

image-20221015103153485

那这到底是怎么回事呢?我们可以先看看它们在生物上的结构,如果我们研究人和动物毛发我们会发现一些共同点。我们把毛发沿径向切开,其中最外面的表皮叫作 Cuticle,里面是 Cortex,最内层的髓质叫作 Medulla。髓质内部的结构非常复杂,光线进入之后会像是进入了散射介质一样被散射到四面八方上去。人和动物都有上面提到的三种结构,但是对于动物来说最内层的髓质却大的可怕,也就是光线进去之后更容易的会发生散射,这就是我们之前的模型没能描述的一个东西。

image-20221015103209625

image-20221015103236989

image-20221015103309750

所以就有人提出了 Double Cylinder Model(双层圆柱模型),双层圆柱模型把髓质这一层给精准的描述了出来。

image-20221015103334689

双层圆柱模型增加了两种光线在穿过髓质的时候会发散到各个方向上去的情况,分别是TTsTT^sTRTsTRT^s

image-20221015103356190

于是我们便可以用五个不同的分量来模拟动物毛发的材质效果。PS:闫老师发明的。

image-20221015103427282

image-20221015103520083

image-20221015103538058

image-20221015103557296

其他还有一类材质被称之为颗粒材质,例如沙子、盐、糖等等。

image-20221015103625631

当一堆这种颗粒状的物体在一起的时候就会很麻烦,所以人们就想出了一些简化的方法,例如计算每一个单元上各种颗粒成分的占比,但是生成一张布满颗粒的图花费的时间是相当多的,到现在颗粒材质也没有得到非常好的解决。

image-20221015103644039

image-20221015103715105

# Surface Models

image-20221015103740542

image-20221015103754728

前面提到了非表面的一些模型,接下来我们再来说说表面的一些模型,例如 Translucent 材质。其中玉石就是一个典型的代表,光线在穿过透明介质过程中,不仅涉及到吸收,还涉及到散射。这说明光线可以从某一个地方进入这个表面,并且从另一个地方出这个表面。

image-20221015103822679

反映在物理上就是,光线从一个点进入了某个表面,在物体内部进行了大量的散射,最后再从另一个点钻出去。为了描述光线这样一种散射方式,我们定义了次表面散射(Subsurface Scattering)。

image-20221015103838634

BSSRDF 可以理解成对 BRDF 的延伸,我们平常说的 BRDF 是光线打到一个点之后又从这个点出来,但是在 BSSRDF 上,我们就可以理解成光线从一个点进来可以从任意一个其他的地方出去。因此此表面散射对应的 BSSRDF 就是定义了光线从哪个点进来、从哪个方向进来、从哪个点出去、从哪个方向出去。那么相应的渲染方程也要改写,也就是原本对于各个方向积分,现在考虑一个着色点,由于它的次表面性质,也就说明其他着色点也是有可能贡献到这个着色点的,并且沿着一个方向去的,也就是说我们不能只考虑从各个方向上进来的光,还得考虑从各个方向进到其他着色点的光,所以我们不仅要对方向进行积分,也要对面积进行积分。

image-20221015103901481

人们发现 Translucent 材质看上去就是,当一根光线打在这个物体上,就好像这个物体的地下出现了一个光源从底下照亮这个着色点周围的一片,为了物理上的真实,大家发现这一个光源还不够,还得在上方再对应一个光源,所以相当于是有两个光源,用这两个光源去照亮着色点周围这一块儿,就很像次表面散射得出来的结果,这个方法叫作 Dipole Approximation。

image-20221015103915841

image-20221015103927829

image-20221015103944270

image-20221015104000545

image-20221015104024137

image-20221015104043849

说完次表面散射,我们再来说另外一个也是和表面相关的材质,也就是布料。为了理解布料,首先我们要清楚它的制作原理,布料是一系列缠绕的纤维构成的,但是它的缠绕有几个不同的层级。不同的纤维首先缠绕成股,不同的股再经过缠绕形成线,然后再用线给织成布料。

image-20221015104058253

对于布来说,既然是缠绕而成的,那么要算它的表面模型就非常头疼,因为它一定会和纺织的方向有关,或者说和织法有关。通过各种各样编织的方案,我们就能知道它最后看上去是什么样子,也就得到了一个 BRDF 的模型。

image-20221015104114603

当然这里用 BRDF 模型肯定有些问题,如果这个布本身真的是在一个表面上,那么我们把它当作一个平面没有问题,但是像天鹅绒这种材质,它本身就不是一个平面,那么用 BRDF 来表示就不合理。

image-20221015104126201

一个更准确的做法,是把织物当做是在空间中分布的体积,然后把空间中分布的体积划分成超级细小的格子,每一个格子里面大概知道它纤维的朝向分布之类的信息,之后我们就能把这些性质转换成光线的吸收和散射,这就像是在渲染云一样,我们不再把布料当作面而是一个体积,但是相应的计算量也会急剧上升。

image-20221015104141652

还有一种做法是,因为布料本身就是纤维,并且我们知道这些纤维是怎么缠绕的,那么一个最暴力的做法就是把每一根纤维给渲染出来,也就是把它当成人的头发一样进行渲染,得到的结果自然看起来会很真实,但是同样计算量也是非常非常惊人的。

image-20221015104154405

image-20221015104207342

image-20221015104231369

image-20221015104256547

现实生活中,一束光打在车子上,我们会明显的看到车上会有划痕,包括鼠标上的高光也是由很多小的凸起形成的高光,而不是一个光滑的表面形成的。也就是说,真实世界中很多东西都是不完美的,但是我们的描述方法却会得到一些很完美的东西。

image-20221015104313730

image-20221015104327522

image-20221015104347291

image-20221015104501822

image-20221015104750805

我们之前讲过微表面模型最重要的是这些微表面的法线分布,但实际上大家描述这个分布用到的是一些非常简单的模型,例如正态分布,但如果我们用这种分布去描述法线的分布,得到的自然是一些看上去并没有什么细节的结果,但是实际上我们想要看到的是法线的分布基本满足统计的规律,但它又带有自己的细节,如果我们能把这些考虑进去,那么自然而然就能得到好一点的结果。

image-20221015104809224

image-20221015104829393

image-20221015104848416

image-20221015104911510

虽然我们可以定义各种各样的细节,但是渲染出它们却非常困难,例如上面这个蜗牛渲染出来大概会花费一个月的时间。

image-20221015104927602

之所以渲染很困难,根本原因在于我们认为每一个小的微表面是一个镜面,那么从摄像机打一根光线过去,打到任意一个表面都能知道它的法线,那么就知道镜面反射的方向是什么,因此很难通过反射的方式让反射的光线打到光源上去,同理从光源打过来也是一样。

image-20221015104948396

所以这里会考虑一个像素会覆盖很多的微表面,如果我们能够能够把这些微表面的法线分布算出来,那么就能替代原本光滑的分布,并且用在微表面的模型里。

image-20221015105003352

如果我们考虑一个像素覆盖了多大的范围,那么就能得到各种各样神奇的法线分布(NDF)。如果一个像素覆盖了非常多的微表面,那么这些微表面自然而言就会显示出一些统计学的规律。如果覆盖的范围比较小,那么它就会显示出一些很独特的性质。

image-20221015105018594

并且不同类型的法线贴图会引起不同的法线分布。

image-20221015105032921

image-20221015105115185

image-20221015105133366

image-20221015105157368

image-20221015105219786

当我们引入这些细节之后,如果还是在用几何光学来解释就不对,当物体非常小的时候,小到和光线的波长相当的时候,我们就不能假设光线的传播是沿着直线传播,而是需要假设这个光线是一个波,因为会涉及到衍射、干涉之类的现象发生。这些现象在这种微型的细节上就会发生。

image-20221015105241021

image-20221015105258792

image-20221015105317934

波动光学得到的 BRDF 和几何光学得到的 BRDF 挺像的,但是又有自己的特点,也就是不连续的特点,这是由于光会发生干涉,干涉会引起有些地方加强有些地方减弱,就会形成这种不连续的效果。

image-20221015105338681

image-20221015105402461

image-20221015105419429

image-20221015105433598

# Procedural Appearance

image-20221015105503138

最后一个话题有关于程序化生成的外观,程序化生成指的是用一定的方式来指导它的生成,而不需要真正地去生成它,可以动态的去查询它。例如这个花瓶刷碎了,我们可以看到花瓶内部的花纹到底是什么样子的,也就是可以定义一个三维的纹理,但是存储量在三维的情况下瞬间就会上去,所以我们不存储这个三维的纹理,而是什么时候要用,什么时候去查,就好像我们有一个空间中的函数f(x,y,x)f(x,y,x),然后给定任意的 xyz,立刻能得到一个值就行。

image-20221015105524755

这种函数有一个特定的名字,叫作噪声函数。这些函数允许我们得到一些很神奇的 noise,由于它们定义在空间中,所以即使上面的花瓶摔碎,我们也能看到它的切面长什么样。之所以叫 Procedural,实际上就是随用随取的意思。

image-20221015105756080

像游戏中车子上的锈,我们就能靠生成一系列的噪声,如果这些噪声我们认为是在 [0,1] 的范围,我们可以对它做二值化以及各种后期的处理,只要处理得当,我们就能得到任意不同的效果。

image-20221015105820351

image-20221015105833004

image-20221015105854036

image-20221015105925085