现代游戏引擎架构
原文出处:现代游戏引擎架构
引言
自从人类文明诞生以来,就一直在探索着人类文明的精神世界,于是就纷纷诞生了许多经典的文学巨著、艺术绘画、音乐戏剧等各种传统的艺术形式。传统的艺术形式可以说极大丰富了人们的生活,但是随着20世纪一场新的革命浪潮-计算机的普及,计算机的问世可以说再一次极大丰富了人们的生活,人们可以在网上购物、可以远程视频、可以快速获取很多的有效信息,人们对计算机带来的技术改革好评如潮。
但是对计算机派生出的电子游戏可谓是饱受争议,很多家长更是直接把电子游戏列为电子鸦片,确实不可否仍电子游戏让部分青少年沉迷网络世界荒废学业,但是科技本身是向善的,电子游戏本身就可以结合我们几千年来的传统艺术形式(绘画、戏剧、建筑、文学故事)等等,如果把握好理念电子游戏本身也是可以作为互联时代下派生出的另一种艺术形式。从马克思的辩证唯物主义思考,电子游戏对立面之间的相互斗争是促成新事物否定旧事物的决定力量,我们只有把握好这种对立统一的关系才能不停发展。假以时日,游戏或许可以成为被社会认可的一种艺术新形式。
游戏与艺术的碰撞,《彩虹坠入》黑白光影走入艺术殿堂
中国开发团队NEXT Studio的黑白光影童话独立
提及到电子游戏就不得不提到游戏引擎,“游戏引擎”这个术语在20世纪90年代中期形成,与id Software开发的爆款第一人称射击游戏(first person shooter,FPS)-《毁灭战士》《DOOM》有关,当时其架构就清晰地的划分了核心软件组件(如三维图形渲染系统、碰撞检测系统等)、美术资产(art assets)、游戏规则(rule of play)等,这样就只需要对引擎作出很少的修改,小规模的游戏工作室就可以创作新的游戏[1]。
毁灭战士(DOOM)系列,是由id Software开发的第一人称射击电子游戏系列,在电子游戏界中被普遍视作第一人称射击游戏的开拓者之一。
游戏引擎的研发也大大的促进了各行各业,现在如火如茶的元宇宙、数字孪生、智慧城市大多是结合游戏引擎来研发的,游戏引擎可以说是涵盖了很多前沿科技领域,也正是因为如此一款成熟商业化游戏引擎的研发是一个难度很大系统性工程。在参与虚幻引擎结合地理信息技术打造智慧城市数字平台的时候,我就感觉到了引擎的代码量如此庞大、系统框架如此难以梳理,当时我就在想搞出UE的团队到底克服了多少困难才做出一款这样成功的商业化游戏引擎。当然不排除有的大佬对UE源码框架很熟练信手拈来,但是我作为和很多新手小白一样接触到虚幻引擎技术也就是2021年研二负责城市级复杂场景的渲染优化时候,当时看到UE几百万的源码的时候觉得自己无从下手,另一方面对图形学和实时渲染毫无所知,当时感觉我的这个研究生课题遥遥无期。好在我自己对这个虚幻引擎结合GIS搞智慧城市很有兴趣,我就去学了这些相关的课程,慢慢也就感到其中的魅力所在。这一路程中我要特别感谢那些细心回答我问题的人,他们没有对我提出的幼稚问题给予打击而是耐心贴切的回答我的疑问and提供学习资料。这也就是我为什么会在知乎上写一些文章的原因,我知道自己的水平还有很多要学习的但是我感觉对我来说学习的最好方式就是和别人一起分享交流进步,深切体会到讲的明白+经得起推敲=真懂。
游戏引擎结合CityEngine生动再现巴西贫民窟
用数字孪生和游戏引擎的阿德莱德虚拟克隆
军事仿真,尤其是在现代化军事中这个越来越重要。
1、游戏引擎基础架构
游戏引擎代码量惊人,毫无疑问是一个大型软件系统。正如所有的大型软件系统一样,游戏引擎要做到避免software layer系统间的复杂耦合,就是说我们尽量不要让下层的系统如操作系统层去依赖上层基础系统如渲染模块,通常我们的基础上层可以依赖下层,但是当下层依赖上层时就会导致(circular dependencey)。所以像这种下层的代码如Operate Systerm、Core Systerms一般都是由大佬写的需要反复权衡,像我这种虾米就改改上层的代码就可以了。
按照Jason 大神在Game Engine Architecture一书中提及到的游戏引擎通常由运行时组件和工具套件两部分构成[2]。其中典型的三维游戏引擎主要运行组件如下(What else can I say,Sea of Codes):
What else can I say,Sea of Codes
确实看这张框架图就要劝退一大波人啊,又是操作系统又是硬件设计还有渲染、物理碰撞、动画仿真等等。其实大可不必去细细了解所有details,先有一个宏观的逻辑架构然后当你需要研究那块时再去细细专研。和所有的大型软件设计一样,游戏引擎也是采用了分层架构,可以大致分为:Platform Layer、Core Layer、Resource Layer、Function Layer 、Tool Layer[3]
分层架构
1.1、Platform Layer
游戏引擎通常需要运行在不同的平台上从而覆盖最大的市场,不同的平台又有不同的Operation Systems、Platform File Systerms 、Graphics API(引用程序接口 -application programming interface,API)、Plateform SDK(第三方软件开发包- software development kit,SDK)。

