【摘抄】Nerf基础知识:体渲染
话不多说,先看论文里的流程图:
这里DNN的功能很好理解,输入一个方向输出颜色和密度。颜色好理解,这密度是干嘛的?答曰:此处颜色和密度是后面这个体积渲染的输入。DNN输出的数据经过体积渲染才能得到图片,而这个体积渲染过程可威风,所以出图之后可以反向传播,所以可以被训练。
那这个体积渲染又是什么?文中之间给了个公式:
并且说离散化的计算过程实际上是:
很显然,这公式需要背景知识才能看懂。
# 体渲染 (Volume Rendering)
渲染可以说是图形学中的核心。所谓计算机图形学,就是让计算机模拟出一个真实的世界。而渲染,则是把这个虚拟出来的世界投影成图像,正如自然界中的各种光线经过碰撞后,投影到我们视网膜上的样子。这是现代电影和游戏中不可或缺的技术。
体渲染属于整个渲染技术的分支,体渲染把气体等物质抽象成一团飘忽不定的粒子群。光线在穿过这类物体时,其实就是光子在跟粒子发生碰撞的过程。
体渲染建模的示意图如下。光沿直线方向穿过一群粒子 (粉色部分),如果能计算出每根光线从最开始发射,到最终打到成像平面上的辐射强度,我们就可以渲染出投影图像。而体渲染要做的,就是对这个过程进行建模。为了简化计算,我们假设光子只跟它附近的粒子发生作用,这个范围就是图中圆柱体包含的范围。
体渲染把光子与粒子发生作用的过程,进一步细化为四种类型:
- 吸收 (absorption):光子被粒子吸收,会导致入射光的辐射强度减弱;
- 放射 (emission):粒子本身可能发光,比如气体加热到一定程度就会离子化,变成发光的「火焰」。这会进一步增大辐射强度;
- 外散射 (out-scattering):光子在撞击到粒子后,可能会发生弹射,导致方向发生偏移,会减弱入射光强度;
- 内散射 (in-scattering):其他方向的光子在撞到粒子后,可能和当前方向上的光子重合,从而增强当前光路上的辐射强度。
于是出射光与入射光之间的变化量,可以表示为这四个过程的叠加:
L0−Li=dL(x,w)=emission+inscattering−outscattering−absorption
为什么?可以看看体积云渲染实战里面对于云遮挡其后物体的半透明特性和云被阳光照射时的发光特性,半透明就是吸收和外散射、云被阳光照射就有放射和内散射。
# 吸收 (absorbing)
先剖析吸收的过程。在从Li到L0的光路上,设有某个区域s,在s处有一个底面积是E,高是Δs的圆柱体。设圆柱体中粒子的密度是ρ。
假设粒子都是半径为r的球体,那么每个粒子的最大横截面积是A=πr2(即,每个粒子对光线的最大遮挡面积)。当圆柱体的高s足够小,小到跟粒子直径一样大的时候(Δs→0),可以认为粒子在圆柱体高这个方向上不会互相重叠(即,粒子都平铺在圆柱体的底面上)。
此时,圆柱体体积为EΔs,粒子总数为ρEΔs。这些粒子遮挡的面积为ρEAΔs,占整个底面积的比例为ρEAΔs/E=ρAΔs。也就是说,当一束光通过这个圆柱体的时候,有ρAΔs的概率会被遮挡。换句话说,如果我们在圆柱体的一端发射无数光线 (假设都朝相同的方向),在另一端接收,会发现有些光线安然通过,有些则被粒子遮挡 (吸收)。这些接受到的光线总强度Io,相比入射光线总强度Ii而言,会有ρAΔs比例的衰减:
Io−Ii=−IiρAΔs
另外,在光线行进方向上,s还可以看作是光线在一个长圆柱体中的行进距离,在这样的圆柱体中,光线的强度显然会随着行进距离而变化,因此可以表示为s的函数I(s);此外,s还可以看作是圆柱体内部的一个位置,圆柱体每个区域的密度可以不同,于是不同位置处的粒子密度也可以表示为s的函数ρ(s)。于是上述公式又可以表示为:
ΔI=Io−Ii=−I(s)ρ(s)AΔs
于是就能写成一个微分方程:
dsdI=−I(s)ρ(s)A=−I(s)τa(s)
解之:
I(s)=I0e−∫0sτa(t)dt
其中I0表示常微分方程中的常数项,物理意义上表示光线的起始点处的光总强度。
上式有丰富的物理意义。如果介质 (粒子群) 是均匀的,即τa(t)处处相等,那么入射光在经过介质 (粒子群) 后,辐射强度会呈指数衰减。这被称为比尔-朗伯吸收定律 (Beer-Lambert law)。
此外,还可以定义 “透射比”(transmittance):
T(s)=I0I(s)=e−∫0sτa(t)dt
它表示从光路起点到某一点之间的粒子云的透明度,数值越大,说明粒子群越透明,光线衰减的幅度就越小。
而透明度本身是关于τa(t)的方程,τa(t)越大,T(s)就越小。而τa(t)=ρ(s)A,它是由粒子密度和投影面积决定的。这在直觉上也很好理解,如果粒子密度大,粒子本身也比较大,那么遮住光线的概率也会相应提升,自然透明度也就下降了。τa(t)也被称为光学厚度 (optical depth)。
# 放射 (emission)
除了吸收之外,粒子本身也可能发光。
假设粒子单位横截面积发射一束光的辐射强度为Ie。按照前文描述,在圆柱体高度足够小的情况下,粒子总的发射面积是ρAEΔs,则总的发光强度为IeρAEΔs。如果我们在圆柱体一端去接收粒子们放射的光线,会发现有时候能接收到,有时候刚好接收点所在的光路上没有粒子,就接收不到。能接收到光线的概率为ρEAΔs/E=ρAΔs,那么接收到的光线的平均强度为IeρAΔs。同样可得放射光强的常微分方程:
dsdI=Ie(s)ρ(s)A=Ie(s)τa(s)
类似吸收,粒子放射的光强同样和τa(s)有关,这在直觉上也是合理的,如果粒子能发光,那粒子密度和粒子颗粒越大,放射的辐射均值也就越大。
# 外散射 (out-scattering)
粒子除了吸收光子,也可能会弹射光子,这个过程称为外散射,即光子被弹射出原本的光路,导致光线强度减弱。
同吸收一样,外散射对光线的削弱程度,也跟光学厚度相关,不过过程相对吸收来说又复杂一些,因此我们用τs来表示外散射对光线的削弱比例,以区别于τa:
dsdI=−I(s)τs(s)
# 内散射 (in-scattering)
光子可以被弹射走,自然就有其他光路的光子被弹射到当前光路,这一过程就是内散射。
内散射的过程比外散射又更加复杂,因为弹射到当前光路的光子可能来自多条不同的光路,因此需要综合考虑其他光路的辐射强度以及各种弹射角度。
我们可以认为来自外部的光线强度为Is,在穿过当前光路时,被散射减弱的能量占比为τs,这些能量显然不会凭空消失,而是回留在当前光路内。于是内散射的光线强度就是:
dsdI=Is(s)τs(s)
# 体渲染方程
把以上四个过程都综合到一个公式中:
dsdI=−I(s)τa(s)−I(s)τs(s)+Ie(s)τa(s)+Is(s)τs(s)
其中,吸收和外散射都会削弱光线的辐射强度,并且由于它们都和入射光有关,因此它们共同构成了体渲染中的衰减项 (attenuation item);而粒子发光和内散射都来自独立的光源,因此被称为源项 (source item)。
令τt=τa+τs,简化之:
dsdI=−τt(s)I(s)+τa(s)Ie(s)+τs(s)Is(s)
求解之,得光路上任意点s的光强公式:
I(s)=I0e−∫0sτt(t)dt+∫0se−∫0tτt(u)du[τa(t)Ie(t)+τs(t)Is(t)]dt
即体渲染方程,其中I0为入射光强度。
这个公式中,I0e−∫0sτt(t)dt即为入射光在光路内强度衰减后在光路终点处的光强;∫0se−∫0tτt(u)du[τa(t)Ie(t)+τs(t)Is(t)]dt即为粒子自己发光和来自外部的光源通过内散射带来的光强。
# 向Nerf靠拢:Nerf中的体渲染公式
在上述体渲染公式和Nerf论文中所给的体渲染公式之间还有一道坎。
为了简化上述公式,进一步假设σ(u)=τt(u),σ(t)C(t)=τa(t)Ie(t)+τs(t)Is(t),上述公式化为:
I(s)=I0e−∫0sσ(t)dt+∫0se−∫0tσ(u)duσ(t)C(t)dt
进一步令T(s)=e−∫0sσ(t)dt:
I(s)=I0T(s)+∫0sT(t)σ(t)C(t)dt
即Nerf论文中的公式多加个背景光项。
仔细看上面的公式,前面说过τt(u)是光学厚度,因此σ(u)也就同样表征了在光路上的距离u处的粒子密度×粒子投影面积;而τa(t)Ie(t)和τs(t)Is(t)分别是放射和内散射的dtdI,都是表征光线在光路上的距离u处被增强了多少;于是C(t)=τt(t)τa(t)Ie(t)+τs(t)Is(t)就可以理解为单位粒子投影面积对光线在光路上的距离u处增强的贡献度,也就是每单位粒子投影面积发出了多少光。
σ(t)C(t)与透射比T(s)相乘,相当于将吸收与外散射给光线对光线的减弱也考虑进来。 背景光I0T(s)就是起点光强×透射比,很好理解。Nerf里忽略了这项,即假定所有光线均由粒子发出。
此外,在Nerf里C表示颜色,这个操作稍微想想就能明白:颜色可以表示为向量,比如RGB三位向量里的值就是三种原色的光强度,上面的公式可以表示一种颜色的光线强度,而不同光线互不干扰,每种原色列一个单独的公式表示,最后三种原色的I(s)组一个向量就能表示颜色了。
# 向Nerf靠拢:Nerf中的离散化体渲染计算公式
上述积分在计算机中是无法计算的,需要离散化。
我们将整个光路[0,s]划分为N个相等间距的区间[tn,tn+1],那么只要能算出每个区间内的辐射强度变化量I(tn+1)−I(tn),最后把 N个区间的辐射加起来,就可以得到最终的光线强度了。N越大,则越接近理论数值。
在尽可能小的区间[tn,tn+1]内,可以将σ(t)和C(t)看作常量σn和Cn,于是:
I(tn+1)−I(tn)=I0T(tn+1)−I0T(tn)+σnCn∫tntn+1T(t)dt
进一步令t1=0,tN+1=s,则I(t1)=I0T(t1)=I0T(0)=I0(在起点s=0处只有背景光),可以将近似离散求和逼近I(s):
I(s)≈I(t1)+n=1∑N(I(tn+1)−I(tn))=I(t1)+n=1∑N(I0T(tn+1)−I0T(tn)+σnCn∫tntn+1T(t)dt)=I(t1)+I0T(tN+1)−I0T(t1)+n=1∑NσnCn∫tntn+1T(t)dt=I0T(tN+1)+n=1∑NσnCn∫tntn+1T(t)dt
而T(t)=e−∫0tσ(u)du需要从起点开始计算,但可以拆分为两段。令T(tn⇒t)=e−∫tntσ(u)du,T(t)可拆为:
T(t)===e−∫0tσ(u)due−∫0tnσ(u)due−∫tntσ(u)duT(tn)T(tn⇒t)
~(有点奇怪,一个差分项与前后的数值有联系,这项来自于微分方程求解,需要进一步思考其含义)~
(不奇怪,想想二项分布和灯泡质检的故事,当前光照强度的变化量和当前光强绝对量有关)
于是,主要的被求和项可以化简:
σnCn∫tntn+1T(t)dt========σnCn∫tntn+1T(tn)T(tn⇒t)dtσnCnT(tn)∫tntn+1T(tn⇒t)dtσnCnT(tn)∫tntn+1e−∫tntσndudtσnCnT(tn)∫tntn+1e−σn(t−tn)dtσnCnT(tn)eσntn∫tntn+1e−σntdtσnCnT(tn)eσntn(−σn1e−σntn+1−−σn1e−σntn)CnT(tn)eσntn(e−σntn−e−σntn+1)CnT(tn)(1−e−σn(tn+1−tn))
带入上述离散求和式:
I(s)=≈=I0T(s)+∫0sT(t)σ(t)C(t)dtI(t1)+n=1∑N(I(tn+1)−I(tn))I0T(tN+1)+n=1∑NCnT(tn)(1−e−σn(tn+1−tn))
进一步令δn=tn+1−tn表示采样步长,则T(tn)也可以离散化:
T(tn)=e−∫0tnσ(u)du≈e−∑k=1n−1σkδk
再令Tn=e−∑k=1n−1σkδk代表离散化的T(tn),则最终离散化的体渲染公式为:
I(s)≈=I0T(tN+1)+n=1∑NCnT(tn)(1−e−σn(tn+1−tn))I0TN+1+n=1∑NCnTn(1−e−σnδn)
对照Nerf中的离散化体渲染公式:
还是多了个背景光项I0TN+1,这说明Nerf中的体渲染不考虑透射和外散射,所有的光线都是物体表面发出的。即Nerf的使用场景中不存在透明物体,直觉上讲还是很合理的,因为体渲染原本用于渲染云雾,Nerf虽然用了体渲染但是却用来渲染物体表面,这个场景下光线就是来自物体表面。
# Nerf中的体渲染
从论文中给出的系统结构图可知,Nerf中的DNN输入是相机位置(x,y,z)和射线朝向(θ,ϕ),其组成5元组表示一条光路;DNN输出是这条光路的各离散采样区间δn内的粒子颜色cn和其粒子密度σn(即光学厚度),之后,按照上述离散化体渲染公式进行积分操作,即得到这条光路射出的光线颜色,就像这样:
于是,渲染过程如下:
- 指定相机的外参(位置、方向等)和内参(焦距、视野、分辨率等)根据外参内参计算出需要采样的各光路的(x,y,z,θ,ϕ)
- 将每个光路的(x,y,z,θ,ϕ)输入DNN,计算得到光路上各离散采样区间δn内的颜色cn和粒子密度σn
- 按照离散化体渲染公式进行积分操作,即得到每条采样光路的颜色
- 根据这些采样光路的颜色和相机内参,计算出相机拍到的图像
- 上面这个离散化体渲染公式很显然是可微的,所以将计算得到的图像和Ground truth作差进行反向传播训练。