前言
这周由于玩《艾尔登法环》的原因,读一篇略短一些的GDC文章的内容,分享人是腾讯的《暗区突围》(英文名:
Arena Breakout)项目的渲染主程Junhong Wang。
我个人认为国内手机跨平台游戏中,在世界规模、美术视觉和性能几个方面综合来说都还不错的有米哈游的《原神》,腾讯的几个枪战游戏,网易的《明日之后》等——这几个也是GDC上都发过分享的国内产品。今年也持续会出很多类似规模的游戏,但具体表现还需要观察。
另外,我个人觉得腾讯游戏其实不是以技术规模制胜的,它更多是一个商业成功的发行商和养蛊者;但反过来说,其实腾讯内部也是有很多技术大神的,我也乐见他们能拿出这种有技术含量的又能商业成功的产品。当然我个人有一套评价标准,肯定不是说商业成功就是一切——读技术分享的文章里我就不对模仿原罪问题和大公司团队内耗问题表达个人观点了。
本次内容还是以配图并翻译原文为主,同时打星号的部分是我个人的一些备注与分析。
一、项目背景概述

*整个项目是搭建在虚幻4引擎上的开放世界大地图FPS游戏,对于安卓和IOS有不同的图形API。

关于《暗区突围》手机版
- 前向渲染管线
- 基于物理的渲染
- 动态天气系统(Chan22 可以参考文末列出的另一篇分享)
- 室内场景的全局光照是基于预计算的

——手机光线追踪
- 最新的智能手机已经具备了硬件加速的射线查询(Ray Query)功能
- 射线查询可以在Shader的任何层级使用(指预计算、顶点、片元等)
- 大部分帧捕获(Frame Capture)分析工具不支持手机Vulkan射线查询
——启用光追的准备
- 开启Vulkan在安卓上的图形API(RHI是Render Hardware Interface的缩写)
- 更新Shader的交叉编译(ShaderConductor是UE的一个功能)
- 拓展UE4中的Vulkan和Metal的RHI

《暗区突围》手机版中的光追应用
*安卓版光追已经发布了,IOS版(截至分享日)还在开发中。
*光追主要应用到反射、软阴影、AO这几方面。
二、光线追踪的场景管理
*虽然基础思想都是射线可以进行递归的弹射查询,但手机端主要考虑的是减少检测数量和优化加速结构。

挑战——图中展示了PC上通过Nisight捕获调试的TLAS
- 底层加速结构(BLAS)的内存使用
- 构架BLAS是很昂贵的
- 在有过多实例时,最终上层加速结构(TLAS)也很昂贵
* Bottom-Level-Acceleration-Structure(BLAS)定义了几何体的基本组成。如果物体是网格形式就是三角面的顶点索引,也支持其他组织形式比如体素,曲线等,但要自行构建加速方式。Top-Level-Acceleration-Structure(TLAS)定义了场景实例层的相关属性例如包围盒、变换矩阵等数据 。简单来说可以参照下图:

Nvidia 的practical-real-time-ray-tracing-rtx中的配图

——世界总览
- 关卡通过关卡streaming系统管理
- 低LOD精度的网格在package加载时一起加载
- 高精度LOD通过网格streaming系统动态加载
- 包含遮罩、世界坐标偏移、透明材质的网格不创建BLAS
——在LOD加载时创建BLAS?
- 需要加载超过4700个BLAS
- 内存占用过大导致崩溃

构建和销毁BLAS
- P是摄像机位置
- r1是BLAS创建的半径
- r2是BLAS销毁的半径
- d是摄像机距离物体包围盒的距离

构建和销毁BLAS
- 当物体在r1范围时进行松散的BLAS加载(lazy 这里指响应不用那么及时的)
- 在n帧后在进行BLAS的延时销毁以避免频繁创建和销毁
- 所有参数都是可以缩放改变的
——结果:
*加载数和内存用量降到了700个BLAS和1.1G

顶层加速结构TLAS
- 每帧更新
- 需要对实例进行剔除(引用的2个剔除算法改进可以参考文末资料名称)

在VIVO X90上的性能情况
三、光线追踪的反射

反射关闭时

反射开启时
*由于射线数量限制,这里的反射只能是镜面反射,没办法做出镜面和漫反射之间的粗糙度效果。因此粗糙度只能通过模糊处理等方式来近似,后面会提到。

