2020年11月17日晚的Unity线上技术大会中,米哈游技术总监弋振中《原神》在主机平台上的渲染技术要点,主要包括了阴影,AO,Local Light,Volumetric Fog & God Ray效果,Reflection以及HDR等多个方向的主要内容,这里是原文传送,下面我们将对其主要内容进行学习与梳理,希望在后续工作中能够有所助益。
1. 阴影
阴影的实现分为动态阴影与烘焙阴影两项,动态阴影这里使用的是8级的CSM方案覆盖800米视距,其中前面四级阴影每帧更新,后面四级阴影则是轮流更新(即轮流间隔四帧更新一次)。
软影采用的是泊松圆盘Random Sample Pattern的PCF,每个像素对应的Sample数目为11个这个会导致较高的计算消耗,为了降低这一块的消耗,使用了一个mask贴图来标记屏幕上各个区域的阴影属性,只对处于半影区的像素进行软影PCF计算。
Mask贴图使用的是分辨率为屏幕分辨率的1/4 x 1/4,正常来说,要判断Mask贴图中的某个像素是否属于半影(绘制输出Mask贴图),就需要对屏幕分辨率下这个像素所覆盖的16个像素进行遮挡判断,之后整合输出(视频中没有给出,所谓的整合输出指的是求取平均值吗?)当前像素的半影判断结果,但是这种做法会有较高的消耗,为了进一步提高计算效率,决定采用抽样调查算法,从16个子像素中按照一定的pattern抽取样本进行判断,这种做法会有一定的误差(某些本该是软影的被错认为只有硬影),为了降低误差,这里会对Mask贴图进行模糊处理(扩大了半影的半径),这样一定程度上可以缓解前面将软影错认为硬影的误差。
整个Mask贴图的生成与模糊处理消耗大概为0.3ms左右,而整个阴影计算的GPU消耗大概为1.3~1.7ms左右,而优化开关的阴影质量基本上肉眼无法区别。
2. AO
阴影只能表示对光照的宏观的单层的遮挡效果,却无法模拟细微的多层(指多光源作用)的效果,比如处于阴影中的地面,只使用阴影的话,将无法模拟地面上的其他物体在其上的投影,从而显得物件悬空。这些瑕疵可以通过Ambient Occlusion方案来解决,而《原神》提供了多种AO方案,可以针对不同的情景进行选择使用。
2.1 HBAO
HBAO是horizon based ambient occlusion的缩写(另外,这里需要注意的是,我们平时说HDAO(High Definition Ambient Occlusion)与HBAO,其实这两者是等价的,只是前者是AMD给出的称呼而后者是NVidia给出的名字),这是一种非常常规的AO方案,可以通过屏幕空间的后处理来为物件提供更为细腻的细节表现,其基本实现原理为:通过对各个方向上的深度进行采样求得最大遮挡角度,通过对各个方向上的最大遮挡角度的正弦进行累加来得到AO,这个算法的质量比SSAO要好,实现逻辑更加物理真实,但是其性能消耗会比较高,业界通常使用的是其优化版HBAO+。具体可以参考此前关于AO实现算法的一篇综述:Ambient Occlusion技术方案综述。
2.2 AO Volume
AO Volume是为静态物体所开发的AO方案,相对于HBAO而言,AO Volume可以生成更大尺寸的遮挡效果,如上图所示,对于这类近似阴影效果的AO,HBAO算法是无能为力的,因此这里需要增加额外的AO Volume算法逻辑。
AO Volume的实现逻辑可以参考此前做过的一篇AO算法的综述分享。
《原神》中对于AO Volume的使用方案是,在离线的时候完成静态物体的AO数据的烘焙,之后在运行时根据烘焙数据计算出最终的遮挡效果。
2.3 Capsule AO
静态物体的阴影可以通过lightmap+AO Volume来实现,而动态物体的阴影如果仅仅依靠HBAO的话会显得过于简陋,但是如果额外增加一个shadow map用于应付这种情况又会显得过于浪费,《原神》这边的做法是通过Capsule AO来为动态物件(比如角色)等添加(相对于HBAO)更为精细的阴影效果。
Capsule AO的具体实现原理可以参考之前的AO算法的综述分享,简单来说,这个算法是通过用多个胶囊体(会跟随角色的骨骼发生位置等变换)对角色进行模拟,之后通过计算胶囊体在待渲染位置上半球的投影面积占比实现对环境光AO(或者阴影)的输出,通过计算胶囊体在带渲染位置以主光源方向为轴线方向,以一定的角度为锥角的圆锥上的占比来计算主光源AO(阴影),从而实现更为精细的AO(阴影)效果。
2.4 AO的优化处理
为了提升AO计算的性能,《原神》这边所有的AO计算都是在1/2分辨率上进行的,且在计算完成后,为了进一步柔化效果减少瑕疵,这里还会增加一个双线性模糊处理(横向+纵向),最后在使用的时候,还会需要一个额外的上采样过程(这个在使用的时候直接使用1/2分辨率的贴图就可以,应该不再额外需要一个上采样)。
在进行模糊处理的时候,每个像素需要采取周边多个像素的数值进行混合,可以看到,如果使用传统的PS,每个像素都会需要多次贴图采样,且这些采样结果实际上是可以在相邻其他像素的计算中进行重用的,因此为了进一步提升计算性能,《原神》这里的做法是将模糊处理放到Compute Shader中来完成。
具体的做法是,将相邻像素的采样结果存储在局部存储空间(Local Data Share)中,之后再模糊的时候取用,一次性完成四个像素的模糊计算,并将结果输出。
3. Local Light
《原神》采用的渲染管线是Clustered Deferred lighting(详情可以参考此前的分享:常见渲染管线整理与总结 - Clustered Forward Rendering),将场景划分为16个slice,每个slice的尺寸为64x64个像素,最多可以支持到1024盏局部光源。
上面这张图中展示了多个局部光源的光阴效果,可以看到,局部光源也是有实时阴影的,但是并不是所有的局部光源都会添加投影(应该是美术同学控制的),这里说到支持实时阴影的局部光源数不超过100盏。
整体的阴影效果由烘焙的静态阴影与动态阴影(推测使用的是阴影贴图,但是计算消耗会很高,其中做了什么优化,从给出的材料中难以推断)的结合。
对于局部光源的静态阴影而言,需要烘焙对应的shadow map,由于光源数目较多,每盏灯都烘焙,会导致硬盘空间占用较大,且由于shadow map是一张深度贴图,也不能使用Block Compression进行压缩,为了解决这个问题,《原神》团队自己设计了另外一套压缩思路。
具体思路(参考了Siggraph 2019的文章A Scalable Real-Time Many-Shadowed-Light Rendering System)给出如下:
- 对原始的shadow map以2x2为block进行压缩,每个block包含4个深度值,总共使用32bits进行保存(如果需要更高的精度,也可以使用64bits),具体的编码方法暂时未知。
- 每个block的编码方式有两种:
2.1 基于深度平面方程进行编码(即对四个深度值进行共面计算,输出AX+BY+CZ+D=0平面方程中的ABCD四个参数)
2.2 通过浮点数压缩算法将FP16转换为FP11? - 通过Quad Tree对block进行汇总,输出稀疏四叉树,通过这种方式进一步提升压缩率。这里的四叉树不是全局的,而是将整个贴图分成多个tile,每个tile包含64x64个像素(即16x16个blocks)。
这个方案的表现为,在室内场景的精度压缩比为0.2 ~ 0.3左右(即从10k压缩到2 ~ 3k,效果上可能会存在轻微瑕疵),高精度模式下的压缩比为0.4 ~ 0.7(瑕疵基本上难以肉眼辨别)。
4. Volumetric Fog & God Ray效果
首先基于相机空间,将整个frustum分割成多个voxel,这些voxel跟前面clustered deferred lighting中的cluster是一致的,这种设定有助于后续局部光源的scattering计算。
更新每个voxel上的光照数据,在这个过程中会考虑到fog参数与局部光作用
使用raymarching算法对射线上的voxel进行数据进行累加,将累加结果输出
体积雾计算方式本身就能够实现God Ray效果,但是用这种方式实现的God Ray可能会不太明显(由于voxel数目少,导致这种做法输出的贴图分辨率比较低,且由于雾气浓度可能不足以产生God Ray,都会使得God Ray效果不明显),这里《原神》团队为了使得God Ray效果更好看一些,使用了一个额外的Ray Marchingpass用于实现God Ray,并且还添加了一些额外的参数,在ray marching结束之后会使用这些参数来对God Ray效果做进一步调整,从而产生更为美观的艺术效果(虽然并不物理)。
5. Reflection
5.1 IBL
视频里展示的场景Reflection Probe与Ambient Probe都是随着时间24小时连续变化的,而传统的反射贴图等都是离线Capture的,通常不能做到随着时间而变化,那么这个表现是如何实现的呢?
5.1.1 Reflection Probe
对Reflection Probe而言,这里的做法是不再直接烘焙一张环境贴图,而是烘焙一套mini GBuffer贴图(包括Normal/Depth/Albedo等信息),之后根据当时的光照数据实时生成环境贴图(Reflection Cubemap),跟传统Reflection Probe一样,美术同学也可以在场景中摆放大量这样的Probe。
实时生成环境贴图主要通过如下几步完成:
- Relight,根据当前的光照数据,利用mini GBuffer的相关数据,进行一遍Lighting计算
- Convolve,对Lighting完成后的cubemap,先生成mip-chain,之后对每级mipmap进行卷积运算,目的是得到各个面能够正确衔接mip贴图。
- Compress,为了降低cubemap的内存消耗,这里还使用了BC6H压缩算法对cubemap进行压缩处理
整个过程是通过Compute Shader完成的,在Compute Shader中通过多个线程可以同时完成六个面的处理,且由于单个Reflection Cubemap的更新其实已经比较费了,为了减轻单帧压力,这里将Reflection Cubemap的更新分散到多帧进行,每帧只对一个Probe进行更新,通过时间轮转完成所有Probe的更新。
5.1.2 Ambient Probe
前面更新完Reflection Probe之后,就得到了当前Probe的光场数据,而Ambient Probe的光照数据就是从这个光场中提取出来的,而由于Ambient Probe的低频特性,这里可以用SH系数来保存整个光场,进一步压缩内存消耗。
光场提取也是通过Compute Shader实现,一次性完成6个面的计算处理。
5.1.3 改进
在完成上述步骤之后,能够得到随着时间动态变化的光照信息,但是还存在一些问题:
- 阴影数据缺失,mini GBuffer无法完成阴影重建
- Relight计算过程只考虑了主光源,如果需要考虑局部光源的话,会导致大量消耗
- 室内外光照环境不一致问题,因为光照环境不一致,如果不做处理,会让人感觉到不和谐
针对这些问题,《原神》给出了如下的解决方案:
- 阴影缺失问题,通过在离线的时候将24小时的阴影数据烘焙后用Shadow SH系数保存下来,在运行时根据当前的时间选择对应的Shadow SH进行插值解决,且从效果上来看基本上看不出瑕疵
- 局部光源的Relight问题,也可以采用与Shadow缺失同样的解决方案,在离线的时候将局部光源SH保存下来,在Relight的时候进行使用
- 室内外光照环境不一致的问题是通让Reflection Probe与Ambient Probe在室内与室外进行不同的处理来解决的,为了更精确的标注室内的范围,美术同学还需要额外摆放一个interior mesh,下面是效果对比
此外,为了避免在室内外过渡区域出现硬切,这里还在过渡区域处进行一下平滑处理,下面是根据interior mesh生成的Mask贴图:
Reflection & Ambient Probe在使用的时候会同时将AO数据也考虑进去,这样可以有效减轻漏光问题。
5.2 Screen Space Reflection
SSR在PS4 Pro上的开销是1.5ms左右,为了增强显示质量,还添加了一个Temporal filter来混合历史数据;此外,为了能够快速得到各种粗糙度下的反射结果,这里还为SSR生成了一个类似于Hi-Z的buffer贴图。
6. HDR
这个原文比较详细了,就不赘述了