《战神4》中的图形学技术——全局光照
全局光照技术
全局光照的定义
全局光照(Global Illumination,GI)或称间接光照(Indirect Illumination)。GI可以分为直接光照和间接光照两部分,通常要考虑直接受到光源照射的能量和反射光源产生的能量的总和。
为了更好理解和分析《战神》中的全局光照技术,下面先介绍一下GI的技术发展路线。
全局光照的技术发展路线
在光栅化渲染管线中,我们需要将场景中的模型数据准备好后输入顶点着色器中,经过细分曲面着色器、几何着色器、光栅化、片元着色器等操作拿到输出图像。这一过程中并不涉及间接光照。直到光线追踪技术的出现。
光线追踪的思想本质就是从屏幕投射一条光线,光线会经过不断的反射、折射直到打到光源,对物体的着色就是在这一过程中不断积分能量并计算渲染方程。
光线投射(Ray Casting)
光线投射由Arthur Appel在一篇名为《 Some techniques for shading machine rendering of solids》中提出。这是光线追踪的第一步,需要将屏幕空间转换到世界坐标下,然后以每个像素为起点向场景中发射一条光线,并根据与光线相交的第一个物体表面的能量进行渲染。
光线追踪(Ray Tracing)
Whitted-Style光线追踪在光线投射的基础上使用了递归式的光线追踪。当光线与材质表面相交时根据不同表面进行计算。例如,如果材质是反射就计算光线的出射光线并发射出射光线继续进行渲染。
路径追踪(Path Tracing)
1986年,Kajiya提出了路径追踪方法[1]。路径追踪在Whitted-Style光线追踪的基础上做了一些调整修改。一个修改是光线的反射,不再是简单的计算入射角和出射角,而是考虑一个反射光线组成的半球,对半球进行采样,随机选择可能出射光线继续追踪。另一个改进是当光源为面光源时采用了蒙特卡洛积分的方法计算光线的贡献。另外Kajiya最重要的贡献是建立了渲染方程理论。
式中,
Lo指x位置处具有的总能量
Le指x位置处自发光的能量
后面的积分表示x位置处接收从w’方向传播来的能量。fr是材质的BRDF公式。
Kajiya的路径追踪方法已经将直接光照和间接光照考虑进去了,可以说已经实现了全局光照。虽然可以采取KDTree或BVH等加速结构进行光线求交这一过程进行加速,但是当时的方法还只能用在离线渲染上,远远不能达到实时渲染的帧率要求。Kajiya方法的问题主要是在反射光线时采样时随机的,并没有考虑光线的重要性。因此为了结果看起来没有很多噪点除了后处理的方式降噪就只能增加每次采样时的次数。而增加采样次数就会造成投射光线的数量指数级增长,严重拖慢渲染速度。
所以一些蒙特卡洛Ray Tracing的实践工作主要集中在重要性采样上。一种简单的方法是均匀采样,即光线达到某物体表面后均匀选取该点周围的光场信息,沿对应方向投射光线。但均匀采样很可能恰好略过重要的微小光源,使得结果不正确。更好的方法是使用概率分布函数(Probability Distribution Function,简称PDF)进行重要性采样,选取能量较高且较为集中方向投射光线。
动态全局光照与流明(Lumen)
以上的GI全部用于离线渲染,为了能够实时进行全局光照的计算,许多图形工作者做出来很多工作。
Reflective Shadow Maps(RSM)
RSM一定程度上结合了Shadow Map的生成方式和光子映射(Photon Mapping)的思想[2]。主要内容是用一个相机在光源处沿光照方向渲染一张纹理,其记录了光源直接照射某一位置(为了相机移动时方便计算,一般使用世界坐标系)的深度、世界坐标系下的顶点位置、法线、反射通量(flux)等。经过计算可以得出被遮挡物体在其他间接光源作用的接收的能量积分。
RSM公式中Φp表示光源辐射通量。
获取到纹理后还需要对纹理进行采样。当我们将x点投影到xp对应的纹理坐标时记录该点的uv坐标为(s,t),随后以(s,t)为中心r为半径进行采样(实际是在做Cone Tracing,Cone Tracing就是以一跟光线为中心的)。原文选择使用极坐标表示。ξ1、ξ2分别为均匀随机数,则采样像素位置则可以表示为
(s+rξ1(sin(2πξ2)),t+rξ2(cos(2πξ1)))
为了使采样点更加均匀还要再除以ξ12进行修正。在采样数为400左右时便可以得到不错的效果了。
但是RSM方法也有不少缺陷。首先每个灯光都需要一张RSM进行描述,其次没有考虑到灯光的可见性,当一个RSM中出现其他灯光时会有一些视觉上的错误。
Light Propagation Volumes(LPV)
LPV方法基于光照辐射率在三维空间中沿直线传播且传播过程中辐射率不变[3]。LPV的思想是将三维场景均匀体素化(不是物体体素化),记录每个体素的辐射度(radiance),将其转换为一个二阶的球谐函数。在着色时计算着色点的辐照度(irradiance)。
Voxelization Based Global Illumination(VXGI)
VXGI是UE4中使用的实时全局光照技术。另外同样使用体素化思想进行全局光照渲染的方法还有Sparse Voxel Octree Global Illumination(SVOGI)。前者使用Clipmap的方式存储体素,后者使用八叉树存储体素。这两种方法在物体发生变化时都需要重新构建体素,而八叉树的构建或调整是复杂且较为缓慢的,Clipmap的重建调整则相对简单迅速。另一方面,SVOGI使用Mip-map存储体素信息,一些与相机距离较远的体素也存储了较高分辨率的体素信息,而这部分信息很可能是用不到或者很少用到的,就会造成一定的内存浪费。Clipmap在此基础上进行优化,只对中心的光照信息进行Mip-map操作,节约了许多内存空间。可以说VXGI优于SVOGI方法,这里也只介绍VXGI方法。
Clipmap在构建时会将三角形投影到xy、xz、yz三个面上,同时记录透明度、颜色、法线、自发光等信息到3D纹理中,对于一个体素包含多个三角形的情况则会取这些三角形属性的平均值。在距离相机较近的位置使用更加精细的体素,在较远的位置使用体积较大的体素。由于近大远小,这些体素看起来大小差距并不大。
构建好体素后,使用RSM的方法拿到纹理,收集每个体素接收到的能量。接下来在每一次Bounce时使用Cone Tracing代替单独一根光线的投射。Cone Tracing就是将一根光线换做有一定角度的圆锥。例如,对于一个半球可以使用多个ConeTracing进行概括描述。对于很锐利的高光则可以使用单一的小夹角的Cone进行描述。在实际反射中会根据材质进行数量、角度上的调整。如果材质是漫反射的,则会在半球上分布多个大夹角的Cone,如果是高光,则只使用一个Cone对出射光线进行描述。
那么替换原先的光线后又应该判断与体素是否发生相交呢?这里使用形似Ray Marching的方法。首先定义一个光锥,记其起始点为C¬o,投射方向为Cd,圆锥体角θ,某时刻光线长度t。
则某时刻圆锥底面半径为:r = t⋅tan(θ/2),假设有一以圆锥底面圆心为中心,r为半径的球,我们需要构建一个与这个球相切的正方体,将整个球包裹在其中。
因此正方体边长为:d =2⋅r = 2⋅t⋅tan(θ/2)
根据正方体的边长我们就可以计算出需要在哪一层级的纹理进行采样了。
level = log2(d/Vsize),其中Vsize是Mip-map的最高层数。
VXGI也存在一些问题,比如3D纹理的存储浪费资源;以及当大尺寸体素中的物体比较小时则会发生Cone与其相交但光线从中漏过的情况,从而产生漏光(Light Leaking)现象。
Screen Space Global Illumination(SSGI)
SSGI的思想比较淳朴,渲染完整个屏幕后,将一些反射面看作一个镜面反射,找到出射方向上对应的像素颜色,就视作找到了间接光源。SSGI的具体实现步骤如下,在渲染完成后,从反射面投射多个射线;每条射线进行Ray Marching以类似二分查找的方式根据GBuffer的深度信息(需要Mip-map压缩)找到阻挡光线的点;最后使用该点的颜色作为间接光源。
常规的Ray Marching算法光线的步长是均匀的,为了更加高效SSGI中对Ray Marching做了一些调整。如果当前的Ray没有检测到被阻挡则下一次循环将以上一次检测距离的两倍进行检测,如果有深度比当前深度小,则向后查找是否漏掉更小的,如果没有则视为找到间接光源的着色点。
SSGI中还可以存储周围的间接光源信息,以便进行重用,从而加快速度。当然SSGI的缺点很明显,如果一个物体的间接光源不在屏幕范围内,那么就不能被找到,于是一些物体的反射效果就会残缺。
屏幕空间环境光遮蔽(Screen Space Ambient Occlusion,简称SSAO)
SSAO通过添加模型内部的阴影体现相互遮挡关系从而达到全局光照的效果。SSAO基于两个假设:所有的材质在进行SSAO计算时均视为漫反射;场景中所有物体接收到的间接光照为指定常量值。其实SSAO就是一个后处理效果,首先向G-Buffer写入Depth、Albedo和顶点位置,再根据G-Buffer中的顶点周围随机放置一些采样点,每个采样点都可以通过投影矩阵获得其深度,将该深度值与G-Buffer中的深度进行比较,如果采样点深度小则采样点可见,反之不可见。据此就能计算出每个顶点的可见性如何,带入SSAO公式即可。
浅谈流明(Lumen)
Lumen是UE5使用的全局光照技术,核心思想是使用有向距离场(Signed Distance Field,SDF)描述模型边缘,以此简化Ray Tracing的计算;使用表面缓存(Surface Cache)存储上一帧的光照信息,为下一帧计算Multibounce提供数据。
《战神4》中的全局光照系统
由于全平台都未找到《战神4》全局光照系统的讲座视频,主要参考资料来自讲座PPT。这部分可能会比较粗糙。
讲座主要介绍了《战神4》中全局渲染技术的三个部分,分别是GI Volume、Cubemap Normalization和AO[7]。这些技术和其他3A游戏中使用的技术相似,但介绍了一些遇到的问题以及解决方案。下面的一些图片来自讲座PPT和GPA抓帧分析。
在正式介绍全局光照技术前需要先了解《战神4》的关卡结构。《战神4》是一款线性叙事游戏,在游戏场景加载上采取的是流式加载。即加载玩家所在的场景以及所在场景的前后两个场景,移除之外的所有场景。因此一些光照信息比如一些Probe可以预先烘焙好,在游戏时与场景共同加载。
全局光照体积 GI Volumes
什么是 GI Volumes
可以类比Unity中光照探针(Light Probe)。程序会自动以Volume为中心烘培体积内的光照信息(包括直接光照、间接光照等)。
为什么使用 GI Volumes
传统制作流程中使用Lightmap记录受到直接光照的物体。Lightmap只与环境有光,不会记录角色的光照信息。Lightmap需要手动维护UV需要手动放置探针。一系列的工作相对繁琐重复。
使用GI Volumes可以减少人工工作量,将环境和角色联系到一起,能配合场景加载系统工作,且性能优越。
GI Volumes 如何工作
《战胜4》的关卡编辑器直接在Maya中运行,GI Volumes也需要美术在Maya中手工放置。一般来讲GI Volume每一米左右放置一个就足以满足画面需求,且美术放置后可以自动烘培。GI Volumes将记录体积内地静态间接光源,存入4个3D纹理,纹理使用16位浮点数记录RGB信息和天空可见性,同时存储2波段的球谐函数(Spherical Harmonics,SH)。
需要说明的是天空环境光和反射信息是分开记录的。分别记录到不同纹理的好处在于,可以结合不同的立方体纹理(天空包围盒)实现相同物体在不同天空下的渲染。
一个场景中有多个GI Volume,渲染时会收集相机前方最近的4个GI Volume,同时每个Volume都会事先分配到一个次序,着色器根据GI Volumes的次序依次进行着色。
GI Volumes 的问题及解决方法
A. 当体素体积过大时,体素可能会将一些小物体包含进去,导致自遮蔽和漏光问题(Light Leaking)。
B. 移动的物体会由于小角度的转动导致法线变化剧烈使物体表面闪烁。
C. 对于一些角落模型法线可能比较混乱从而出现漏光。
D. 仅使用的Cubemap的话会导致部分反射发光,这是因为Cubemap的信息没有本地的遮挡关系(Local Occlusion)。
解决A问题可以将GI采样位置沿物体法线方向偏移一个体素。解决B问题可以在烘培时不对法线进行偏移。解决问题C可以通过额外的GBuffer平滑模型法线。对于问题D需要对立方体纹理进行归一化,用GI对光照信息进行修正。下面将具体讨论Cubemap 的归一化。
立方体纹理的归一化 Cubemap Normalization
Cubemap仅仅记录了环境的颜色信息,环境中的一些低频光会被球体接受并反射,从而发生上述问题。因此将这部分替换为正确的信息即可。使用GI Volumes的SH移除Cubemap的低频细节,使用GI的低频信息替代,就得到了合理的结果。
但是归一化的过程可能会出现除0的情况(特别是在球的底部),或者因为场景的不同效果出现差异等问题。下面是一个离散化为N阶球面空间的公式。s是空间分布的一片微小区域,用(球坐标表示),g表示对不同方向上的光照强度进行采样,并与基底函数Yi相乘并累加求和,经过球的表面积平均后可以得到球谐系数ci。为避免SH等于0(或接近0)的情况时出现可以减小在采样Cubemap时的方向性和饱和度。
环境光遮蔽 Ambient Occlusion
环境光遮蔽部分使用SSAO与AO map、以及环境阴影等技术共同完成。同时还需采样角色的胶囊体以产生角色的阴影。环境阴影采取和《Last Of Us》相同的技术。