基于GPU预计算的大气层光效渲染

  • 前言
  • 大气物理模型
  • 渲染方程及其实现
  • 实验结果
  • 参考文献

前言

本文叙述基于物理模型的大气层光效渲染,不仅考虑单重散射,而且也尝试实现多重散射的效果。主要参考论文为Eric Bruneton和Fabrice Neyret的《Precomputed Atmospheric Scattering》。

大气物理模型

1、大气散射现象

大气散射是指,太阳光在射入大气层时,与大气中的空气分子或空气溶胶等发生相互作用,使得入射的光能以一定的规律在各个方向上进行重新分布的现象。太阳光在射入大气层时,遇到大气分子、尘埃、雨滴等颗粒后,都会发生散射现象。其中一部分的光能会被这些粒子吸收转化为热能,而另一部分光能则会以该粒子为中心,向四面八方扩散开来。所以,在经过了大气的散射作用之后,有部分太阳光将无法抵达地球表面。大气散射在自然界中是一种十分重要而又普遍存在的物理现象,人们平时用肉眼观察到的光很大一部分都是散射光。如果没有大气散射,那么只要不是太阳光直接照射到的位置,都将是完全黑暗的。

2、空气物理模型

空气中的介质颗粒根据其直径大小的不同可分为两种:直径远小于光线波长的空气微粒、与直径与光线波长相当的空气溶胶。由前者引起的散射我们称为Rayleigh散射,它是导致晴朗天空呈现蓝色的主要原因。由后者引起的散射我们称为Mie散射,它是导致阴霾的天空呈现灰色的原因,因为阴天的空气中存在大量与光波直径相当的水滴。

①Rayleigh散射:由空气中远小于波长的微粒(如空气分子)引起的散射称作瑞利散射。Rayleigh散射强度与光线波长的四次方成反比,这意味着白光中波长较短的颜色光(蓝色)会比波长较长的光(红色)有更强的散射强度,导致天空在白天偏向蓝色,而在黄昏偏向橙红色。 当日出或日落的时候,由于太阳的位置接近地平线,阳光斜射入大气,会在大气层中穿过很长的距离。在这个过程中,太阳光中的蓝色光几乎都会被散射殆尽无法抵达人眼,只剩下了波长较长的红色光,所以在太阳及其周围的天空都会呈现橘红色。

Rayleigh散射的散射系数可以使用如下公式计算:

Android GpuImage效果预览_github

其中Android GpuImage效果预览_缩放_02是视线与太阳光线的夹角,Android GpuImage效果预览_大气层渲染_03是大气分子密度,Android GpuImage效果预览_大气层渲染_04是大气的折射率,Android GpuImage效果预览_大气层渲染_05是入射光的波长,Android GpuImage效果预览_pbr_06是单位化的相位函数。由上可知,Rayleigh散射明显与波长的四次方成反比,在实现中我们可用一个RGB向量来表示,散射系数可表示为:

Android GpuImage效果预览_缩放_07

由于Rayleigh散射几乎是各向同性的,即光线会被粒子向各个方向均匀散射,其相位函数可以表示为:

Android GpuImage效果预览_大气层渲染_08

相位函数描述了散射的方向特征,也就是在视线与光线夹角为Android GpuImage效果预览_缩放_02的情况,在总共散射的光线中有多少被散射到视线方向上,可以理解为概率或者比例。

② Mie散射:在空气中直径与波长相当的微粒(如尘埃、雾滴等)所导致的散射现象称作Mie散射。与Rayleigh散射不同,Mie散射与波长无关,散射方向表现出明显的各向异性,光线会被粒子更多的向后方散射。而当阴雨天气时,空气中存在大量的水滴颗粒,Mie散射导致天空呈现灰白色。现今经常出现的雾霆天气,同样是因为空气中悬浮的大颗粒过多而导致的Mie散射现象。

由于Mie散射与波长无关,故可以用标量表示,Mie散射系数为:

Android GpuImage效果预览_计算机图形学_10

Mie散射的方向是各向异性的,光线会被更多的向后方散射,其相位函数为:

Android GpuImage效果预览_缩放_11

在公式(5)中,Android GpuImage效果预览_缩放_02是光线方向与视线方向的夹角,而Android GpuImage效果预览_计算机图形学_13表示散射的对称性。若Android GpuImage效果预览_计算机图形学_13是正值,则大多数光线会被粒子向后方散射;若Android GpuImage效果预览_计算机图形学_13是负值,则更多的光线会被向前方散射。通常,g取值[-0.75,0.99]。

Android GpuImage效果预览_pbr_16

③ 大气密度:对于瑞利散射和米氏散射,它们对太阳光的散射作用都和空气粒子的密度有关。许多大气模型都假设摄像机总是在地面上或者是在十分接近地面的位置,这样就可以认为空气具有一个恒定的粒子密度,这就在很大程度上简化了Nishita在1993年提出的散射积分方程,并在近地空间可以得到很好的渲染效果。然而在远离地表的高空,这种做法得到的渲染结果并不准确。

实际中的大气密度在地球引力的作用下,越靠近地表空气密度越高,越远离地表空气越稀薄。所以,我们假定空气粒子的密度是沿着海拔高度h呈指数递减的:

Android GpuImage效果预览_github_17 其中Android GpuImage效果预览_计算机图形学_18是在海平面的空气密度 (6)