平台层(Platform Layer)的本质就是你在引擎上写核心代码时可以无视平台的区别(Windows、Linux、macOS等等),把平台的差异性掩盖这是现代游戏引擎非常核心的功能。如果没有这一层平台层,我们做一款跨平台游戏的时候就会很崩溃,比如你会遇到很多不一样的Graphics API如DirectX12、Monterey、OpenGL、Vulkan等等。所以在现代游戏引擎平台层中有一个硬件渲染接口RHI(Render Hardware Interface),他重新定义一层图形API把各个硬件的SDK区别把它封装起来。RHI 是图形 API 和渲染器之间的抽象层。它允许渲染器完全独立于 API[4]。

这个里面的原理细讲的话是很复杂的,但是可以简单理解为上层的开发人员只需去写核心代码不必负责底层的,因为RHI本身又用c++的虚函数重写了一遍所有的API来实现跨平台。
1.2、Core Layer
Core Layer是什么意思呢? 可以简单的理解为像游戏引擎这种大型软件系统,都需要核心层提供一些有效的功能使用程序(utility),它们需要满足超高的性能设计以及高标准的编码。如下图所示一些常见的功能如:自定义的数据结构与算法、内存分配、数学库等等。
Core Layer - Foundation of Game Engin
a、自定义数据结构与算法
一般游戏引擎都会有自己的一套工具去管理基础数据结构(Vectors, maps, trees, Customized outperforms STL)以及算法(查找、排序等),减少动态内存分配(比如c++标准的STL提供的典型容器Vector:已经被_分配_出去的的内存空间大于请求所需的内存空间and会分散的很开)从而来避免内存碎片化(Avoid FRAGMENT Memory,内存碎片即“碎片的内存”描述一个系统中所有不可用的空闲内存)。
Core - Data Structure and Containers
b、内存分配管理
回收内存时,将内存块放入空闲链表中; 因内存越分越小,内存块小而多;当需要一块大内存时,尽管此时空闲内存综合可能满足需求,但过于零散,没有一个合适的内存块。面对内存碎片导致的负面影响,游戏引擎都会有自己自定义的内存分配系统,以保证高速度的内存分配及释放。在游戏引擎中我们预先申请了一大块内存由我们自己管理数据资源分配(比如Tree、Array等)从而来追求最高的效率。
游戏引擎本身也可以看做是一个抽象的图灵机,要高效的分配它的内存管理基本上可以从图灵大神的简单逻辑走起,要高效实现图灵机的高效读取,我们可以指定以下的规则:1、尽量一次性申请内存的时候大一点(也就是尽量把数据放在一起);2、读数据的时候最好是依次连续读取;3、删除数据的时候一块一块(block)抹掉不要一个一个。
图灵机
当然还有计算机系统的虚拟内存技术,它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
虚拟内存技术
1.3、Resource Layer
每个游戏引擎基本都有自己的资源管理方式,提供一组统一的接口去加载不同类型的Resource。我们知道渲染一个简单的3D模型需要Mesh和Materail,但是Mesh又有不同的数据来源比如OSGB、3DMAX、MAYA、Revit、BIM等格式,我们不可能在游戏引擎中把这些数据格式去逐个加载打开,一般都会有一套自己的工具把它们转换为自己定义的数据资产格式.assert。这样我们就可以实现不同的Resource到asserts,从而能够减少大量冗余的数据大大提高效率。
资源管理层
以虚幻引擎为列,UE本身研发了一套Datasmith的工具用来各种数据转换为UE自定义的Uasserts格式,这种非破坏性、高保真的转换使其不会损失核心数据。其实数据格式转换这一单方面还是有很多技术的,如果大家有兴趣的话可以去研究一些虚幻引擎中的Datasmith源码[5]。
UE本身研发了一套Datasmith的工具用来各种数据转换为UE自定义的Uasserts格式
当Resouce Importing引擎化变成自己的资产以后,我们引擎中有数百万的的资源这时候如何使Mesh、Materail等对应起来来渲染我们指定的物体呢?在现代游戏引擎中最重要的就是数据资产之间的关联,在游戏引擎中(如UE)一般是由GUID(Globally Unique Identifier,全球唯一标识符)来进行资产关联的。
在现代游戏引擎中最重要的就是数据资产之间的关联,在游戏引擎中(如UE)一般是由GUID(Globally Unique Identifier,全球唯一标识符)来进行资产关联的。
资源层进行导入并且关联后,我们要对不同的资产进行生命周期管理,一般有垃圾回收机制(GC,Grabage Collection)等等。这个机制在现代游戏引擎中也是非常重要的,如果做的不好我们在一个关卡切换至另一关卡时的这一帧就会突然卡住。
1.4、Function layer
游戏引擎的功能层极其庞大、无所不有。比如渲染、资产的管理等等,后面我们也会针对游戏引擎的渲染框架做一些深入的探讨。我们先讲一个简单的大逻辑——Diveinto Ticks。

