前言
这篇接着上周的上半部分,继续来读Tango在2024GDC大会上的分享。
由于会介绍到很多引擎中实现光照和阴影的细节,对于完全不了解游戏引擎的人,可能会惊奇的发现——原来阴影不是打上光自动的就算出来的;对于有过游戏开发经验的人来说,如果不用开发卡通风格的环境,其实也不必深入定制光照系统的方方面面。因而看看他们如何来定制修改光照系统可能也会有打开黑盒的效果。
开始之前插进来一小段谈谈环境光遮蔽(AO)与阴影的关系。其实两者都是光传播中的暗处表现,原则上如果通过物理法则来计算,两者是一致的,即受光越少表现为越暗。提出AO这种近似主要用来解决靠近物体表面及相邻部分的阴影,且这种相邻阴影是充分考虑环境间接光照后的归纳结果;而所谓的阴影往往用来表现物体挡住光源产生的轮廓,因此对光源的强度和数量都有一定的依赖,并且整体实现的计算量会大于AO。所以后面的介绍中会看到一些近似阴影的方案。
后面的内容还是以翻译原文为主,打星号的则是我自己的备注和评论。 文中有大量的Volume这个词被翻译为“体积”,其实需要理解为“能表达一定范围的体积区域”,这一点后面就不反复解释了。
1 卡通风格光照
*这部分介绍了经过定制化的一些实时光照组件。这里的光照可以理解成——以定制化的轮廓照亮指定的区域或物体表面,所以其实是一组着色方案,不涉及产生阴影。
*最终他们选择了前向光源和贴花光源为主,其中前者都是用的引擎中的元件,所以提到的不多。

主光源(光、影)
——Hi-Fi RUSH的卡通着色使用一个简单的两色调光影着色模型。全局的主光源是以UE4的前向渲染的基础pass,而非延迟渲染的pass来计算的。
——在开发的后期阶段,我们开始利用UE4的渲染指令来进行数据传送和渲染pass的回调和线程的委托,这使我们能实现源生的游戏内渲染pass(原文是original render passes game-side,这里应该是能开发自定义渲染pass的意思)。现在我们对于如何做UE4定制化修改有了更多经验,但这一认知也是通过实现了更多核心技术后积累的。
*现在的主流主机游戏管线往往都是延迟和前向结合的,可以兼容两种管线的特性。这一段及后面的一段内容主要解释了为什么选择前向主光源方案,至于上一篇提到的其它渲染特性大多数还是用的延迟渲染管线里的。

使用延迟渲染主光源的情况
——最终我们需要支持2个主光源,并且把卡通风格的后处理与主光源关联、最终汇入延迟渲染pass的方案也在性能上有所不足。对于延迟和前向渲染的光照进行混合的方案可能也是我们可以改进的方向。
*这一段我的理解是当时延迟渲染的可定制程度不支持他们的风格(因为风格化部分要通过对光照区域后处理来实现,因此无法通过体积后处理来做光照),为此风格化光源必须通过前向渲染来实现,但这又会增加性能开销;到了需要室内室外2个主光源的情况,这一复杂度就指数级提高了。不过最终他们似乎并没有提到对此的优化和改造。

可放置的卡通风格光源
——在前向主光源之外,艺术家可以使用点光源或聚光灯之类来实现局部环境光照。

卡通光源(前向渲染光源)
——对于可放置光源,基于和主光源一样的原因我们选择了前向渲染光源。很多小开发团队可能对此很挣扎,不过多大程度上能对UE4进行定制开发是(进度和可行性上)安全的是我们开发早期也很挣扎的一点。
——最初我们试验了前向光源,不过对于延迟渲染中的贴花光源其实更易于优化,最终也对艺术家更有用。
*本页中也解释了,选择不需要修改引擎的前向光源也有疫情期间的考虑,以及最终贴花光源成了有效且强力的扩展方案。

卡通光源(贴花光源)
——添加一个有着切断式衰减(cutout light attenuation)的贴花光源可以使场景通过3D灯光来照射同时又不看起来像3D。
——贴花光源支持任意的切口纹理。我们的艺术总监准备了7种纹理样式来使贴花光源在不同的游戏场景种有丰富的变化。

贴花光源以传统光源的方式渲染
——贴花光源是延迟渲染光源,因此当贴花光源被放置在场景中时,他们是以传统的球体或锥体的其它延迟光源的方式来运算的。