Android GpuImage效果预览_github_19为当前采样点的海拔高度,Android GpuImage效果预览_缩放_20是缩放高度(在实现中可设为大气层高度)。理论上说大气层并没有确定的高度,但在实现中我们需要一个统一高度来渲染天空弯顶,这样空气密度随着高度的增加而呈指数递减。对于Rayleigh散射与Mie散射我们分别使用不同的缩放高度:Android GpuImage效果预览_缩放_21Android GpuImage效果预览_大气层渲染_22。这是因为影响Mie散射的大颗粒(尘埃、水滴等)更多的存在于近地表的对流层中,再往上Mie散射效果不明显,但Rayleigh散射的作用依然存在。

3、光线内散射

太阳光在大气中传输的时候会与空气中的微粒产生交互作用。有两种重要的交互方式:散射,它改变了光线的方向;吸收,它将光能吸收并转变为其它形态的能量(如热能)。而散射效果对场景中物体的影响又分为两个方面:一方面是一部分由物体反射的光被散射到视线之外,并不能到达摄像机,因而被衰减,称作外散射;另一方面是一部分太阳光被空气中的粒子散射正对向摄像机,这些正朝向视线的散射被称作内散射。

Android GpuImage效果预览_github_23

最后抵达视点被人眼所观察到的光线可分为两部分:衰减后的物体反射辐射度、被内散射的大气散射辐照度。

Android GpuImage效果预览_缩放_24

其中Android GpuImage效果预览_计算机图形学_25为最终抵达摄像机的总光强,Android GpuImage效果预览_缩放_26为物体的反射光(当视线不与物体相交时则为Android GpuImage效果预览_pbr_27),Android GpuImage效果预览_计算机图形学_28为从O到C点路径上所有内散射光线的总和,这里暂时忽略太阳直射。

Android GpuImage效果预览_pbr_29

公式(7)中的Android GpuImage效果预览_pbr_30是光线从O点到C点的衰减系数,其中Android GpuImage效果预览_pbr_31被称作光学深度(Optical Length),它是散射系数与密度乘积在整条路径上的积分。

① 光学深度:在上图中,大气层内有一点Android GpuImage效果预览_大气层渲染_32,它在视线Android GpuImage效果预览_缩放_33上。太阳光线照向地球,在穿过大气层的时候会受空气分子和空气溶胶的散射作用而发生衰减(外散射的影响),最终到达Android GpuImage效果预览_大气层渲染_32点处的光能总量为:

Android GpuImage效果预览_计算机图形学_35

其中Android GpuImage效果预览_pbr_36是太阳光到达大气层前的初始辐射度。上图中Android GpuImage效果预览_缩放_37点是光线到达Android GpuImage效果预览_大气层渲染_32点之前与大气层的交点,则Android GpuImage效果预览_缩放_39被称作Android GpuImage效果预览_缩放_37点到Android GpuImage效果预览_大气层渲染_32点的光学深度(Optical Depth),它本质上就是Android GpuImage效果预览_缩放_37点到Android GpuImage效果预览_大气层渲染_32点这条路径上散射系数乘上空气密度的积分(包含Rayleigh散射与Mie散射):

Android GpuImage效果预览_计算机图形学_44

公式(9)中的参数前面都已提到过:Android GpuImage效果预览_大气层渲染_45即Rayleigh散射系数,Android GpuImage效果预览_大气层渲染_46是Mie散射系数,而形如Android GpuImage效果预览_github_47的则分别是Rayleigh散射粒子密度分布函数、Mie散射粒子密度分布函数。在这里我们散射系数当作一个在海平面上的常数值,则式(9)可变为如下形式:

Android GpuImage效果预览_pbr_48

所以我们只需对AP路径上的空气密度进行积分,这个积分值被称光学长度(Optical Length),直观的意义就是在光线照射的路径上空气粒子的总量。

② 散射系数:散射系数决定了散射介质对光线的散射的强弱程度,也反应了光线在通过该介质时的衰减程度。我们已经在前面提到,Rayleigh散射对不同波长的光线散射强度不同,在实现中我们可以将其在海平面处的散射系数设为一个三维向量:

Android GpuImage效果预览_缩放_07

而Mie散射对波长的变化影响不明显,所以可以将其在海平面上的散射系数设为标量:

Android GpuImage效果预览_计算机图形学_10

③ 相位函数:己知入射光能和介质的散射系数,我们就可以计算出有多少光线会被介质散射出去。但并非所有的光线在散射之后都会朝向摄像机,有一部分会被散射到其它方向,无法被肉眼所观察到(称作外散射)。所以为了计算内散射的光线量,还需要有另外一个因子描述这个物理量。而相位函数Android GpuImage效果预览_github_51则描述了在该点有多少光线散射之后朝向摄像机,其中的参数Android GpuImage效果预览_缩放_02是太阳光到点Android GpuImage效果预览_大气层渲染_32的向量Android GpuImage效果预览_计算机图形学_54与点Android GpuImage效果预览_大气层渲染_32到摄像机位置的向量Android GpuImage效果预览_github_56的夹角,如下图所示。

Android GpuImage效果预览_缩放_57

相位函数是标准化的,函数本身在所有方向的积分为Android GpuImage效果预览_缩放_58。Rayleigh散射特点是各向同性,光线会以介质粒子为中心均匀地向各个方向散射,其相位函数是前面提到的公式(3)。而Mie散射呈现明显的各向异性,光线会被更多的介质粒子向后方散射,其相位函数是前面提到的公式(5)。

④ 单重散射:目前我们讨论的都是单重散射,即太阳光在到达视点之前只会进行一次散射。点Android GpuImage效果预览_大气层渲染_32的内散射光在达到视点前还会受到空气颗粒影响而衰减,衰减程度取决于点Android GpuImage效果预览_大气层渲染_32到点Android GpuImage效果预览_大气层渲染_61(视点)的光学深度Android GpuImage效果预览_pbr_62,因而衰减因子为Android GpuImage效果预览_大气层渲染_63