现代游戏引擎每执行一次Tick的时候系统就会把所有该做的事情做完(可以简单理解为Trick使虚拟世界动起来了),我们在33ms内把整个世界的logic和Render都跑了一遍。
Dive into Ticks
1.5、Tool layer
工具层的理念就是Allow Anyone to Create Game,让大部分人只需修改一些代码就可以表达自己的创意。我们有各种编辑器,以虚幻引擎为列:有关卡、蓝图等编辑器,这一部分的代码量也是相当大的。
Allow Anyone to Create Game
游戏引擎的输入数据形式形式广泛,列如三维骨骼、纹理贴图、动画数据、音频文件等,这些数据源基本都由美术人员由数字内容创作(Digital Content Creation,DCC)软件制作。比如Autodesk公司的Maya和3ds Max通常用来制作三维网格数据及动画数据,Adobe公司的PS及其家族成员用于创作纹理,Sound Forge是制作音频的流行工具。

2、虚幻引擎实时渲染框架(Real-Time Rendering in UE4)
面对这个庞大的分层体系,你要是想精通每一层级的理念和前沿技术那是不可能的(除非向天再借五百年),像渲染(Rendering)、碰撞及物理(Collision & Physics)、动画(Skeletal Animation)等中的每一个都是一个大方向,其中又可以细分很多小方向作为研究课题。。。因此我在这里主要概述的是虚幻引擎实时渲染框架(因为对这个有所了解)。
2.1、实时渲染基础
实时渲染是混合了多种不同解决方案的复杂过程,从本质上可以将其看作两个阶段:预计算阶段和具体的实时渲染阶段[6]。
在具体的讲解实时渲染的整体流程之前,我们有必要去了解影响实时渲染的一些因素(在学习一个新的知识之前我觉得闫令琪的why、what、how学习三步法值得借 鉴)。
a、主要关注CPU和GPU这两个的渲染时常,效率最低的一方通常会决定其性能(在UE4中可使用stat unit命令)