贴花光源渲染使用贴花体积
——贴花光源,基于其名字,它的特殊性就是可以指定一个贴花体积作为范围来以贴花的形式渲染光照。
——场景艺术家可以通过传统方式来设置贴花光源,并且他们可以为不同的光源指定不同的贴花体积。

使用贴花体积作为优化
——投影到一个贴花体积的一个明显好处是它渲染了相对更小的一片范围,因此有着更好的性能。

为墙壁添加贴花光源
——除了提高性能外,贴花光源也为艺术家提供了为指定局部进行光照的更高的控制力。
——例如,我们可以分别为地面和墙壁指定不同的贴花光源(图中纹样不同)。

阻止漏光
——在《恶灵附身2》中,我们的PBR光源可以使用光裁剪体积(clip volumes)来阻止点光源之类的漏光到隔壁房间。
——贴花体积也可以起到类似的阻止漏光的作用。

角色如何呢?
——在Hi-Fi RUSH中,角色光照和环境光照是分离的系统。这意味着前向光源与贴花光源都不影响角色。(角色的光照着色上一篇介绍过了)
*整体看来,在风格化渲染中光照更多的是服务于艺术表现而不是其物理特性,所以会比传统BPBR渲染管线还拆解得更细一些。
2 实时阴影
*游戏引擎中对于主光源的实时阴影其实是可以通过配置主光源直接实现的,但这里基于性能和表现考虑做了拆解,且不止一种层面。

——环境和角色有合适的阴影即使对于卡通风格的渲染也是很重要的。
——如果无策略的采用阴影,性能开销会变得很高。在UE4中,阴影可以看起来质量很高但开销也很大。
——为了平衡质量和性能,我们对于如何渲染阴影非常仔细谨慎。

卡通光源不能投下阴影
——我们出于对性能的考虑我们限制了主光源的阴影投射。卡通风格的光源无法投射阴影。
*这里主要指光照渲染的物体范围不能作为阴影渲染的物体范围,否则性能和精度无法平衡。

多层阴影纹理(动态阴影)
——对于户外环境阴影,我们使用UE4标准的多层阴影纹理(cascaded shadow maps)。
——我们有自己的一套预烘焙阴影纹理系统,用于表现部分平行光的阴影以提升性能,但对于场景的大部分,我们需要动态多层阴影纹理能提供给我们的质量。
*采用多层阴影纹理主要是让阴影在较大和较小的尺度都能有不错的精度,但是这个范围也不是无限的,代价也是可观的。

只投射阴影的光源(动态阴影)
——对于多层阴影纹理之外的动态影子,艺术家需要使用只投射阴影的光源(shadow-only lights)。
——由于我们是2D卡通风格,因此光照和阴影分别有不同的多个光源也不是问题。

玩家的Shadow-Only光源
——玩家角色被分配了一个额外的shadow-only光源。在户外区域,玩家角色不是通过多层阴影纹理,而是通过这个单独的光源来渲染。
*这个方案既可以保证玩家角色阴影的精度,还可以只为这个阴影做风格化处理(上一篇提到的基于有向距离函数划线的样式)。

角色的Shadow-Only光源
——巨型BOSS也被分配了单独的shadow-only光源来使其看起来富有戏剧性。这一页展示的阴影光源就只为BOSS投下阴影。

从暗到明的风格化渐变
——玩家阴影和部分烘焙阴影纹理是通过风格化的由暗到明的过渡来进行绘制(图中的框)。
——风格化的线在阴影距离投下阴影的物体较远时变得更细。

胶囊阴影
——非玩家角色使用更高性能的胶囊体阴影作为其阴影方案。
——胶囊体阴影是UE4的标准功能。艺术家预先制作了能预估角色轮廓的胶囊体,它们被用来预估并逐像素计算主要的方向光源投下的软影子。
——这对于照片质量真实的游戏是不合适的,不过由于我们是卡通风格,因此可以接受这种斑点状的阴影。

胶囊阴影可以在任意地方投影
——由于胶囊阴影不需要额外的阴影投射灯光,因此我们能得到与场景光照条件无关的,室内室外都合适的次要角色阴影。

AO体积的贴花阴影
——胶囊阴影对于高空中、体型小的角色会投下过于大且分散的阴影。
——对于这种角色,我们实现了一个简单的基于AO体积的贴花系统。CPU射线检测被用来寻找空间中合适的贴花位置。