所以最终达到视点C的内散射方程如下:

Android GpuImage效果预览_github_64(10)

上式中有两个衰减因子,一个是从Android GpuImage效果预览_缩放_37Android GpuImage效果预览_大气层渲染_32的衰减因子,一个是从Android GpuImage效果预览_大气层渲染_32Android GpuImage效果预览_大气层渲染_61的衰减因子。整个积分路径是从Android GpuImage效果预览_大气层渲染_69Android GpuImage效果预览_大气层渲染_61,这一方程描述了从Android GpuImage效果预览_大气层渲染_69Android GpuImage效果预览_大气层渲染_61路径上全部内散射光的总和。

Android GpuImage效果预览_缩放_73

内散射积分公式(10)中,在积分路径OC上太阳光与视线的夹角Android GpuImage效果预览_缩放_02保持不变,因此有必要将相位函数Android GpuImage效果预览_github_51从积分内部中提取出来。而太阳光是平行光,Android GpuImage效果预览_pbr_36是大阳光在大气层顶层的辐射度,视为常量,也可从积分内部提取出来。散射系数亦如此。故公式(10)可变为如下:

Android GpuImage效果预览_计算机图形学_77

故要计算一个视点到物体之间的内射光线,我们需要对视线路径上每一点的衰减因子以及空气密度进行积分。

⑤ 多重散射:光线在传输过程中被空气中的一个粒子影响,称为光的一次散射。当空气中大颗粒较多时,被粒子散射的光又会被散射方向上的其它粒子再次散射,这个过程称为多重散射(Multiple Scattering)。在晴朗干净的天空中,由于空气中大粒子的数量较少,多重散射的作用不是很明显。而在空气浑浊或黄昏时,多重散射会对场景的真实性产生较明显的影响。

我们前面的讨论都是单一散射模型。这一模型在白天的时候比较合理,这一假设在白天的时候比较合理,因为在白天的时候太阳光强度较高,多重散射作用不明显;而在傍晚的时候,由于太阳直射光强度变弱,多重散射对场景的影响会变得更加重要,在渲染真实图像中必须加以考虑。即便如此,单一散射模型在此时依旧可以提供一个相对较好的结果。

关于多重散射的文献资料较少,因为单重散射模型目前已经有了不错的渲染结果。在我阅读的这篇论文《Precomputed Atmospheric Scattering》中考虑了多重散射的情况,较为复杂,在后面论述。

⑥ 体积光:当光线照射到遮挡物时,一部分光线会从物体的边缘和空隙中穿过,并产生很明显的光柱效果,在视觉上给人以很强的体积感,所以称之为体积光(Light shaft)。体积光在自然界中是十分常见的现象,如太阳光从云隙中透过时产生的云隙光,森林中阳光从树叶中穿过产生的光柱。体积光现象有时又被称作“丁达尔效应”。其理论基础同样是光线的散射原理,可以使用前面描述的Mie散射理论来解释。对于溶胶,其粒子大小通常与可见光的波长相当,所以在光线穿过气溶胶时,会发生明显的Mie散射现象,产生肉眼可观察到的光柱体。

Android GpuImage效果预览_缩放_78

渲染方程及其实现

为了便于论述,我们记Android GpuImage效果预览_pbr_79为视点Android GpuImage效果预览_缩放_80从方向Android GpuImage效果预览_计算机图形学_81接收的总的辐射度,其中Android GpuImage效果预览_缩放_82是太阳方向向量。记Android GpuImage效果预览_缩放_83为视线Android GpuImage效果预览_计算机图形学_81的终点(通常为地面、物体或大气顶层)。Android GpuImage效果预览_缩放_80Android GpuImage效果预览_计算机图形学_86之间的衰减因子Android GpuImage效果预览_缩放_87Android GpuImage效果预览_计算机图形学_86处的反射辐射度Android GpuImage效果预览_github_89、在某一点Android GpuImage效果预览_pbr_90Android GpuImage效果预览_github_91内散射的辐射度Android GpuImage效果预览_pbr_92定义如下:

Android GpuImage效果预览_大气层渲染_93

Android GpuImage效果预览_缩放_94

Android GpuImage效果预览_缩放_95

Android GpuImage效果预览_大气层渲染_96

公式(12)、(13)、(14)对应上图的(a)、(b)、(c)。有了以上的函数表示,现在我们可以定义渲染方程了。

1、渲染方程

Android GpuImage效果预览_缩放_97

Android GpuImage效果预览_缩放_98

Android GpuImage效果预览_计算机图形学_99

Android GpuImage效果预览_github_100

Android GpuImage效果预览_pbr_79为视点Android GpuImage效果预览_缩放_80从方向Android GpuImage效果预览_计算机图形学_81接收的总的辐射度。Android GpuImage效果预览_大气层渲染_104是到达Android GpuImage效果预览_缩放_80的太阳直射光,因此当视线Android GpuImage效果预览_计算机图形学_81与太阳方向向量Android GpuImage效果预览_缩放_82不相等时Android GpuImage效果预览_大气层渲染_104为0(又或者太阳被遮挡了)。Android GpuImage效果预览_大气层渲染_109是在点Android GpuImage效果预览_计算机图形学_86收到的反射的辐射度。Android GpuImage效果预览_计算机图形学_111则是从Android GpuImage效果预览_计算机图形学_86Android GpuImage效果预览_缩放_80路径上接收的内散射光。从渲染方程可以看出,衰减因子Android GpuImage效果预览_缩放_87无处不在,这是因为在大气层内,涉及到光线的传播都要考虑外散射以及光线被吸收的影响。

