[英雄联盟]工程师讲述做场景渲染的全过程

在很多游戏中,我们都可以看到内容丰富而且视觉效果华丽的场景,在美丽的外观背后,这些到底是怎样做出来的呢?在最近发布的技术贴当中,《英雄联盟》渲染团队的工程师Tony Albrecht通过单个场景的方式详细介绍了Riot渲染团队处理场景的全过程:

【Gamelook专稿,转载请注明出处】

Gamelook报道/在很多游戏中,我们都可以看到内容丰富而且视觉效果华丽的场景,在美丽的外观背后,这些到底是怎样做出来的呢?在最近发布的技术贴当中,《英雄联盟》渲染团队的工程师Tony Albrecht通过单个场景的方式详细介绍了Riot渲染团队处理场景的全过程。

他在总结中表示,该游戏一整个场景需要用20万个三角形,其中有9万个是为了做粒子系统、通过695次draw call渲染了2800万个像素,为了达到60以上的fps,所有这些过程都要在16.66毫秒内完成,这还只是GPU端做的事情。所有的游戏逻辑、人为输入处理、碰撞、粒子处理、动画和渲染指令提交也必须在同样短的时间里在CPU端完成,所以,如果要达到300 fps,那就必须3.3毫秒完成以上所有效果。

以下是Gamelook编译的完整博文内容:

我是Tony Albrecht,是《英雄联盟》Sustainability Initiative部门Render Strike Team组的工程师。我们团队的主要目标就是提高游戏的渲染引擎,而且我们对于能够参与这项工作非常兴奋。在本文里,我将详细介绍我们目前正在用的引擎是如何工作的,希望这些简单的介绍能够让大家对讨论我们随后做的改变有所帮助,这也是我写本文的重要原因之一。

我会详细展示《英雄联盟》是如何建造并展示游戏里每一帧的(要知道的是,在高端设备上,每秒帧数是超过100的)。这里讨论的问题是技术层面的,但我希望哪怕没有渲染经验的人也可以知道我在说什么,为了让更多人一目了然,所以文章里的一些复杂的东西会被省略。

首先,简单说下我们可以用到的图形库。《英雄联盟》要在多个平台上尽可能高效率地运行,实际上,Windows XP是该游戏运行人数第四多的操作系统版本,仅次于Win7、10和8之后。每个月有数千万玩家在XP平台玩,所以为了支持这些玩家,我们必须向后兼容DirectX 9,而且只能使用它提供的功能。我们还在OS X版本使用了来自OpenGL 1.5的可比数据,不过这很快就会改变。

所以,我们接下来开始切入正题。首先我们来看看电脑实际上是如何画东西的。

渲染对于新手来说是什么?

大多数的电脑都有一个CPU和一个GPU,其中CPU负责游戏的逻辑和运算,而GPU负责接收来自CPU的三角形和纹理数据,然后以像素的形式把它们展示到屏幕上。小的GPU程序叫做像素着色器,可以让我们影响渲染的过程,比如我们可以改变纹理对三角形的应用,或者指引GPU为每个纹理当中的像素进行运算。通过这样,我们可以简单地把纹理和三角形对应,在一个三角形上增加或者翻倍纹理数,或者做一些更复杂的处理,比如凹凸贴图、光照、反射、甚至是做高保真皮肤着色。所有的可视化对象都被引导至一个屏幕外缓冲区,而这只有在所有渲染完成之后才会显示一次。

01.webp (1)

我们举个例子,图片里是(德玛西亚之力)盖伦的图像,展示了组成他的无纹理模型所用的6336个三角形。这个模型是由我们的美术师创造,并且输出为一个《英雄联盟》引擎可以加载并且使其栩栩如生的格式。需要注意的是,盖伦并没有使用平面投影,因为用语检查渲染的应用比较有限。

除了看起来无聊之外,这个无纹理的模型也很难说清楚问题,更无法传达我们想展示的盖伦的样子,为了让盖伦更加生动,我们需要使用纹理。

02.webp

加载之前,盖伦的纹理以DDS或者TGA文件的形式存放在磁盘上,看起来有点恐怖。一旦把它们正确的贴到模型上之后,看起来就是这样的:

03.webp

到这里,我们总算有了点进展。渲染我们皮肤网格的着色器并不只是做纹理,但这个问题我们稍后再谈。