环境的Shadow-Only光源
——最后,我们还允许引入一个shadow-only的聚光灯用于环境物体的投影。
——对于图中的场景,移动的巨型机器手臂就是被这种灯光投影的。
*这里提到的还都是实时影子,已经用了一种内置方案和额外的3个补充方案。游戏渲染本身就是一些巨大的trade off,即使视觉上差不多的东西做法也可能完全不同,性能可能差十倍不止。
3 静态烘焙阴影
*静态烘焙阴影可以理解成预处理一些阴影计算需要的数据,但不完全是指烘焙成最终的纹理。

静态阴影纹理
——(传统的)静态阴影纹理是指离线烘焙的阴影纹理。它们在性能提升和艺术家易用性两方面都非常重要。
——UE4的烘焙阴影纹理被整合到Lightmass烘焙中(烘焙光照系统),不过我们为了更好的整合性,通过卡通风格的延迟渲染管线创造了我们自己的阴影烘焙系统。

离线烘焙了哪一部分?
——阴影纹理的组成通常包含两步骤。
——第一步生成一个光源作为透视摄像机的深度纹理。
——第二部通过着色点(像素)在光源空间的深度(通过空间变换),和第一步的深度纹理中对应位置的值比较来产生实际的阴影。
——我们预先烘焙的是第一步的深度纹理。
*实时阴影计算的原理之前的其它文章也介绍过。当然这只是基础思路,后续有各种方案来处理失真问题、精度问题和软影子问题等。

静态阴影纹理Actor
——通过在场景中放置静态阴影actors(UE中的基本物体单位),艺术家可以应用静态阴影纹理。
——这些actors由两部分组成:一个负责拍摄深度纹理的摄像机,和一个负责渲染阴影纹理的延迟渲染体积包围盒(a deferred box volume for rendering the shadow map)。

最大化阴影纹理覆盖(对指定范围的)
——艺术家使用屏幕右下角的深度纹理预览来手动最大化深度纹理的覆盖,以提升阴影质量。
*如果依赖引擎自动化的阴影,覆盖范围往往是引擎通过算法动态估算的。另外,烘焙的就是图中这个纹理了。

计算屏幕空间阴影纹理
——通过比较着色点在光源空间中的深度与烘焙深度纹理中的深度值,就可以渲染出指定的贴花体积范围内的阴影纹理了(以屏幕空间阴影纹理的方式)。
*屏幕空间渲染,可以理解成对指定范围内的像素进行逐像素运算得到的渲染结果。这里指定一个贴花体积作为范围主要是性能优化考虑。

阴影纹理抗锯齿
——未进行抗锯齿的阴影纹理会有明显的锯齿感。为此我们采用了4X4的PCF(Percentage Close Filtering)作为阴影纹理的抗锯齿方案。
*PCF可以理解成一种把周围几个像素的权重进行混合的过滤器(Filter),具体算法参考文末资料。

静态阴影纹理布置
——这里是是一张展示了静态阴影纹理布置的截图。许多覆盖了一定小范围的静态纹理摄像机被放置在关卡中。

不同的阴影纹理摄像机
——即使图中的很小的把手也有单独的静态阴影纹理。

静态阴影纹理预载
——使用了静态阴影纹理不仅是基于艺术设计的考虑,也是处于纹理预载(streaming )效率的需要。我们的首要关切点是内存用量和预载故障(streaming hitches)方面。
——我们通过监控纹理尺寸来管理静态阴影纹理预载的工作量,并且关注在如何在多帧之间分配这些预载的工作量。为此,我们不能接受单个过大的阴影纹理。
——我们在预载队列中通过设置每帧内存限制来平均分配预载的工作量。
*静态阴影纹理这一步更多的就是场景美术的体力活了,要在美术表现和性能之间做一个取舍。
4 全局光照
*这里的全局光照主要基于预采样的一种补充光照组件,另外提到probe探针就是基于球谐光照(可能结合CubeMap或3D纹理)的低频光照技术——低频指无法反映出信号源剧烈变化的部分。至于使用方式和光照覆盖的范围,后面会提到。

包含了全局光照的卡通渲染
——我们认为支持标准3D灯光要素例如全局光照(GI)是很重要的(图中的数据球及周围)。