这个渲染方程计算量非常大,尤其是公式(18),一重积分内部还嵌套了两重积分。纯粹地暴力计算对于实时渲染来说几乎不可能。为了能够实现实时渲染大气层,不少论文提出了查找表的优化思想,这是一种基于预先计算的优化方法。但大多数的论文都只是考虑了单重散射,我阅读的这篇论文《Precomputed Atmospheric Scattering》将多重散射也考虑进去了,提出了一种4维查找表的方法,在后面论述。除此之外,渲染方程也设计到大量的积分计算。为此,我们采用梯形法则和光线步进(Ray Marching)来快速计算数值积分。

下面的叙述部分,由于代码比较繁多,我尽量用伪代码描述。

2、光线衰减因子

前面已经提到过,从Android GpuImage效果预览_缩放_80Android GpuImage效果预览_计算机图形学_86光线衰减因子如下(实际计算中把散射系数提出积分外):

Android GpuImage效果预览_github_117

每一帧去计算它并不现实,因此早在1994年就有人提出了查找表的优化方法。如下图所示,假设我们要计算Android GpuImage效果预览_pbr_118Android GpuImage效果预览_计算机图形学_119的衰减因子。Android GpuImage效果预览_大气层渲染_120Android GpuImage效果预览_pbr_118点沿视线与大气顶层的交点。则有:Android GpuImage效果预览_pbr_118Android GpuImage效果预览_大气层渲染_120的衰减因子=Android GpuImage效果预览_pbr_118Android GpuImage效果预览_计算机图形学_119的衰减因子乘上q到i的衰减因子(这里相乘的原因是决定衰减因子的光学深度是在其公式的指数位置上,衰减因子相乘等于相应的指数相加)。那么Android GpuImage效果预览_pbr_118Android GpuImage效果预览_计算机图形学_119的衰减因子=Android GpuImage效果预览_pbr_118Android GpuImage效果预览_大气层渲染_120的衰减因子除以Android GpuImage效果预览_计算机图形学_119Android GpuImage效果预览_大气层渲染_120的衰减因子。因此只要知道点到大气顶层的衰减因子,就可计算任两点之间的光线衰减因子。

Android GpuImage效果预览_计算机图形学_132

此外,O’ Neil发现了衰减因子的计算取决于两个参数:当前点的高度Android GpuImage效果预览_缩放_133和视线的天顶角Android GpuImage效果预览_缩放_02。也就是说我们可以通过预先计算(Android GpuImage效果预览_缩放_133,Android GpuImage效果预览_缩放_02)的全部组合决定的衰减因子存放到一张纹理中,后面的实时计算直接根据需要计算的(Android GpuImage效果预览_缩放_133,Android GpuImage效果预览_缩放_02)查找这张纹理。为了方便,我们取参数(Android GpuImage效果预览_缩放_133,Android GpuImage效果预览_pbr_140),记Android GpuImage效果预览_github_141

① 点p到大气顶层的距离:即计算向量Android GpuImage效果预览_计算机图形学_142的长度。建立如图所示的坐标系,点Android GpuImage效果预览_大气层渲染_69为地心,则向量Android GpuImage效果预览_计算机图形学_142距离点Android GpuImage效果预览_pbr_118Android GpuImage效果预览_缩放_146的一点坐标(Android GpuImage效果预览_缩放_80,Android GpuImage效果预览_pbr_148)为:(Android GpuImage效果预览_缩放_149,Android GpuImage效果预览_github_150)

那么设距离Android GpuImage效果预览_缩放_146为向量Android GpuImage效果预览_计算机图形学_142的长度,则(Android GpuImage效果预览_缩放_80,Android GpuImage效果预览_pbr_148)即为点Android GpuImage效果预览_大气层渲染_120的坐标。已知大气层半径为Android GpuImage效果预览_缩放_156,则由勾股定理有:Android GpuImage效果预览_大气层渲染_157,整理后即为二元一次方程:Android GpuImage效果预览_计算机图形学_158,其中Android GpuImage效果预览_缩放_133Android GpuImage效果预览_大气层渲染_160Android GpuImage效果预览_缩放_156已知,可求出距离Android GpuImage效果预览_缩放_146。同样可通过该二元一次方程的判别式判断是否有解,从判断射线(Android GpuImage效果预览_缩放_133,Android GpuImage效果预览_大气层渲染_160)是否与大气层(或地表)存在交点。

点p到地球表面交点的距离同理,将Android GpuImage效果预览_缩放_156换成Android GpuImage效果预览_github_166即可。

② 计算点p到i(与大气顶层的交点)的光学长度:计算衰减因子需要计算点Android GpuImage效果预览_pbr_118Android GpuImage效果预览_大气层渲染_120的光学深度,也就是对Android GpuImage效果预览_pbr_118Android GpuImage效果预览_大气层渲染_120的散射系数和空气密度乘积进行积分。其中散射系数(包括Rayleigh散射和Mie散射)系数我们取海平面上相应的散射系数,故我们只需对Android GpuImage效果预览_pbr_118Android GpuImage效果预览_大气层渲染_120路径的空气密度进行积分,这就是光学长度–Android GpuImage效果预览_大气层渲染_173

计算积分我们采用梯度法,以光线步进(Ray Marching)循环采样计算累加和。如下图所示,假设我们取Android GpuImage效果预览_缩放_174-Android GpuImage效果预览_大气层渲染_175这五个采样点,依次计算每个点的空气密度乘上积分步长,累加计算。