就如我们在游戏引擎架构-Function layer所讲的,游戏引擎有一个简单的大逻辑那就是Drive into Ticks,现代游戏引擎每执行一次Tick的时候系统就会把所有该做的事情做完,可以简单的分为Ticklogic( )和Tickrender( )。
从上图Real-Time Rendering Fundamentals中可以看出,虚幻引擎的大逻辑也可以分为CPU处理的逻辑运算以及GPU负责的渲染计算,两者并行(可以用stat engine、stat scenerendering命令查看)。
b、绘制调用(Draw Call)
Draw Calls绘制调用是实时渲染引擎的渲染方式,它是逐对象渲染而非逐三角面渲染。所以如果场景中Mesh数量很多,即使每个Mesh的网格体面数量很低,渲染消耗依旧很大,这比渲染一个三角面片极高的Mesh慢得多(列如就是在渲染一片草地时,有几十万颗草要渲染每颗草或许就每个10来个面,是比渲染一个几千万个面的精细模型慢的多,因此推出了ISM和HISM来优化草地的渲染,对于同一种实列instance的草只需记录他们的偏移量,调用一次DrawCalls就可以渲染同一个实列instance的上万颗草,从而来优化渲染)。
采用HISM来优化草地的渲染
2.2、实时渲染的整体流程
在具体的GPU实时渲染之前,CPU实际上一直在计算。在Frame 0帧,CPU在计算所有的逻辑和变换,因为我们在渲染之前需要明确存在哪些物体、还有其所在的位置。然后在接下来的Frame 1帧就是绘制线程Draw Thread,这一步主要是完成可视性处理比如视锥体剔除、距离剔除、预计算可见性、遮挡剔除(这一详细讲解可参考我的上篇文章-Unreal Engine里面的可见性和遮挡剔除)。GPU的后两帧开始实时渲染。因此在按下按钮后到CPU计算的结果反应在游戏之间存在两帧的延迟。


(1)Ahead Of Rendering——Frame 0
首先CPU进行计算,计算内容包括所有逻辑和变换,因为知道一切对象所在的位置是渲染对象的前提。
Ahead Of Rendering——Frame 0
(2)Visibility Features——Frame 1
然后CPU和部分GPU会计算遮挡过程,剔除所有不可见对象,将所有可见对象记录在一个列表中。剔除方法包括 距离剔除、视锥剔除、预计算可视性、遮挡剔除 。
Visibility Features——Frame 1
2.3、Start Of Rendering—Early Z pass
GPU开始实际的渲染过程,但是在正式渲染几何体之前,还需要处理另外一个问题那就是模型的渲染顺序。因为模型是逐对象渲染而不是逐像素渲染,所以当模型对象产生大量重叠时会产生大量的像素冗余。而解决的办法就是利用一个前期的Early Z pass来进行深度测试,这样每个像素只记录离它最近的对象就可以了。
Early Z pass
2.4、BasePass-基础通道
BasePass最重要的基础通道,也是最大的渲染步骤之一[7]。基本通道是指它穿过的初始渲染通道,它的作用是渲染几何体的所有部分(Meshes和Materials)。首先它会确定所有的3D对象如(Static Meshes、HLOD等),所有的这些对象都会成为一个个的绘制调用Drawcall。
对于在基础通道里面进行的几何Meshes渲染,我们已经知道Drawcall是逐对象渲染的,但是如果真的一次drawcall只渲染一个对象的话那渲染效率是极其低效的。所以虚幻引擎中默认采用了动态实列化-Dynamic instancing,就是说相似的Mesh网格体(或者是共享同一个材质的所有实列)可以一次drawcall批量渲染。Drawcalls所产生的消耗远比多边形面数要大得多,因为每渲染完一个drawcall,计算机就需要停下等待下一条渲染指令,而这之间的停顿时间就成了性能消耗的最大因素。
对于在基础通道里面进行的材质渲染可以借助光照贴图LightMaps,光照贴图来自一个UE的一个独立应用Lightmass模块[8],当你构建光照BulidLighting的时候将会启动Lightmass系统的代理应用SwarmAgent。
BasePass-基础通道
2.5、G-Buffer
从G-Buffer(Geometry Buffer)开始,引擎不在依靠计算机计算的结果,而是利用存储在图片中的信息进行后续的处理(这里可以解释为什么延迟渲染技术可以解决大量光照的渲染消耗问题,我们可以先得到G-Buffer再去做光照计算,由于屏幕上的像素只有这么多如1024920,只需要把这些点进行光照迭代就行了。延迟渲染的本质是先不要去做三角形迭代和光照计算,而是先找到你能看到的所有像素再去做光照计算。直接迭代三角形的话,很多三角形是看不到的,无疑是巨大的浪费*)。G-Buffer可以存储各种的通道:如世界空间法向量(World Normals)、高光度、粗糙度、金属度、漫反射等等。