光追反射的挑战
——反射像素的着色
- 没有RTPSO(DX12中自带的一套可编程的光追次级结构)
- 没有无绑定纹理(ShaderModel 5.1中提出的一套更灵活的纹理寻址方式,可以优化GPU调用及显存)
- 上千种不同材质
——混合的渲染管线
- 当前的主渲染管线是前向渲染(引入光追后需要引入一些类似延迟渲染的帧缓冲)
——性能:能耗、帧率

反射管线分析:
基础pass(不透明场景颜色、法线粗糙度和检测标志、深度)——场景查询pass——可见性处理pass——联合双边滤波过滤——联合双边滤波过滤(参数不同)——混合——后处理
*后面逐步介绍了各步骤的一些细节。

(数据)准备
- 创建用于射线反射查询的特殊网格shader
- 收集射线查询网格的渲染指令
- 每帧重新为网格绘制指令分配网格实例ID
- 当构建TLAS时,指派网格实例ID和HitGroupId

基础pass
——MRT(这里是MultiRenderTarget,绘制到多个帧缓冲。相应的RT就是渲染目标纹理)
- RT0:场景颜色(R11G11B10 数字是颜色位数)
- RT1:定制化的GBuffer(法线+粗糙度+标志位 A2B10G10R10 参考延迟渲染管线)
——深度模板:D24S8
——可选:(对于金属材质)反射颜色

屏幕空间场景查询pass
- RT0:压缩编码后的三角面ID和射线命中的重心坐标
- 可选RT:射线命中的方向
- 深度缓冲:射线命中的纹理绘制ID
- 算法:
- 重新构建摄像机相关的世界空间坐标和反射线方向
- 射出射线,如果命中TLAS实例,获得其网格实例ID、三角面ID和命中点的重心坐标
- 输出结果至RT0和深度缓冲
*关于重心坐标在Games101中有详细介绍,主要是通过和三角形顶点的关系来定义三角面中的任意点。

——可见性处理pass(CPU)
- 发出射线查询可见性处理的渲染指令——绘制ID通过统一的缓冲区提交
- 每一个绘制指令覆盖全屏四边形范围
——可见性处理pass(GPU)
- 如果绘制ID不匹配(检测不通过),则丢弃该像素
- 在像素(片元)着色器中进行PBR参数的插值
- 发出阴影查询射线(*与深度缓冲比较)
- 计算直接光照和间接光照
- 写入反射RT

优点:较好的硬件兼容性
缺点:
- 绘制指令数量和TLAS数量一致(600多个)
- 重叠绘制——太多全屏四边形绘制

与之前相比的优化方案
——绘制指令:使用GPU剔除查询来去除不可见的网格绘制指令
——重叠绘制:
- 在顶点shader中,为网格绘制ID分配四边形深度
- 开启深度检测以避免重叠绘制

*优化后减少到了110个draw call,且没有重叠绘制了。

模糊pass1
输入纹理:反射贴图、定制化的GBuffer贴图、深度贴图
算法: 联合双边滤波,模糊核偏移尺寸基于粗糙度调整
*关于模糊算法和核(Kernel)之前我有一篇文章介绍过。关于联合双边滤波,简单来说是一种可以较好保持原图边缘的模糊和降噪算法,文末会附加资料链接。

模糊pass2
*通过联合双边滤波再算一遍,但模糊核偏移更大了。

混合pass
- 边缘保持
- 对于光滑地板有锐利的反射结果
- 对于粗糙墙壁有模糊的反射结果
- 对完全粗糙的材质不发出射线
*这里的模糊反射效果是基于单一射线查询加模糊算法得出的,而复杂的光线传播是会有多条光线散射并求积的情况的。离线渲染中会对粗糙度不同表面采用蒙特卡洛积分(采样)的方式来求积。所以从这个意义上说这里算反射还是用的前现代的光追算法,和1974年的Witted Style一致。

优点:较好的硬件适配,高性能
缺点:难以处理半透明或有遮罩的材质
四、光线追踪的AO和软阴影

关闭光追AO

开启光锥AO

基于多层阴影纹理的阴影(Cascade Shadow Map)

光追开启时的阴影和AO
*可以看到开启光追后,阴影和AO质量都有较大提升。