Android GpuImage效果预览_大气层渲染_176

Android GpuImage效果预览_pbr_177

计算Rayleigh光学长度和Mie光学长度均采用以上的方法计算。分别采用以上方法计算之后,再乘上相应的散射系数,就是光学深度,然后衰减因子就按照公式(12)计算即可。

③ 坐标映射:我们把预计算的结果存入一张2D的纹理中,所以需要将(Android GpuImage效果预览_缩放_133,Android GpuImage效果预览_大气层渲染_160)映射到纹理坐标(Android GpuImage效果预览_pbr_180,Android GpuImage效果预览_pbr_181)中。我们知道纹理坐标数值范围是Android GpuImage效果预览_缩放_182,故对于一个数值Android GpuImage效果预览_缩放_80,我们首先要将Android GpuImage效果预览_缩放_80映射到Android GpuImage效果预览_缩放_182,设Android GpuImage效果预览_缩放_80的值域为Android GpuImage效果预览_缩放_187。则令Android GpuImage效果预览_github_188,可将其映射到Android GpuImage效果预览_缩放_182

然而值得注意的是,将Android GpuImage效果预览_缩放_80映射到Android GpuImage效果预览_缩放_182之后,边界部分我们应该要去掉。这是因为我们在对纹理进行查找时需要线性插值,边界部分会产生一些外推值。为了避免这种情况,我们进一步令Android GpuImage效果预览_缩放_80(此时Android GpuImage效果预览_缩放_80已属于Android GpuImage效果预览_缩放_182):

Android GpuImage效果预览_大气层渲染_195,其中Android GpuImage效果预览_大气层渲染_04是纹理的大小,Android GpuImage效果预览_大气层渲染_197就是一个纹素的大小。如此我们将Android GpuImage效果预览_缩放_80Android GpuImage效果预览_缩放_182映射到了Android GpuImage效果预览_pbr_200上,去掉了边界部分。

接下来我们要将Android GpuImage效果预览_缩放_133映射到Android GpuImage效果预览_大气层渲染_160,而Android GpuImage效果预览_大气层渲染_160映射到Android GpuImage效果预览_计算机图形学_81

对于Android GpuImage效果预览_缩放_133,它代表当前点到地心的距离,显然其值域为Android GpuImage效果预览_大气层渲染_206。然而为了更高的精度(避免r接近地表时失真),我们采用了一个非线性映射的方式。如下图所示,实际上对于每个不同Android GpuImage效果预览_缩放_133,都对应着一个不同的Android GpuImage效果预览_缩放_208,它是视点Android GpuImage效果预览_pbr_118到过视点的与地表相切的切线的切点的距离,Android GpuImage效果预览_缩放_208的最大值则是如下图中的Android GpuImage效果预览_缩放_20(最小值为Android GpuImage效果预览_pbr_27)。故对于Android GpuImage效果预览_缩放_133我们采用该映射方式映射到Android GpuImage效果预览_pbr_180Android GpuImage效果预览_大气层渲染_215

对于天顶角Android GpuImage效果预览_大气层渲染_160,每个特定的天顶角,都对应着不同的距离Android GpuImage效果预览_缩放_146(视点到大气顶层交点的距离)。Android GpuImage效果预览_缩放_146的下界为Android GpuImage效果预览_计算机图形学_219,上界为为Android GpuImage效果预览_大气层渲染_220。故其映射方式为:Android GpuImage效果预览_大气层渲染_221

Android GpuImage效果预览_缩放_222

Android GpuImage效果预览_缩放_223

至于计算(Android GpuImage效果预览_缩放_208,Android GpuImage效果预览_缩放_20),可以通过两个三角形勾股定理,不再赘述。我们将(Android GpuImage效果预览_缩放_133,Android GpuImage效果预览_大气层渲染_160)映射到2D纹理坐标,同样也需要逆过程,这将在预计算阶段用到。逆过程我们将上面的几个公式反推一下即可,也不再赘述。

④ 点p到太阳的光线衰减因子:我们需要计算点Android GpuImage效果预览_pbr_118到太阳的光线衰减因子。太阳不是一个点光源,而是一个圆盘发光体。因此Android GpuImage效果预览_pbr_118到太阳的光线衰减因子,是以太阳圆盘为区域的衰减因子的积分。在这里我们把太阳圆盘区域上的衰减因子视作相同的常量。故该值等于衰减因子乘上太阳圆盘在水平线上部分占整个圆盘的比例。

设过视点p与地表相切的切线为l。当太阳天顶角Android GpuImage效果预览_计算机图形学_230大于切线l的天顶角Android GpuImage效果预览_缩放_231+太阳的角半径Android GpuImage效果预览_pbr_232时,这部分比例为Android GpuImage效果预览_pbr_27;当Android GpuImage效果预览_计算机图形学_230小于Android GpuImage效果预览_pbr_235时为Android GpuImage效果预览_缩放_58。故我们可以用相应的余弦值来定性地衡量这一比例(注意余弦函数在Android GpuImage效果预览_github_237递减)。

Android GpuImage效果预览_计算机图形学_238时,为Android GpuImage效果预览_pbr_27;(约等符号是因为Android GpuImage效果预览_计算机图形学_240)

Android GpuImage效果预览_计算机图形学_241时,为Android GpuImage效果预览_缩放_58

中间部分则用埃尔米特(Hermite)插值,可直接用GLSL的smoothstep函数。

Android GpuImage效果预览_计算机图形学_243

3、单重散射