可以查看G-Buffer
当然延迟渲染的缺点之一就是对显存和宽带消耗很大,假设渲染窗口是1920 1080 4(RGBA) 4(MRT) 8(Byte tobit) * 60(FPS)约等于15G,在PC段勉强接受在移动端压根扛不住,我想这也是移动端用前向渲染的原因之一吧。
为了减少G-Buffer消耗巨大的显存和宽带因素,通常会对纹理压缩、或者采用多级渐进纹理Mipmaps,Mipmap是由许多原纹理1/4大小的纹理组成,只有分辨率为2N的纹理才能自动生成Mipmaps,可以不是正方形。
多级渐进纹理Mipmaps
使用Mipmaps是因为高清纹理在远处会产生大量的噪点,也因为要处理纹理流送,纹理流送是指需要确定引擎在某一时刻根据玩家的所处位置以及摄像机的角度来确定加载那张纹理和Mip。如果不用Mipmaps直接加载所有需要的纹理,会导致显存宽带消耗过大,且页面加载的时间过长。(我们有时会发现在游戏刚加载完成时,有的贴图由模糊变得清晰,这是因为电脑没有足够的显存宽带来加载完成分辨率的纹理,而是使用了一张低分辨率的mipmap来代替)。
2.6、Dynamic Lighting&Shadows
我们知道实时动态灯光和阴影是很难计算的,它们要消耗大量的性能。因此光照的部分计算量被分流到了预计算阶段,也就是静态光照。
在介绍动态光照之前,我们有必要了解一些静态光照的流程和优缺点:
a、静态光照会先预计算把很多信息储存在光照贴图Lightmaps[9]中,这样做的好处是加载渲染性能快(由于是预计算,无论场景中有多少盏灯光,在烘焙后性能都是一样的),缺点是需要花费很长的时间去预计算而且增加内存占用量,最主要的是当模型有变化的时候又要去重新预计算渲染(当然静态光照它可以获得更真实的阴影效果包括高质量的软阴影,但是一旦计算完毕就无法移除和改变光照和阴影)。

动态光照流程的优缺点:
动态光照是借助GBuffer实现的实时渲染,动态光照的阴影对性能影响非常大,所以通常会降低渲染质量作为补偿,而且动态光照不会生成软阴影。

和许多现代的实时渲染器一样,目前它包含大量专用的功能,我们需要通过不同的解决方案来完成具体的任务(比如室内的阴影渲染和室外的渲染时截然不同的)。光照有IES Profiles文件和Light Functions。