这些只是基本的东西,但《英雄联盟》除了英雄模型与纹理之外还有很多东西需要渲染。接下来我们就介绍做到下面场景效果使用的完整渲染的全过程:

04.webp

第0步:战争迷雾

在所有的场景都没有画出来之前,我们首先要准备战争迷雾和阴影,战争阴影是由CPU以128×128方格形式存储的,随后增加到512×512。我们然后对纹理进行模糊处理并且放到游戏里以及小地图里比较暗的合适位置。

05.webp

第一步:阴影

阴影是一个3D场景不可缺少的部分,如果没有它们,物体就会看起来四处飘。为了创造一个看起来像是小兵或者英雄产生的阴影,我们需要从光源的视角对它们进行渲染,从光到阴影的距离为每个像素都以RGB组件形式进行了存储,而且我们把alpha组件分离出来,这可以从下图看出来。左侧的图片里是有被围攻的防御塔、小兵和两个英雄的RGB阴影,右侧的图片则只有alpha组件,这些纹理是为了更清晰地展示不同物体的阴影。

06.webp

最后我们模糊阴影,让它的边缘更柔和,这样就做出了一个可以在静态几何体上展示阴影的纹理。

第二步:静态几何体

有了准备好的战争迷雾和阴影纹理,我们就可以画出这一帧里其他的场景了。首先,是静态几何体(之所以这么叫,是因为它没有动画)。这个几何体吧战争迷雾和阴影信息通过本身的纹理结合起来给我们得到如下场景:

07.webp

注意场景边缘的小兵以及英雄的阴影,召唤师峡谷着色器并不会给静态几何体渲染动态阴影。由于主光源没有移动,我们把静态网格预焙(pre-bake)到它们的纹理中,这样美术师们就可以更好的掌控地图,还可以提升游戏性能,唯一产生阴影的就是小兵、防御塔河英雄。

第三步:皮肤网格

现在,我们有了地形与阴影之后,就可以往上面放东西了。首先就是小兵、防御塔和英雄,这些都是需要看起来能够真实移动的物体。

08.webp

每个动画网格都包含一个骨骼和一个三角形网格。每个三角形的顶角都被分布在一到四个骨骼之间,这样当骨骼移动的时候,这些顶角就可以像皮肤一样一起移动,这就是皮肤网格。我们优秀的美术师们为所有东西设计动画和网格,并且在游戏一开始的时候就把它们输出为可以加载进《英雄联盟》的格式。

09.webp

上面的图片展示了盖伦所有的网格,左侧展示的是他的骨骼,而且每个骨骼都是有名字的,右侧的图片中蓝色方块展示的是特定顶角,黄色的线展示的是骨骼以及它们所控制的位置。

皮肤网格着色器不仅可以为帧缓冲区画皮肤网格,还可以向另一个缓冲区渲染它们的景深,这样就可以帮助我们在随后画出轮廓。另外,皮肤着色器还可以做Fresnel和放射光照、计算反射、并且调节战争迷雾的光照。

第四步:轮廓(描边)

按照定义,描边可以让我们的皮肤网格提供更锐利的轮廓,这可以从背景中减少皮肤网格,尤其是在低对比度区域,比如下图,左侧是关掉描边的效果,右侧是开启之后的效果。

10.webp

轮廓是通过之前阶段的景深并且用Sobel滤镜提取边缘得到的,我们还可以把它渲染到皮肤网格中,每个网格都会这么做。还有一种回降法是为GPU使用模版缓冲,但无法一次性渲染多个物体。

第五步:草丛

为了说明水和草丛的渲染,我们来看看另一个加入了这两种元素的场景。

这一帧图是没有水和草丛的,只是静态的背景地形以及一些皮肤网格。

11.webp

需要注意的是,草丛的阴影已经以静态地形的方式加入进去,而且我们不会对其进行动态渲染。接下来我们加入草丛:

12.webp

这里加入的草丛实际上是皮肤网格,这样当你的英雄走到附近的时候,草丛就会随着移动,还可以在召唤师峡谷的风吹下出现摆动效果。

第六步:水

在加入了草丛之后,我们用带有波纹动画的半透明网格对水进行渲染,然后增加睡莲、石头旁边的涟漪、河岸以及一些生物,所有这些物体动画都是为了让场景看起来栩栩如生。

13.webp

为了强调水的效果,我们保持了水的透明度并且忽略了水下的静态几何体,这可以突出水面效果,带来更好的体验。

增强涟漪之后的效果:

14.webp

这里我们可以清晰地看到河边以及石头旁边喝睡莲边的水面效果。

当正常渲染和动画之后,水面效果看起来是这样的:

15.webp

第七步:贴图

当水和草丛加入之后,我们开始加入贴图,也就是置于地形之上的简单纹理几何体,比如下图中的防御塔攻击范围图。

16.webp

第八步:特殊轮廓

17.webp

这里我们要处理的是在活动或者特殊状态下被鼠标触发的一些更厚的轮廓,就像下图展示中的那样,这些做起来基本上和我们前面做皮肤网格一样,但我们对轮廓近一步模糊,这样它看起来更厚。这些高光区域还可以更加突出,因为它们可以在随后的渲染过程中进行,而且可以覆盖现有效果。

第九步:粒子系统

这个阶段是最重要的,也就是粒子。每一个技能、buff和效果都是一个需要进行动画和更新的粒子系统,这个特殊的场景可能表现的东西没有5v5团战那么多,但仍有很多粒子需要展示。

如果去掉背景场景只看粒子的话,我们得到的是这样的场景:

18.webp

对组成粒子的三角形用紫色轮廓渲染(没有纹理,只是几何体),我们可以得到如下效果:

19.webp

如果我们把粒子做到正常的话,就可以看到更具识别度的场景了。

20.webp
第十步:后期

在完成了场景核心部分的渲染之后,我们可以给所有东西做一些优化。我们做优化也是分几个步骤的,首先我们做一个抗锯齿(AA)通道,这样可以避免粗糙的边缘,让整体画面看起来更干净。这是在一个静态图像里的微妙效果,但是放到高对比度边缘移动屏幕的时候,会有很大的帮助。对于《英雄联盟》来说,我们使用的是快速近似抗锯齿(FXAA)算法。

21.webp

左侧图是FXAA之前的小兵,右侧是处理之后的,请注意边缘变得更加柔和了。

一旦FXAA通道完成之后,我们开始做伽马校正通道,这样我们就可以调整场景的亮度。为了进一步优化,我们最近还为伽马通道增加了死亡界面饱和度效果,这样就不用在英雄死亡的时候移除所有当前的可视化网格着色器了。

第十一步:伤害与HP条

下一步我们要渲染的是游戏里的指示物,比如HP条、伤害数字、屏幕文本以及所有全屏非后期处理的效果,比如下图中的伤害效果。

22.webp

第十二步:HUD

最后,用户界面终于完成了。所有的文本、icon和物品都在屏幕上单独画出来,覆盖了背后的所有东西。在我们分析的这个案例中,我们大概使用了1000个三角形绘制HUD,其中300用于小地图,另外700是其他区域。

23.webp

把以上所有步骤加起来的话,整个场景用了20万个三角形,其中有9万个是为了做粒子系统。通过695次draw call渲染了2800万个像素,所有这些过程都要做到尽可能快,确保游戏可以玩。为了达到60以上的fps,所有这些的都必须在16.66毫秒内完成,这还只是GPU端做的事情。所有的游戏逻辑、人为输入处理、碰撞、粒子处理、动画和渲染指令提交也必须在同样短的时间里在CPU端完成,所以,如果要达到300 fps,那就必须3.3毫秒完成以上所有效果。

为何要重构着色器?

讲了这么多,可能很多人应该大致了解了《英雄联盟》一帧渲染所涉及的复杂性。但这还都是输出方面的事情,你在屏幕上看到的是我们渲染引擎上的数千个功能指令的结果,而这个引擎必须不断改变和进化来满足我们的需求。这就导致了《英雄联盟》代码库出现了不同风格的渲染代码,我们写新代码的时候还必须维护老的代码。比如召唤师峡谷的渲染方式就和虚空哀嚎以及扭曲丛林略有不同。

有些着色器只能满足老版本要求,而另一些着色器则从来没能发挥全部潜力。Render Strike Team团队的工作就是接管所有的渲染代码并且重构,这样所有的渲染都可以在同一个界面进行。如果我们的工作做的好,那么除了一些地方的运行速度更快之外,你们应该感觉不到太大的变化。不过,一旦我们完成了所有的重构工作,就可以给《英雄联盟》不同模式做大量的提高。

我希望介绍《英雄联盟》渲染过程的这个博客能够给你们提供有用的信息。

如若转载,请注明出处:http://www.gamelook.com.cn/2017/01/280474

关注微信