单重散射是指光线在到达视点之前只发生了一次散射。接下来将叙述如何计算单重散射,如何将其映射到3D纹理上。如下图,Android GpuImage效果预览_大气层渲染_160是视点Android GpuImage效果预览_pbr_118处实现的天顶角的Android GpuImage效果预览_计算机图形学_246值,假设太阳到达Android GpuImage效果预览_计算机图形学_119点发生了散射,Android GpuImage效果预览_大气层渲染_248的距离为Android GpuImage效果预览_缩放_146Android GpuImage效果预览_大气层渲染_250是太阳光方向向量在Android GpuImage效果预览_pbr_118处的天顶角Android GpuImage效果预览_计算机图形学_246值,Android GpuImage效果预览_计算机图形学_253是太阳光方向向量,Android GpuImage效果预览_计算机图形学_81是太阳光方向向量与视线Android GpuImage效果预览_大气层渲染_248夹角的Android GpuImage效果预览_计算机图形学_246值,Android GpuImage效果预览_大气层渲染_257是太阳光方向向量在Android GpuImage效果预览_计算机图形学_119处的天顶角Android GpuImage效果预览_计算机图形学_246值。Android GpuImage效果预览_缩放_133是点Android GpuImage效果预览_pbr_118到地心的距离,Android GpuImage效果预览_大气层渲染_262是点Android GpuImage效果预览_计算机图形学_119到地心的距离。

Android GpuImage效果预览_github_264

到达p点的内散射辐射度为:

Android GpuImage效果预览_pbr_265

其中的Android GpuImage效果预览_pbr_36和两个相位函数我们先不管,计算内散射辐射度我们需要多p到大气顶层交点之间对光线衰减因子和空气密度进行积分。以上图积分点Android GpuImage效果预览_计算机图形学_119为例,我们需要Android GpuImage效果预览_计算机图形学_119到太阳的光线衰减因子、Android GpuImage效果预览_pbr_118Android GpuImage效果预览_计算机图形学_119的光线衰减因子,而这两个值可直接借助查找我们前面已经计算好的纹理获得。故对一个积分采样点,其积分函数值计算的伪代码如下。

Android GpuImage效果预览_pbr_271

① 内散射积分:同样地,我们采用梯度法和光线步进法进行积分。积分路径的终端实际上不一定是大气顶层,有可能是地表,但积分过程都是一样。

Android GpuImage效果预览_缩放_272

② 相位函数:对于Rayleigh相位函数和Mie相位函数,直接分别套用公式(3)和公式(5)。

Android GpuImage效果预览_计算机图形学_273

Android GpuImage效果预览_pbr_274

③ 坐标映射:计算单重散射积分同样非常耗费性能。因此我们一样使用预计算查找表的方法计算单重散射积分。与光线衰减因子不同的是,单重散射积分取决于四个参数,就是前面提到的(Android GpuImage效果预览_缩放_133,Android GpuImage效果预览_大气层渲染_160,Android GpuImage效果预览_大气层渲染_250,Android GpuImage效果预览_计算机图形学_81),这意味着我们需要将这四个参数映射到4D纹理坐标。

对于Android GpuImage效果预览_pbr_279的坐标映射,与前面的提到的映射方法相同,这里不再赘述。

对于Android GpuImage效果预览_计算机图形学_81,其值域为Android GpuImage效果预览_缩放_281,我们做简单的线性映射,令Android GpuImage效果预览_缩放_282

对于Android GpuImage效果预览_大气层渲染_250,通过非线性映射,如下所示(原因不明):

Android GpuImage效果预览_计算机图形学_284Android GpuImage效果预览_github_285Android GpuImage效果预览_pbr_286

而逆过程则直接根据上述公式倒推即可。现在我们把(Android GpuImage效果预览_缩放_133,Android GpuImage效果预览_大气层渲染_160,Android GpuImage效果预览_大气层渲染_250,Android GpuImage效果预览_计算机图形学_81)映射到了4D纹理坐标,然而实际上纹理维度最多3D。故映射到4D之后,我们还要将4D坐标映射到3D坐标。为此,我们可通过取整、取模来实现。

4、多重散射

在考虑多重散射的时候,渲染方程就变为:

Android GpuImage效果预览_缩放_291

其中Android GpuImage效果预览_github_292代表光线散射Android GpuImage效果预览_大气层渲染_120重。事实上,在白天的时候多重散射的效果微乎其微,而在傍晚的时候效果较为明显一点。因此实现多重散射是性价比非常低的事情,计算量比单重散射多很多,但是渲染的提升效果可以说是非常小了。

多重散射的来源有两个:一个是经过Android GpuImage效果预览_缩放_294次散射之后再发生了一次散射,而另一个是从地面的反射的光线。在这里我们先暂时不讨论地面的反射。多重散射可以分解成Android GpuImage效果预览_pbr_295重散射、Android GpuImage效果预览_缩放_296重散射、Android GpuImage效果预览_大气层渲染_297重散射…等等Android GpuImage效果预览_大气层渲染_04重散射的累加和。而且,第Android GpuImage效果预览_大气层渲染_120重散射可以根据第Android GpuImage效果预览_pbr_300重散射计算得到。

