半年前退坑的我大概也想不到终于有一天我会遗忘这些基础技艺并且会重返Rendering
0. 闲话:重拾Volumetric Rendering的一些碎碎念
这学期选了图形学课,自然而然也要准备大作业。我的想法自然是结合曾经学过的那些东西,为自己的Rendering之路画上一个句号。这其中就包括那个我没有爬上去的门槛——双向路径追踪,半年前彻底地读了Veach写的理论与实现,但是由于各种原因,最后自己并没有实现。
考虑到双向路径追踪其实做的是对路径空间的操作,在这个理论框架下体积渲染和表面渲染是可以统一的,所以这个学期我最终考虑实现双向的体积路径追踪,一方面填上BDPT这个坑,另一方面也把学过的体积路径追踪集成进来,不过目前来看,这个目标真的遥遥无期。。。
到现在,如果在中文互联网上搜索“体渲染”这个关键词,自然会搜出一大堆NeRF相关的讲解,然而NeRF所谓的“体渲染”堪称粗糙,在这个语境下作出了大量简化并且散射也直接被忽略了。。。这样自然也就略去了参与介质的很多有趣的光学性质,多少还是很遗憾。这种体渲染能叫体渲染吗(暴论)
Update: 失败啦,没做双向
1. 光传输的算子表述
在初读A Null-Scattering Path Integral Formulation of Light Transport时,这篇论文里的形式化表述非常让我恼火写的什么玩意儿。
现在再来看的时候发现这篇神棍文默认你读过Veach的博士论文,因为太过有名这里就不说叫啥名字了相信能点开这篇文章的人不会一无所知,并且采用了那里面的形式化表述。所以首先得回去阅读这篇开山之作对此有一个完全的理解。
一个小插曲是之前读的BDPT那篇文章其实是Veach的论文的其中一章,不得不佩服上古时代的肝帝,那些年头的人真的是在拿应用数学对待Rendering。
所以为了方便后面的学习,在这里好好学习一下Veach中的算子表述。
1.1. 光线空间
在基于辐射度学的渲染框架中,我们认为Radiance沿着光线传播,最终形成稳定分布的光场,而实际上Radiance(以下用中文统称为辐射度)这个物理量可以被看成是光路的函数。实际上光路还可以承载很多的物理量以及数学量,为了方便表述,我们首先形式化地定义光路的空间。
定义光线空间R=M×S2,其中M为整个场景的表面(这篇文章中尚未考虑到参与介质),S则是球面,这还是非常好理解的,于是一条光线就可以定义为r(x,ω),一个表面的点和一个方向。
除此以外,也可以有其他的表示,例如我们也可以定义这样的路径空间:
R=M2
这样就是用光线的起点和终点来表示。
1.2. 通量测度
接下来我们定义空间R上的测度μ,这被称为通量测度,这个自然就非常直观,就是面积的测度乘上立体角的测度虽然背后的数学严谨性我不清楚,但是谁爱关心谁关心去。
dμ(r)=dA(x)dσx⊥(ω)
于是我们就可以在这个框架下定义辐射度,当然这是辐射度的的定义,但是我们有了一个紧凑的表示
L(r)=dμ(r)dΦ(r)
回想上面说的,光线空间也可以用M2进行表示,在这种情况下,通量测度就定义为
dμ(r(x,y))=V(x,y)∣∣x−y∣∣2cos(θx)cos(θy)dA(x)dA(y)
这也就是我们经常在路径追踪代码中看到的测度转换的细节。
1.3. 光线空间的函数
这个没啥好说的,我们可以在空间R上定义函数,例如辐射度L(r),后面还会引入重要度W(r)。
这些自然还可以定义其范数,我们不会像数学系的那些同学们一样追求各种稀奇古怪的反例,我们只考虑对于某个给定的p范数有界的那些函数构成的函数空间Lp(R)。可以证明我不会,这个空间是完备的(也就是说所有的柯西列都收敛),因此是一个巴拿赫空间。这个函数空间上自然还可以定义内积等等概念。
好吧,我承认上面这些扯淡都是为了强行拔高本文的姿势水平
1.4. 散射与传播算子
函数空间的存在允许我们对于光线定义物理量了,接下来要操作这些物理量自然就要定义函数空间上的算子。
散射算子。定义如下算子K为散射算子:
(Kh)(x,ωo)=∫S2fs(x,ωi→ωo)h(r)dσx⊥(ωi)
当然,Veach的这篇文章里只考虑了表面渲染,因此只需要在同一个点对于不同的立体角积分。
于是这样我们就可以用Lo=KLi这样的式子十分紧凑地表达光的散射。
传播算子。Veach的论文里用了一段来形式化:
首先定义xM(r(x,ω))为沿着x点ω方向出发第一次遇到的表面M方向上的点,当然如果遇不到那就是没定义。原文把这个定义也形式化了但是我觉得真没那个必要。
然后可以定义传播算子G如下:
(Gh)(x,ω)=h(xM(x,ω),−ω)
当然,这个算子也很好理解,就是描述了物理量从一个点传播到另一个点,在体渲染中这个算子还可以把衰减项也考虑进来。
这些定义并不能算是Veach的原创,Arvo在1995年就提出过类似的记号,和Veach的略有不同。Veach说这种定义的好处在于,当fs是对称函数的时候,两个算子都是自伴算子,而在Arvo的定义中没有这种好性质,因此他不得不引入另一个算子H,考虑H以后就是等价的的定义了。
算子自伴意味着如果我们把光路逆过来也会得到相同的物理量。
在上述两个算子的基础上就可以定义传输算子
T=KG
如果把场景中的所有发光体发出的辐射度定义为函数Le,我们熟悉的表面渲染的渲染方程就可以在上述记号下简洁地写为算子方程
L=Le+TL
更多分析学的讨论这里就不继续了,这个方程并不总是有解的,我们只需要考虑有解的情形就行——实际上,对于物理世界里正常的场景,都是有解的,这个时候我们可以定义其解算子S为
S=(I−T)−1
于是就有L=SLe,L被称为平衡辐射度函数。
1.5. 传感器和测量方程
我们输出图片的方式自然是像素,但是拿像素来形式化显然不够本质,毕竟我们可以用不同的形式来表述一张图片。如果使用光线追踪/光栅化等等方法,那么我们自然可以把图像表示为一个颜色序列表示每个像素的颜色。而如果我们使用有限元,那么输出图片就是一个系数组合表示每个基函数对应的系数。当然,我们渲染出来的东西甚至也可以不是颜色。
更一般的,对于线性的传感器,我们可以定义“响应函数”如下
We(x,ω)=dΦ(x,ω)dS(x,ω)
S是传感器观测到的物理量,在图形学中,We被称为重要度函数,We大多数时候都是0,因为我们考虑的是正常的相机,大部分时候只关注一个平面上收到的辐射通量。这里的We考虑了你可能在相机上玩的所有花活儿——透镜系统,滤波器等等都在这个框架之下。
如何描述一张图片呢?这就是测量方程:
I(x,ω)=∫RWe(r)Li(r)dμ(r)
也可以通过点积紧凑地写为
I=<We,Li>
这是一般性的方程,对整个光线空间积分,实际上因为很多情况下我们只关注胶片上的图片长什么样子,只需要算出胶片上的I即可。
Li和L的关系可以通过传输算子联系起来(让辐射度传输到相机上),于是我们可以把测量方程写为
I=<We,Li>=<We,GL>=<We,GSLe>
1.6. 通过伴随算子的重要度传输
回忆一下,对于一个算子H,我们可以定义其伴随算子H∗为
<H∗f,g>=<f,Hg>
使用伴随算子的记号,又可以把测量方程写为
I=<(GS)∗We,Le>
当然,实际使用我们还是得把算子展开,这也就需要把G和K的伴随算子写出来,G在任何场景下都是自伴的,但是K仅在场景中BSDF均对称时才是自伴的,我们首先定义伴随的BSDF。
fs∗(x,ωo→ωi)=fs(x,ωi→ωo)
然后我们可以定义K的伴随算子
(K∗h)(x,ωo)=∫S2fs∗(x,ωi→ωo)h(x,ωi)dσx⊥(ωi)
然后论文里很不负责任地假设BSDF都是对称的。。。于是K就是自伴的了。
在这种情况下可以证明GS也是自伴算子,我们可以把测量方程里的伴随去掉,于是就有
I=<(GS)∗We,Le>=<GSWe,Le>
所以前面的讨论约等于放了一发空炮,但是这也是一个非常重要的提醒:一定要小心场景中的不对称性,对于不对称的BSDF要使用其伴随。这种情况并不罕见,实际上如果稍微写过一点儿表面材料就知道透明绝缘介质的折射会导致这种情况。
由于GS是自伴的(在我们美好的假设里),这个时候优雅的对称性就允许我们跟光传输一样一样地写出重要度传输,也就是。
W=We+TW
W=SWe
这也就是为什么很多时候我们从相机出发计算路径追踪也是对的。
不过K不自伴也不是什么难事,将K替换为K∗定义TW然后把上面的T替换为TW即可。
最后的最后,我们重新回顾四个重要的物理量,入射辐射度Li,出射辐射度Lo,入射重要度Wi,出射重要度We,这四个量之间的关系可以用下面的式子表示:
Li=GLo
Wi=GWo
Lo=KLi
Wo=K∗Wi
对于整个场景,大多数时候我们最初始能拿到的条件就是We和Le。少数时候用入射的We和Le考虑则更为自然,论文里说这通常是在使用有限元法解渲染方程的时候,不过考虑到本文的目的,这里就不展开了。
扯句闲话就是,这一套理论和物理学与工程学中的许多问题也是相关的,很多问题和光传输也有着类似的性质,这一套框架部分也是从那些领域中来的,甚至可以追溯至上世纪七十年代的中子传输问题。某种意义上可以说图形学的老祖宗还真就是计算物理
2. 体积渲染的算子表述
2.1. Recap: 体积辐射传输
接下来时间快进到2019年null-scattering的论文。体积渲染相比于表面渲染的本质区别在于光线空间可以取遍整个场景,不再局限于表面。
体积辐射传输方程在此就不再叙述了,这些属于基础中的基础,可以参考CSE-272作业笔记。
考虑所谓的“空散射”之后,体积辐射传输方程可以写为
(ω⋅∇)L(x,ω)=−μˉtL(x,ω)+μˉtLmˉ(x,ω)
其中
Lmˉ(x,ω)=μˉt(x)μa(x)Lem(x,ω)+μˉt(x)μs(x)∫S2ρm(x,ω′→ω)L(x,ω′)dω′+μˉt(x)μn(x)L(x,ω)
上式中第一项为发光项,第二项为实散射项,第三项为空散射项。
第一个方程还可以进一步ODE得到
L(x,ω)=∫0zTˉ(x,y)μˉt(y)L(y,ω)dy+Tˉ(x,z)Ls(z,ω)
Ls顾名思义就是散射的辐射度。在本文中,所有由于考虑空散射而造成的“假想”的物理量都有一个上划线,可以据此区分。Tˉ就是考虑空散射后的传输项。
T(x,y)=exp(−τˉ(x,y))=exp(−∫0yμˉt(x−sω)ds)
2.2. 路径空间
你当然需要注意路径空间和光线空间的区别。此外,在本文中,为了方便表达δ分布,还引入了Dirac测度δx。
定义路径空间为$$\mathscr{P}=\bigcup\limits_{k=0}^\infty (\mathscr{A}\cup\mathscr{V}\cup\mathscr{V}_\delta)^k$$
这个非常直观,一共有三种路径节点:在表面上,在参与介质中且改变了传播方向,在参与介质中并且没有改变传播方向。
dxˉ=i∏dxi
这里dxˉ表示路径的测度,xi表示路径上的第i个节点,dxi则需要根据节点类型进行不同处理。
这里唯一较为特殊的地方大概是Vδ中的节点,其测度定义为
dxi=dδxi−↔xi+(xi)
这很好理解,Dirac测度意味着我们把积分限制在了这一个方向上。
接下来,为了统一介质与表面,我们引入一般化的函数,这些函数根据点的不同类型取不同的值,有了一致的函数表达以后就可以使用算子表述一统天下了。
定义:
G(x,y)=∣∣x−y∣∣2D(x,ωxy)V(x,y)D(y,ωyx)
其中对于表面上的点D(x,ω)=∣n(x)⋅ω∣而对于体积上的点D(x,ω)=1。
这个G函数说白了就是用来将立体角测度转化为节点测度的。
对于表面上的点,$$L_e(\bold{x},\omega)$$就是发光的辐射度,而对于参与介质上的点,这个值还要乘上一个吸收系数μa(x),不过我好奇介质自己发光自己也会吸收么。。。
然后是一般性的散射函数(这个名字我自己起的)ρ,对于表面上的点,就是BSDF,对于参与介质上的点,就是Phase Function乘上散射系数(乘散射系数这真的是个写VPT特别容易翻车的点),而对于Vδ上的点,ρ的值是μn(x)H(ω⋅ω′),H是阶跃函数。
使用上面的表达就可以改写体积渲染方程:
L(x,ω)=∫0zTˉ(x,y)Lo(y,ω)dy+Tˉ(x,z)Lo(z,ω)
Lo(x,ω)=Le(x,ω)+∫S2ρ(x,ω′→ω)L(x,ω′)D(x,ω′)dω′+μn(x)L(x,ω)
2.3. 算子表述
这就是和1.类似的手法了,定义如下四个算子
(Rmh)(x,ω)=∫S2∫0zρ(ω,x,ω′)T(x,y)D(x,ω′)h(y,ω′)dydω′
(Rsh)(x,ω)=∫S2ρ(ω,x,ω′)T(x,z)D(x,ω′)h(z,ω′)dω′
(Nmh)(x,ω)=μn(x)∫0zT(x,y)h(y,ω)dy
(Nsh)(x,ω)=μn(x)Tˉ(x,z)h(z,ω)
于是可以定义$\mathbf{R}=\mathbf{R_m}+\mathbf{R_s},\mathbf{N}=\mathbf{N_m}+\mathbf{N_s} $
R表示“实”,而N表示“空”。
这样一来,把前面两个渲染方程中的L干掉,留下Lo,其积分方程就写成如下紧凑的算子形式
Lo=Le+(R+N)Lo
同样定义测量函数,在本文中,就默认使用像素表达了。