采用什么方案做GI呢
——在项目初期我们准备尝试UE4的Lightmass组件,因为它带来的烘焙光源有着很高质量,我们非常满意。
——但同时,我们也考虑到使用Lightmass的光照纹理会为艺术家带来处理光照UV(的难度)和较长的烘焙时间,这些因素是有着较高的时间成本的。
——高质量的卡通风格资源需要艺术家不断手动调整,因此场景编辑在整个项目的过程中会是非常忙碌的工作。
*这里主要就谈到质量与协同工作便利性的取舍,实际上这一取舍在工业化游戏开发中会不断面对。

让我们定制化UE4的体积光纹理
——我们发现(单独)使用Lightmass中的体积光照纹理(volumetric lightmaps)对于环境光照也很有用,它可以有更快的烘焙速度且不需要光照纹理的UV。
——我们决定定制化体积光照纹理的功能,聚焦在提高烘焙迭代的速度和易用性上。

局部体积GI光照
——一个允许我们这么定制化修改的大因素是我们的游戏有着卡通风格的艺术方向,这意味着我们可以艺术地(artistically)选择地图上哪些部分需要全局光照。
——在这一页图中,只有窗户周围一小块区域设置了全局光照,(即使如此)对于整体画面也足够了。

GI烘焙限制在需要的区域
——这里展示了同一关卡中GI光探针调试显示(debug display)开启的样子。
——场景艺术家决定在较强的散射性的物体(strong emissives)周围烘焙光探针,不过对于有着强光源照亮的区域则不烘焙探针。

世界空间中的体积光源
——UE4有一个称为Lightmass重要性体积(importance volume)的内部功能,它能使光探针烘焙限制在指定的范围内。
——我们修改定制了这一功能以实现我们自己的全局光照体积actor。
——在UE4的vanilla中,光探针数据是逐关卡存储的;在经过我们修改后,在我们的游戏中光探针数据是逐actor存储的。

优化体积光纹理数据
——另外必须提到的是,我们优化了UE4的体积光照纹理的数据以节省内存(图中),因为我们只需要无方向性的全局光照信息。
——我们把单个光探针球谐纹理(spherical harmonics 3d textures)从8减到1。这节省了很多内存。

世界体积光烘焙流程
——对于前面我强调过的烘焙迭代速度和易用性等关键点,我会快速的展示一下GI烘焙工作流的总览。
——首先我们通过一个GI光照体积来围住需要添加GI的区域。

世界体积光烘焙流程
——这之后,艺术家选择菜单中的Build Selected Volumes功能。
——在定制化的烘焙代码被执行后,GI被烘焙到限制的区域中。
——烘焙时间依赖于Lightmass设置和PC的性能,不过总的来说对于标准的开发PC来说,一次GI烘焙只需要1-2分钟时间。
——由于烘焙被修改成针对单一actor的,艺术家可以快速调整迭代指定区域的烘焙结果。

世界体积光烘焙参数
——GI光体积同时包含了烘焙和渲染时的参数。
——一个常用的参数时细节单元尺寸(detail cell size),它能控制光探针散步的粒度。
——我们的场景艺术家使用这一参数来平衡质量和内存用量。
*整个这一节主要在说trade off的事,既有质量和性能的权衡,也有绝对质量和易用程度之间的权衡。
结语
在原文章的PDF中,在现场版分享内容之后他们还补充了4个短课题,分别是GBuffer Stencil 、Volumetric Fog 、Toon Lensflare 、GPU Physics Simulation。由于篇幅原因这里无法包括在内,有兴趣的可以去找原文来看看。
最近刚好听到《游戏帝国 第二季 EP6》,提到了EPIC创始人对于引擎和开发效率之间的关系。其实从游戏行业进入需要协同开发阶段以来,这就是项目规划和管理的重要一环了。从这篇分享中的很多方面也能看出,实际游戏开发中甚至都不是用的引擎中的内置最先进的功能,因为还有易用性和性能之类很多维度需要考虑。
另外一个点其实他们可能提到的不多,但确实他们的很多人员是从恶灵附身和幽灵线东京团队中逐步成长起来的,无论游戏卖的怎样,在UE4的技术积累上他们已经达到了相对黄金的阶段。这也是我对于微软砍掉这个工作室经济上不理解的地方,毕竟这种项目团队积累不是说随便招几十个人就能很快得到的,这时候放弃等于团队建设和培养成本全放弃了。
最后还是资料链接:
GDC Vault的《3D-Toon-Rendering-in-HiFi Rush》
介绍Percentage-Closer Filtering的一篇文档