先讨论视点Android GpuImage效果预览_pbr_118接收到的第Android GpuImage效果预览_大气层渲染_04重散射,设视点Android GpuImage效果预览_pbr_118沿视线Android GpuImage效果预览_计算机图形学_81的终端为Android GpuImage效果预览_大气层渲染_120Android GpuImage效果预览_计算机图形学_119为路径Android GpuImage效果预览_计算机图形学_142上的任意一点。对于Android GpuImage效果预览_计算机图形学_119点,我们要计算Android GpuImage效果预览_计算机图形学_119点接收的经过Android GpuImage效果预览_计算机图形学_310重散射(第Android GpuImage效果预览_大气层渲染_04重散射时发生内散射,射向视点)的辐射度,这需要对整个球体方向进行积分,是二重积分的计算量。然后我们需要对路径Android GpuImage效果预览_计算机图形学_142上所有的Android GpuImage效果预览_计算机图形学_119点(Android GpuImage效果预览_计算机图形学_119点是Android GpuImage效果预览_计算机图形学_142上的一点)进行积分,是一重积分的计算量。由此我们可以知道,计算第Android GpuImage效果预览_大气层渲染_04重散射,一重积分里面嵌套了两重积分,为三重积分的计算量。如果对于每一重散射的计算,都从头开始的话,这必然导致很大的计算量,而且有不少重复的计算。

为此,对于多重散射,我们采用迭代的方式来一重一重地计算,而且同样采用查找表的优化方法。每计算一重散射,我们把结果存储到纹理中,然后下一重的散射计算就直接查找这个纹理。如此,我们通过迭代的方式避免前一重的散射计算。

然而即便如此,以三重积分的方式计算第n重散射依然存在着不少重复的部分。如下图所示,设Android GpuImage效果预览_大气层渲染_317Android GpuImage效果预览_计算机图形学_119点接收的经过Android GpuImage效果预览_计算机图形学_310重散射最后第Android GpuImage效果预览_大气层渲染_04重散射到Android GpuImage效果预览_大气层渲染_321方向的总的光线辐射度。如果以三重积分计算Android GpuImage效果预览_大气层渲染_04重散射,那么在Android GpuImage效果预览_pbr_118点和Android GpuImage效果预览_计算机图形学_324点都会重复地计算到Android GpuImage效果预览_大气层渲染_317。事实上,对于Android GpuImage效果预览_pbr_118点到Android GpuImage效果预览_计算机图形学_119点之间所有的点,都会重复地计算Android GpuImage效果预览_大气层渲染_317。显然,为了性能考虑,我们必须避免这一重复的部分。以空间换时间是个不错的方法。

Android GpuImage效果预览_计算机图形学_329

最终,对于计算Android GpuImage效果预览_大气层渲染_04重散射我们分两步走:

第一步:对于Android GpuImage效果预览_pbr_118点沿视线Android GpuImage效果预览_pbr_332上的每一个点Android GpuImage效果预览_计算机图形学_119,我们计算Android GpuImage效果预览_计算机图形学_119点接收的经过Android GpuImage效果预览_计算机图形学_310重散射的辐射度,这需要两重积分;

第二步:在Android GpuImage效果预览_pbr_118点沿视线Android GpuImage效果预览_pbr_332的路径上,计算第Android GpuImage效果预览_大气层渲染_04重散射,我们查找第一步计算得到的纹理,这只需单重积分。

① 第一步:计算Android GpuImage效果预览_计算机图形学_119点接收的经过Android GpuImage效果预览_计算机图形学_310重散射的光线(第Android GpuImage效果预览_大气层渲染_04重散射射向Android GpuImage效果预览_大气层渲染_321)。

如下所示,对于所有可能的方向Android GpuImage效果预览_计算机图形学_343,我们需要计算从Android GpuImage效果预览_计算机图形学_343方向接收的入射辐射度Android GpuImage效果预览_github_292Android GpuImage效果预览_github_292由两部分组成:一部分是Android GpuImage效果预览_计算机图形学_310重散射的辐射度(可以直接查找Android GpuImage效果预览_计算机图形学_310重散射的纹理得到);另一部分是当射线Android GpuImage效果预览_计算机图形学_343与地面相交时,我们需要考虑地面的反射辐射度。

Android GpuImage效果预览_计算机图形学_310重散射辐射度由前面的迭代计算得到,不再讨论。我们需要重点讨论的是地面的反射辐射度。设射线(Android GpuImage效果预览_计算机图形学_119,Android GpuImage效果预览_计算机图形学_343)与地面的交点为Android GpuImage效果预览_缩放_133,那么从地面接收的反射辐射度应该是以下几项的乘积:

  • Android GpuImage效果预览_计算机图形学_354和点Android GpuImage效果预览_github_355之间的光线衰减因子;
  • 地面的平均反照率;
  • 地面的Lambertian BRDF函数的Android GpuImage效果预览_计算机图形学_356
  • 地面接收的经过Android GpuImage效果预览_大气层渲染_357次散射辐照度,这是个半球方向的积分,我们将在后面讨论,现在假设我们已经可以计算得到。
  • Android GpuImage效果预览_计算机图形学_358

  • Android GpuImage效果预览_缩放_359

② 第二步:第二步就是利用第一步的计算结果进行单次积分。在Android GpuImage效果预览_pbr_118点沿视线Android GpuImage效果预览_pbr_332的路径上,计算第Android GpuImage效果预览_大气层渲染_04重散射,我们查找第一步计算得到的纹理。对于Android GpuImage效果预览_pbr_118到边界交点的每一个点Android GpuImage效果预览_计算机图形学_119,设Android GpuImage效果预览_计算机图形学_119计算得到的Android GpuImage效果预览_计算机图形学_310重散射密度为Android GpuImage效果预览_大气层渲染_317,则由Android GpuImage效果预览_计算机图形学_119Android GpuImage效果预览_pbr_118的辐射度应该再乘上一个Android GpuImage效果预览_计算机图形学_119Android GpuImage效果预览_pbr_118之间的光线衰减因子。