阴影和AO做光追的挑战
两者都需要大量射线:阴影1根、AO1根、没有降噪辅助。
需要考虑性能表现。

软阴影和AO的光追管线分析:
前置pass——场景查询pass——分帧降噪——A-Trous过滤(2个pass)——基础pass——后处理pass
*前置pass包含了场景的法线和深度,后面从场景查询pass开始介绍。

- 把太阳当作一个圆球(普通光源可能抽象成一个点,但光追很难追踪一个点)
- 采用深度来重新构建射线原点,使用Halton序列来生成采样方向
- (每帧)阴影1根射线,AO1根射线
- (帧缓冲的)R通道用于阴影、G通道用于AO
- 阴影射线背面剔除
- 以较低分辨率执行
*这里其实就类似现代光追面临的问题了,在每个采样点需要更多信息来确认阴影和AO情况,但性能上只允许每个点一帧采样一根射线。可以看到只运行一帧时偏差很大,所以Games202的闫老师说过目前实时光追的核心技术是降噪,这既有调侃的意味,很大程度上也是事实。

分帧降噪pass
- 对屏幕位置与前一帧的阴影和AO纹理(中的点)做重映射之后的比较
- 基于深度比较做排除(例如前一帧未被遮挡,后一帧被挡住的情况)
*Temporal——基于时间的。如果了解过TAA应该不难理解。

A-Trous过滤pass(2次)
*A-Trous算法也被翻译为多孔算法。简单来说它比联合双边滤波有更好的性能,但单次运算效果相对较差,需要多帧平摊来得到还不错的结果——且两者都有着不错的边缘保持特性。
*关于分帧降噪在Games202的实时光线追踪一节中有详细介绍,这两种降噪算法都涉及到了。

*通过加倍太阳的尺寸,得到了更柔和的阴影。(以及展示整体的阴影和AO结果)

与场景混合后的结果

一些缺点:
- 一些有遮罩的网格(例如树叶)必须使用其它的阴影算法
- 有透明材质的网格无法采样出正确的阴影和AO值(必须为此发射新的射线)
- 太多渲染pass和纹理采样导致较高的能耗
结语
总的来说,手机版的光线追踪还处于秀肌肉大于实用的阶段,毕竟玩手机其实画质比起帧数或能耗来说反而不那么重要。作为我个人来说,手机游戏都是看看高画质然后开省电玩的,毕竟手机也不是只打游戏的。
但我确实理解为什么他们要尽早在手机上做光追(虽然性价比高的还是前现代光追算法做的反射,而现代光追算法做的阴影和AO手机上有点太耗了),这不仅仅是一个噱头:一方面来说,这套兼容传统管线和简单光追的管线既然适配了手机端,那么完全可以以更高质量应用于全平台(实际上这游戏也有PC端);另一方面,这对于开发人员本身也是宝贵的技术积累,毕竟国内做带有光追的大型3A项目的机会更少,也更难深度定制与优化渲染管线——能在保证项目质量的基础上开展技术升级也是很难得的(反面则是不断练手但是项目烂尾了)。
实际上这个游戏中他们没有发明全新的定制化光追方案,而是一事一议的在UE4的管线、Vulkan新版本API的框架内做了很多优化——与其说他们是“做”光追,不如说是优化光追。很多优化成果也是对前人成果的一些迭代和演进。

图中是一些关联的分享或论文内容
很多时候做手机游戏进行技术突破,就面临一个“为什么要做”的灵魂问题——毕竟常有一类说法是“某某游戏画面那么稀烂反而买量容易、更好赚钱”;但真等到《原神》这类项目憋大招出来靠管线技术突破和规模效应获得了商业成功,再要从头追赶又傻眼了。从我个人的观察来看,腾讯魔方工作室在3D项目的技术积累上确实略好于天美工作室群,可能是因为天美本身有太多躺着赚钱的产品了。
这里还是要复读我的一个观点——3D手机游戏的技术积累不是仅仅积累给手机端的,尤其是对于商业(通用)引擎的技术积累上,这对于整个国内游戏行业都是普适的。
最后是资料链接:
《Ray Tracing in Arena Breakout Mobile》 这篇分享的GDC地址
Games202官方主页
Nvidia的Practical Real-Time Ray Tracing With RTX
一篇写得很好的介绍Unreal引擎光追的知乎文章