但是动态光照却要复杂得多,阴影渲染通常需要占用大量的性能。有标准动态阴影(Regular Dynamic Shadows),这是最常用的类型也是可移动光源默认的自带的阴影,此类阴影边缘过于清晰尖锐。
标准动态阴影(Regular Dynamic Shadows)
为了解决阴影渲染占用大量性能的问题,有级联阴影贴图(Cascaded Shadow Maps),级联贴图专注适用于室外环境。因为不希望整个环境中渲染阴影消耗大量的性能,因此我们可以使阴影消失在某个点,但是直接消失效果会不好看因此我们需要逐步消失阴影。这时候级联阴影贴图就发挥作用了,级联阴影贴图是三种不同的阴影贴图并且采用级联的方式,分别是低质量、中质量、高质量并且在不同距离消失从而实现淡出效果。
级联阴影贴图(Cascaded Shadow Maps)
即使使用级联阴影贴图(Cascaded Shadow Maps)计算仍然占用大量的性能,其次阴影消失出的效果仍然很难看,因此在UE4中引入了距离场阴影(Distance Field Shadows)。阴影渲染的真正困难在于必须考虑阴影贴图几何体,正常投射阴影需要知道各种点之间的距离,如灯光的方位、灯光离几何体的距离,但是询问这些数据是一个很慢的过程。
但如果可以用某种方法能够存储模型之间的距离信息,就可以花费更少的时间来实时计算这些所有的数据,而距离场阴影(Distance Shadow Maps)就是UE4中加速该过程的方法之一。使用距离场信息而非几何体信息实现超远距离的阴影,不是很精确但是消耗很低。但也是目前实用的方法,通过创建体积纹理(Volume Texture)用于阴影计算,纹理的分辨率决定了阴影的细节程度。通常情况下分辨率很低。仅能用来实现远距离的阴影。
距离场阴影(Distance Shadow Maps)
2.7、Reflection
实时反射计算也是很难实现的,如果从理论精确来说每出现一次反射都需要重新渲染整个场景,但考虑到硬件性能不可能这样做,因此只能通过一些Trick来近似模拟。主要介绍三种常用的反射:
a、Reflection Captures(反射捕捉)
反射捕捉意味着在某一个特定的位置捕获一张预计算出来的静态立方体(CubeMap),虽然快但是不精确且只有局部效果。

b、Planar Reflection(平面反射)
Reflection Captures(反射捕捉)对于镜面效果并不好,而平面反射的精确度要高得多。但是平面反射消耗比较大,而且只能用于平面。

c、屏幕反射空间SSR(Screen Space Reflection)
SSR是默认的反射系统,它能反射在屏幕空间内所有当前可见的对象,并且是实时反射的,但是输出结果噪音很多而且损耗很大。
屏幕反射空间SSR(Screen Space Reflection)
2.8 、Final Frame
以上就是虚幻引擎实时渲染流程的一些大概阐述,有兴趣的可以去看看Sjoerd de Jong[10]的官方教程的记录,有助于对实时渲染有较为全面深入的了解

参考
- 游戏引擎架构-Milo yep
- Game Engine Architecture-Jason
- GAMES104-现代游戏引擎:从入门到实践》 https://www.bilibili.com/video/BV12Z4y1B7th/?spm_id_from=autoNext
- What is a Render Hardware interface (RHI)? https://andrewcjp.wordpress.com/2019/11/09/designing-a-render-hardware-interface-for-explicit-multi-gpu-programming/
- Datasmith Workflow https://www.unrealengine.com/zh-CN/datasmith
- UE4实时渲染基础及深入探究 https://www.233tw.com/unreal/30882#toc-5
- UE4渲染入门 https://www.bilibili.com/video/BV1JZ4y1V7Nv?p=2&spm_id_from=pageDriver
- UE4中预计算遮挡剔除(PVS)及UnrealLightsmass数据格式的源码解读 https://zhuanlan.zhihu.com/p/445249513
- 像小说一样有趣的虚幻引擎:渲染模块(UE4)-LightMap、动态光源渲染、实时光线追踪 https://zhuanlan.zhihu.com/p/477799165
- Sjoerd de Jong—UE RTR