同样的,我们采用梯度法和光线步进计算Android GpuImage效果预览_缩放_372到边界路径上的黎曼和。

Android GpuImage效果预览_pbr_373

③ 坐标映射:与单重散射一样。

5、地面辐照度

地面接收的辐照度是直接辐照度、单重散射或多重散射之后接收的辐照度总和,我们分为直接辐照度和间接辐照度。计算地面接收的辐照度有以下两个目的:

  • 计算Android GpuImage效果预览_github_374重散射的时候,我们需要考虑从地面反射的辐射度;
  • 渲染地面的需要。

① 直接辐照度:太阳光线直达地面,中间不发生的散射(但是会向外散射导致光强减弱),所以我们将太阳的辐射度乘上地面到大气顶层的光线衰减因子即可。同时,值得注意的是太阳是一个圆盘,我们还需要考虑太阳可见圆盘的比例,这在前面已经讨论过了。比较简单,直接贴代码了。

Android GpuImage效果预览_缩放_375

② 间接辐照度:间接辐照度考虑单重及多重散射,如下所示,我们需要对以地面法线为轴向的半球方向进行积分。

Android GpuImage效果预览_pbr_376

Android GpuImage效果预览_github_377

5、预计算

有了以上的铺垫,我们现在可以将光线衰减因子、单重散射、多重散射以及地面辐照度预先计算到纹理中,然后渲染的时候直接根据相应的参数去查找纹理(需要纹理坐标的映射)从而获取相应的值,如此在渲染时省去了大量的计算,这带来了非常大的性能提升。

Android GpuImage效果预览_github_378

实验结果

演示的是一个非常简单的场景,地球以及地球表面上的球体。由于仅仅只有两个球体,那么绘制轮廓部分用光线追踪的办法是非常简单的,而且在片元着色器也很容易实现,只需求解几个二元一次方程即可。而光照部分则是查找前面已经计算好的散射纹理。

① 实验平台:

  • 操作系统: Windows8.1
  • IDE: Qt Creator
  • 语言: C++
  • API: OpenGL3.3+, Qt 5.7

② 可调参数:

  • 太阳光谱:选择常量值还是真实值(通过真实的太阳光谱线性插值)
  • 臭氧层:是否开启臭氧层(臭氧层也会吸收一部分光线)
  • 散射重数:最低为Android GpuImage效果预览_大气层渲染_379(即只考虑单重散射)
  • 体积光:是否开启丁达尔效应
  • Rayleigh散射:是否开启rayleigh散射
  • Mie散射:是否开启Mie散射
  • Mie散射对称系数:控制Mie散射的方向性,为正则向后散射,为负则向前散射
  • Android GpuImage效果预览_pbr_380

③ 实验结果:

1)、首先,把Rayleigh散射和Mie散射都关闭了,也就是说相当于没有大气层的存在,和月球上的情况相似,所以天空不再是蓝色而是黑色(直接看到外太空了),太阳周围也不会出现光晕。而且由于没有散射,那么阴影部分(非太阳直射的地方)将完全漆黑。

Android GpuImage效果预览_计算机图形学_381

2)、现在把Rayleigh散射和Mie散射都开启。

Android GpuImage效果预览_pbr_382

3)、仅开启Rayleig散射,这时由于没有Mie散射,也就是我们剔除了气溶胶的作用,天空的朦胧感降为Android GpuImage效果预览_github_383,天空看着很清澈,这与我们的生活经验一致。

Android GpuImage效果预览_缩放_384

4)、而如果仅开启Mie散射,那么天空不会呈现蓝色,而是呈现如下情况。可以看出,Mie散射呈现的是一种丁达尔效应的朦胧感。

Android GpuImage效果预览_github_385

5)、单重散射、多重散射的对比。实现的最大难度在于多重散射,需要编写大量的代码,而且占用更多的空间,但是提升的效果其实很小。如下图。

Android GpuImage效果预览_计算机图形学_386

对比上面的几张图,可以看到其实单重散射的效果已经非常不错了。而且散射重数多了其实区别也不大。6)、体积光效果:体积光效果是大气光效渲染比较复杂的一个方面,但是实现的话看起来是很令人震撼的。遗憾的是,论文作者提出的体积光实现是基于阴影体的,简单场景没什么问题,但是比较复杂的就不太现实了。

Android GpuImage效果预览_缩放_387

7)、Mie散射对称系数:控制Mie散射的方向性,为正则向后散射,为负则向前散射。为正时越大向后散射得越多。

Android GpuImage效果预览_计算机图形学_388

8)、调整曝光率可以出现一些有趣的光效。

Android GpuImage效果预览_缩放_389

9)、一些从外太空观察的效果。

Android GpuImage效果预览_计算机图形学_390

此外,值得注意的是,渲染的速度非常快,FPS稳定在Android GpuImage效果预览_缩放_391。基于预先计算的查找表的优化方法把渲染时大量的计算挪到程序启动的初始阶段,而且开始阶段耗费时间也不多,最多两三秒。对于散射重数低于Android GpuImage效果预览_pbr_392的,几乎是秒开。

参考文献

1、《Precomputed Atmospheric Scattering》

2、《SIGGRAPH 2009 - Lighting Research at Bungie》

3、《基于GPU的实时大气散射渲染优化算法研究与实现_方辰》

4、《PreethamSig2003CourseNotes》

5、《数字地球大气散射的GPU实现》

6、《基于GPU的行星大气散射效果实时渲染技术研究_刘维敏》

7、《基于GPU的地球大气散射现象可视化仿真_杜芳》

8、《多重散射的天空光照效果建模与实时绘制_艾祖亮》