全局光照浅析
全局光照在图形学中应该是最重要,最复杂的系统了,里面涉及到大量的数学知识及实现细节。但实际上里面很多理论知识都是很朴素的思想,这篇文章不会过多的讨论数学和实现细节,希望能够从科普的角度把全局光照技术梳理出一条脉络,以后会专门再写几篇文章来讨论全局光照涉及到的数学知识及工程上的实现细节。先来看一张图,让我们了解一下什么是全局光照。实时渲染通常使用光栅化渲染,被光栅化的一个个像素在计算光照的时候,使用的都是光源,比如方向光,点光源,聚光灯。这就好比光源发射的光子碰撞到物体表面,经过一次弹射进入人的眼睛,我们把这种光照现象叫做Direct light。
光被物体遮挡就会产生影子,直接光照被遮挡产生的影子叫做Direct shadow,通常在实时渲染中可以使用shadow map来实现。
在真实的世界中,光子不可能只发生一次弹射,如上图中绿色的书,光子经过二次弹射照亮了墙面,这种光照叫做Indirect light。实时渲染中的Indirect light,一般分为静态和动态两种。对于静态场景我们可以使用很多预计算的方法将Indirect light的信息缓存起来,然后实时渲染的时候利用这些信息还原真实的光照,属于空间换时间的优化。对于动态实时的Indirect light,往往都是通过简化光照方程来近似模拟真实的光照。
同理如果Indirect light被物体遮挡就会产生Indirect shadow,如果是光栅化渲染,在3D空间中做实时的Indirect shadow是非常困难的,所以光栅化渲染中的Indirect shadow往往是在屏幕空间中计算,比如SSAO算法。如果使用实时光线追踪RTX或者SDF,Indirect shadow还是比较容易实现的,不过弹射的次数肯定是被限制的,通常只支持两次弹射。
光线与物体表面发生碰撞,一部分光线会被反射,另一部分光线会被折射。反射又分为两种情况一种是mirror reflections镜面反射,可以理解成光线只会沿着一个方向反射。另一种是diffuse reflections漫反射,光线会沿着四面八方均匀反射。具体使用什么反射是由材质属性决定的,如果使用微表面模型来定义材质表面,那么就是由BRDF(双向反射分布函数)来决定的。现实中的反射大部分情况下并不是mirror reflections也不是diffuse reflections,而是介于两者之间,这种反射叫做glossy reflections,对应的材质也叫做glossy material.
Caustics焦散在图形学中的定义是指光线经过高光物体的反射或折射,然后弹射到漫反射表面,再弹射到眼睛的效果,如上图中书下面阴影中高亮的部分。Caustics很难实现,即便在离线渲染中使用path tracing也很难实现,通常会借助Photon Mapping光子映射技术来实现Caustics。
Subsurface scattering次表面散射也叫sss技术,指的是光线射入透明物体,在物体内部发生了多次折射,最终穿出物体的效果,在实时渲染中应用广泛,比如皮肤渲染。
所谓的全局光照就是实现上面全部效果所需的渲染技术。每一种光照效果都需要特定的图形学算法,通常我们会在项目中选择组合多种算法来实现最终的效果。
不管使用什么算法,如果是基于物理的光照算法都要遵守以下两条准则:
[*]光线是沿直线传播的
[*]遵守能量守恒定律
一. 离线渲染算法简介
Whitted-Style Ray Tracing
Whitted-Style Ray Tracing
算法简介
[*]从视点发射光线。
[*]光线碰撞到物体表面,如果表面材质是diffuse材质则光线停止传播,否则沿着反射方向继续传播。
[*]每个碰撞点都需要发射一条shadow ray做遮挡查询。
[*]shading point需要累加所有碰撞点的Irradiance,作为最终的光照结果。
效果展示
Whitted-Style Ray Tracing
算法评价
Whitted-Style Ray Tracing算法损失了两种重要的全局光照效果,Glossy reflection和Color bleeding。
Whitted-Style Ray Tracing&Path Tracing
Whitted-Style Ray Tracing算法无法实现右边的glossy reflection效果,因为光线是按照镜面反射的方式传播的,真实世界大部分物体都不是mirror materail。
Whitted-Style Ray Tracing&Path Tracing
Color bleeding效果就是漫反射的间接光照,右图中的墙的颜色会反射到Box上。Whitted-Style Ray Tracing因为遇到diffuse material光线就不会继续传播了,所以左图中的箱子及屋顶墙面都没有接收到间接光照。
Distributed Ray Tracing
Distributed Ray Tracing
算法简介
基本思路和Whitted-Style Ray Tracing一样,只是为了解决漫反射光线传播,在每一个碰撞点上的半球空间都会发射多条光线来模拟漫反射。
算法的缺点非常明显就是计算量爆炸,这个算法即便是离线渲染也是不能接受的,不过这个算法应该是最朴素也是效果最好的光线追踪算法,说不定以后硬件性能爆炸,这个算法也有回春的一天。
Path Tracing
Path Tracing
算法简介
[*]从视点每个像素会发出多条射线,每条射线会形成一条光路。
[*]光线碰撞到物体表面后会在碰撞点的半球空间随机选择一个方向进行反射来模拟漫反射。
[*]如果光线触碰到光源则停止传播,否则使用俄罗斯轮盘算法来决定是否继续传播。
[*]每条光路相当于光源经过多次弹射进入视点,算法是递归执行的,相当于每个碰撞点都会乘以BSDF进行能量衰减,这符合能量守恒定律。
[*]每个像素的多条光线进行平均获得最终的光照效果。
Path Tracing
效果展示
Path Tracing
算法分析
这个算法的最大问题是光线命中光源的概率很低,因此会有很多噪点。
光源越小需要的光线越多
通过加大光源面积和投射光线的数量可以缓解这个问题。另外可以把每个碰撞点的光照分为直接光照和间接光照,直接光照从光源中进行采样以保证命中概率,并且直接光照会做shadow ray,而间接光照还是使用上面的算法,这样噪点仅存在于间接光照的部分,算法伪代码如下:
Path Tracing
Bidirectional Path Tracing
有的时候光源的环境很复杂,使用上面的Path Tracing依然会获得很低的光源命中率,如下图:
Path Tracing
上图左边的台灯方向是朝上的,就算是在光源上采样进行直接光照的计算,因为光源的方向很多地方也是无法照亮的。现实中这种情形,照亮场景的应该是间接光照,而间接光照的命中率又很低,所以上面的算法不太适用这种场景。
双向路径跟踪算法(Bidirectional Path Tracing,BDPT)在路径跟踪算法的基础之上,额外从光源出发创建光路,再连接从照相机追溯的光路和从光源出发的光路上的点,创建多条光路,并根据光线从这些光路传播的概率,合并各个光路传递的辐射亮度,即进行所谓的多重重要性抽样(multiple importance sampling),计算光线最后进入照相机生成图像的光照结果,如下图:
Bidirectional Path Tracing
这个算法的核心就是如何合并两种光路,这里就不介绍细节了,感兴趣可以看相关的论文,看一下效果对比:
Bidirectional Path Tracing
Metropolis Light Transport
Metropolis Light Transport
上图的光照环境特别复杂,光源只有一个Window,Room3要想被照亮需要穿过Door1然后再穿过Door2,这个时候就需要MLT光线传输算法了,这个算法很容易理解,其核心思想就是当在一个复杂的场景中好不容易找到一条能够到达光源的路径,就应该在该路径附近寻找更多的路径。而在一条路径附近随机寻找另一条路径的方法就是MLT的一个重要概念—突变策略
MLT和双向路径跟踪算法一样,理解起来很容易,但是真正实现的话还是非常困难的。
光子映射Photon Mapping
Path Tracing算法最大的问题就是光路命中光源的概率太低,即便是使用大量的SPP也会产生很多高频的噪点。另外在光线传播的过程中光路都是随机生成的,Caustics这种需要先弹射到镜面然后再弹射到漫反射物体的命中概率就更低了,因此Path Tracing很难模拟Caustics效果。
上面的问题可以通过光子映射来弥补,光子映射技术可以降低高频噪点,更容易模拟Caustics,也可以拿空间来换取时间,但是天下没有免费的午餐,光子映射会带来新的问题,比如低频噪点,高内存使用。
算法简介
光子映射的基本算法分成两个Pass。
[*]Pass1-光源从随机方向发射光子,并在一切表面上弹射,如果弹射的表面是diffuse材质则记录光子信息包括光子的POS,光子的能量POWER,光子的入射方向Dir,可以把这些光子理解成为次级光源,使用俄罗斯轮盘算法结束光子的递归弹射,这样可以保证能量守恒。这些光子信息与视点无关,所以可以保存起来供后续pass使用,这也是空间换时间的算法。
[*]Pass2-从相机射出的光线,打到非diffuse材质表面进行反射或折射,打到diffuse材质表面则根据收集半径检索附近的光子进行间接光照计算。
效果展示
Photon Mapping
算法分析
光子映射算法的核心就是从光源发射光子,然后缓存场景中的次级光源,然后在Path Tracing的时候使用这些次级光源做间接光照。和Path Tracing相比实际上是利用了空间局部性,复用了空间中计算过的光子,这个可以和TAA做个对比,TAA利用的是时间局部性从而复用spp。可以看到效果图中有一种脏脏的感觉,这就是因为每个shading point只采集局部的光子进行计算,从而造成了光照不连续,这种效果就是之前说的低频噪点。
因为收集的光子是从光源发射的,也就是说这些射线都是百分百命中光源的,因此也就大大降低了Path Tracing的高频噪点,从而也提高了Caustics的命中率。
如果想强化Caustics效果可以专门收集镜面反射-到漫反射的光子,然后单独存储这些光子,计算光照的时候也单独收集Caustics光子,这样就可以获得非常棒的Caustics效果。
传统的光子映射算法,最大的问题就是需要大量的内存空间存储光子,另外光子数量太多即便使用加速结构,Final Gather阶段的性能也不理想,为了解决这个问题,提出了PPM渐进光子映射算法。
PPM渐进光子映射
简单理解PPM就是将PM中的两个Pass调换顺序,然后第二个Pass变成多个Pass。
[*]从视点发出射线,找到视点可以观察到的point并缓存起来,这些point是需要收集光子的point。最终的光照计算可以把这些point当成多光源,进行直接光照计算。
[*]从光源发射光子,这些光子不需要缓存,遍历view point收集光子并修改相关参数,迭代反复多次来修正view point的结果。
PPM渐进光子映射
PPM算法的缺点是需要缓存很多view point,缓存的数量不确定,view point越多效果越好。
SPPM随机渐进光子映射
PM是缓存光子,PPM是缓存view point,SPPM不需要缓存任何东西,每次先从光源发射一定数量的光子,然后再从每个像素发射一条view射线,类似path tracing,path tracing的时候利用这些光子进行间接光照的计算,反复迭代上面的过程。
辐射度算法
算法简介
[*]将场景离散成patch,patch可以是矩形也可以是三角形。
[*]光源会先照亮一部分patch,然后这些patch会被当成次级光源继续照亮其他patch,然后被照亮的patch继续向下传播。
[*]算法的关键因素是如何计算一个patch会为另一个patch提供多少的能量,在算法中这个因素叫形状因子。
算法分析
这个算法和前面介绍的算法最大的区别就是把场景离散化,原先是光子的能量传播,现在变成了patch的能量传播。这个算法的缺点是复杂场景离散化比较费时,形状因子不容易计算。
二. 实时渲染算法简介
LightMap
对于静态场景,LightMap是使用最多的实时全局光照解决方案,基本原理就是将模型表面映射到2D贴图中,然后使用离线渲染将光照信息保存到贴图中。至于使用什么离线渲染算法,lightmap贴图的格式,以及lightmap光照信息的编码和解码,这些不同引擎,不同平台使用的都不一样,可以根据需求进行定制。
lightmap是空间换时间的算法,对于大型复杂的场景会占用大量的内存空间,不过可以使用虚拟贴图进行流送。lightmap最大的问题就是离线烘培时间,对场景做一点点改动就需要重新烘培,这不太符合现在大型场景的制作流程,所以现在越来越多的项目使用动态全局光照算法。
lightmap实际上就是一种全局光照的缓存技术,它是以物体为单位,所以它只能影响静态物体,动态物体不受影响。对于动态光源它也可以支持一部分效果,比如TimeOfDay中的白天和黑夜,这取决于lightmap中存储的信息,如果lightmap中只存储光照强度,那么就可以获取动态光源的颜色然后乘以lightmap中的强度进行最终的光照计算。
lightmap有一个非常好的特性就是无漏光,这是很多其他算法无法做到的。
Light Probe(Irradiance Volume)
light probe就是在空间中摆放很多的probe,这些probe可以当成次级光源,在离线的时候将irradiance保存到probe中,通常使用球鞋函数来缓存光照信息。
这样空间中的每个点都可以找到离它最近的四个probe,这四个probe可以构成一个四面体,然后计算四面体的重心位置,通过重心位置和坐标点进行差值来计算最终的光照信息,如下图:
light probe是逐物体的在CPU端计算每个物体的四面体,这样当物体很大超过了四面体的包裹,光照计算自然就是错误的。
这种情况可以把比较大的物体进行拆解然后取多个采样点算多个四面体,然后类似三线性插值,先计算每个四面体的插值,然后再在四面体中进行插值。
也可以将light probe注入到3D纹理中,通过硬件的插值来计算shading point的间接光照,如下图:
可以将上图的probe注入到两层mipmap的3D纹理中,使用3D纹理可以方便的插值但是内存空间占用太大,如果内存预算不足还是需要使用其他的方式存储probe,这里就提一下不继续展开了。
light probe因为缓存的是次级光源,所以无论是静态物体还是动态物体都可以用其进行全局光照的计算。但是因为light probe是离线计算的所以只能支持动态光源的部分性质。另外使用light probe还有漏光的问题,如下图:
花瓶在墙内,本来不应该受C,D光源的影响,但是因为算法的缺陷,所以会使用ABCD四个probe做成四面体,因此产生了漏光的现象。
Dynamic Diffuse Global Illumination
DDGI其实就是Irradiance Volume的实时版本,它利用实时光线追踪来计算probe信息,而Irradiance Volume是离线计算的,从这点上来说DDGI是完全支持动态光源的算法,但是它依赖可以做实时光线追踪的硬件或者是使用Ray Marching。
另外DDGI使用了切比雪夫不等式来预测probe是否被遮挡,减缓漏光问题。DDGI的核心是优化Irradiance volume的更新,这里就不详细介绍了。
LPV
LPV也是全动态实时全局光照解决方案,首先通过RSM找到次级光源,然后将空间均匀划分成3D网格,将次级光源注入到3D网格中,每个网格通过上下左右前后六个方向传播Radiance,通过几次迭代传播最终会稳定下来。每个shading point计算间接光照只需要获取所在网格的Irradiance即可,这个算法最大的优势就是计算Irradiance不需要依赖硬件的光线追踪,也不需要SDF进行Ray Marching,适合移动平台。它的缺点是漏光比较明显,另外因为每个网格使用球谐函数存储Irradiance,所以不支持镜面反射间接光照。
VXGI
VXGI也是全动态实时全局光照解决方案,它先用一个Pass将场景表面离散化成体素的形式,为了提高精度需要硬件支持保守光栅化或者开启MSAA提高分辨率,如下图:
体素化的voxel可以组织成层次结构,每个voxel中会记录Irradiance Cone和Normal Cone,如下图:
shading point发射Ray Cone Tracing计算间接光照,如下图:
VXGI的优势是支持镜面反射全局光照,缺点是运行效率慢,主要是需要运行时离散化场景,Ray cone效率不高,处理漫反射也比较费劲需要使用多个cone覆盖半球空间。
GIBS
GIBS与VXGI不同之处是VXGI将场景的物体表面离散成了voxel,而GIBS将场景表面离散成了surfel。描述一个surfel的基本数据结构是位置,法线和半径,这就定义了一个2D的圆形表面,如下图:
GIBS通过屏幕空间的像素反推到世界空间来查看该点被surfel的覆盖程度,如果覆盖程度最低则生成新的surfel。
这些surfel可以用来做Irradiance cache,和光子映射类似,只不过光子映射存储的是一个点,surfel存储的是一个圆形,圆形内部的点可以做插值计算Irradiance,用一个圆形代表一片区域的光子,降低缓存光子的数量。
PRT
PRT就是将光照方程进行拆解,然后用球谐函数记录每个部分,利用球谐函数的性质还原光照方程,因为light transport部分包含场景的遮挡关系且这部分是预计算的所以PRT只支持静态场景动态光源。
SSGI
ssgi是基于屏幕空间的全局光照算法,利用深度信息进行Ray Marching,效率不如SDF,但是不需要额外的存储空间,因为是基于屏幕空间的光照算法,所以缺少屏幕外的光照信息。
Lumen
ue5的lumen主要是实现了一套软光线追踪算法,其中涉及的内容几乎包括了上面所有介绍的实时全局光照算法,会单独写一篇文章介绍,这里就不详细介绍了。
总结一下实时全局光照算法,基本分为三个步骤:
[*]离散化场景空间或物体表面,probe,voxel,surfel,均匀网格,还有一个lumen的mesh card后面介绍。
[*]计算Irradiance cache。
[*]利用Irradiance cache还原光照方程。
mark 好文章 有不足 有改进 逐渐递进
页:
[1]