游戏引擎架构#3 链接、图形计算库、资产管理模块
背景:
作为游戏开发从业者,从业务到语言到框架到引擎,积累了一些知识和经验,特别是在看了好几遍《游戏引擎架构》后对引擎的架构感触颇深。
近段时间对引擎剖析的想法也较多,正好借着书本对游戏引擎架构做一个完整分析。此书用简明、清楚的方式覆盖了游戏引擎架构的庞大领域,巧妙地平衡了广度与深度,并且提供了足够的细节,使得初学者也能很容易地理解其中的各种概念。
我的目标是掌握游戏引擎架构知识,我的方法是借助《游戏引擎架构》这本书、结合引擎源码和自己的经验,深入分析游戏引擎的历史、架构、模块。最后通过实践简单引擎开发来完成对引擎知识的掌握。
游戏引擎知识面深而广,所以对这系列的文章书编写范围做个保护,即不对细节进行过多的阐述,重点剖析的是架构、流程以及模块的运作原理。
由于《游戏引擎架构》此书的部分知识太浅或太过陈旧,所以不得不将部分知识重新深挖后总结出自己的观点。
概述:
本章开始对引擎中的重要的模块和库进行详细的分析,我挑选了十五个库和模块来分析:
- 时间库
- 自定义容器库
- 字符串散列库
- 内存管理框架
- RTTI与反射模块
- 图形计算库
- 资产管理模块
- 低阶渲染器
- 剔除与合批模块
- 动画模块
- 物理模块
- UI核心框架
- 性能剖析器的核心部分
- 脚本系统
- 视觉效果模块
本篇内容为列表中的6、7。
正文:
简单回顾下前文,前面我们聊了时间库、自定义容器、字符串、内存管理这四个模块的技术原理和特点,它们都是大型软件架构所必备的模块,同时简单讲述了它们在Unreal和Unity中存在的特点。
编译链接过程与内存布局
图1
图2
简单回顾一下C++编译过程:
- 源文件.cpp文件被编译成.o文件后由链接器链接成可执行文件或库文件。
- 库分为静态库和动态库。其中静态库为只是简单集合了.o文件,而动态库则是一个完整的编译、链接产物。
- 头文件.h文件不是编译的必需品,它只是包含在源文件中的声明文件。
- Linux的so和Windows的DLL虽然都是ELF文件格式,但最终格式差了很多,因此不能互相使用。
- 编译器开启优化后(一般是Release时),会优化代码,包括内联、调换代码顺序、更改代码为最优等。
具体可以看我前面写的《链接、装载与库》
http://luzexi.com/2021/06/20/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B029
也同时回顾下C++内存布局: 1.C++内存布局中有,类、变量、内存对齐、虚表、RTTI 2.类和结构在C++中差异较少 3.每个变量内存占用量不同,int(32bit)、short int(16bit)、long long int(64bit)、float(4bit)、double(8bit)、char(8bit)等 4.默认按4字节(32bit)对齐,不足4字节的编译器会补齐 5.虚函数或虚继承的类有虚表及虚表指针 6.没有RTTI的情况下,虚表只有当前的虚函数指针 7.有RTTI的情况下,虚表中有type_info指针 8.有RTTI的情况下,虚指针指向虚表中的第二个格子,即虚指针,第一个格子为type_info指针
具体可以看我前面写的《深度探索C++对象模型-总结》
http://luzexi.com/2020/11/20/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B014
图形计算库
图形库涉及内容有图形元素和运算两种,它们分别包含,点、矢量、矩阵、四元数、图形对象以及相关的运算(包括SIMD)。
下面我们来介绍一下:
矢量运算包括:矢量加减法、模、归一化、点积、投影、叉积、线性插值等。
矩阵有,单位矩阵、转置矩阵、逆矩阵、齐次坐标。矩阵运算,包括矩阵乘法、加减法、除、与、或等。
四元数运算包括,四元数乘法、加减法、逆运算、旋转矢量、等价的四元数矩阵、旋转线性插值、球面线性插值等。
各图形转换操作,包括旋转、缩放、投影、平移、LookAt等。
图形对象包括,直线、线段、球体、平面、包围盒、平截头体、圆形、矩形、三角形等。
这部分内容可以在Unreal的Engine\Source\Runtime\Core\Public\Math中找到。
下面简单介绍下SIMD:
硬件加速SIMD运算(Single instruction multiple data),单指令多数据,是指,现代微处理器用一个指令并行地对多个数据执行数学运算,它能帮助我们加速运算。
游戏引擎中最常用的是SSE模式(Streaming SIMD extensions,SSE),它包裹了4个32位float值,它们都被打包进了一个128位寄存器。
单个指令可对4对浮点数进行并行运算,如加法或乘法。在计算四元矢量和4x4矩阵相乘时特别有用。
需要注意的是 1.由于在浮点运算器和SSE寄存器之间传输数据很糟糕,所以不要混合使用普通浮点数和SIMD运算,这样会使得CPU整个指令执行流水线停顿,浪费CPU周期。 2.在VS中用SIMD数据类型__m128声明的临时变量或参数,编译器通常会把它们直接置于SSE寄存器中而非内存栈。 3.动态分配SIMD结构时要注意内存按16字节对齐
资产管理模块
先说文件系统
文件系统
游戏引擎中的文件系统相对比较简单: 1.每个平台的路径、API不同,对平台需要做些封装。 2.文件读取的阻塞方式分同步和异步。
其中同步需要阻塞当前进程来等待IO,异步则通过分线程阻塞等待IO,其两者原理是一样,都是调用内核读取文件且都需要等待IO。
以前写过一篇关于操作系统内核中文件操作的底层原理《链接、装载与库 - 内核运行库》大家可以参考下。
这里顺便简单回顾一下文件内核原理:
图1
图2
这两张图清晰的表达了内核文件的读写原理: 1.操作系统内核中对每个打开的文件都有个内核对象 2.所有文件内核对象都被集中索引到一个数组中,称为文件打开表 3.文件表数组前三个元素填充的是stdin、stdout、stderr这三个内核对象 4.为了增加读写效率,内核已经实现了文件读取缓冲,会读取一段一段的读取 5.读取步骤,用户程序先开辟一段内存,内核程序则利用缓冲读取,不足时多次读取,结束时返回数据。
资产管理器
早前很多引擎都有独立的资产管理器,它被制作成了一个独立的软件,专门用于管理游戏资产,包括网格、材质、纹理、着色器程序、动画、音频、配置等。
资产管理器本身是一个具有清晰设计、统一、中心化的子系统,负责管理游戏中用到的所有类型的资产,只是现代大多引擎已经将资产管理整合到引擎编辑器中。
资产管理器解决了什么问题?答案是:
1.资产预览,资产在引擎中快速预览,并对不同资产类型区分展示。 2.资产查找,通过查找功能快速查找到资产。 3.资产组合,通过组合信息管理资产各个依赖。 资产通常组合在一起使用,因此组合信息是资产管理的一部分。 加载时需要依赖多个资产,引擎通过资产元数据将这些信息保存下来。 4.资产转换,将外部资产导入到引擎中使用。 外部资产需要经过一定的转换才能在引擎中使用。 引擎通常有自己的资产导入系统,将外部资产转换为自身使用的数据格式。 5.运行时资产管理,引擎向应用层提供加载和释放资产接口,并管理已加载资产对象。 资产的加载和释放,引擎需要提供给应用层加载和释放资产的接口,引擎本身也需要对这些资产进行管理。
下面我们从资产管道、资产类型、运行时资产管理、元文件、资产包,四个方面介绍下引擎中的资产管理器。
资产管道:
每个资产都需要通过资产管道才能最终被游戏引擎所使用。每个资产管道的始端都是DCC原生格式的源资产(Maya的.ma或.mb、3DMax的.max或.obj、Photoshop的.psd文件等)。资产经过资产管道的导出器、资产编译器、资产链接器,最终生成了游戏引擎可以使用的数据格式。
第一步,通常DCC工具需要撰写自定义插件(大都已提供现成统一的插件),把DCC里的数据导出为某种中间格式(例如.fbx格式),一般DCC工具都会提供接口或脚本供程序员写导出插件。 第二步,中间格式数据仍然需要经过一定的转换才能被引擎使用,因此引擎通过资产编译器转换中间格式。 第三步,通常多个资产组合后才成为一个完整资产,例如网格文件、材质文件、动画文件、贴图文件等,它们经过资产链接器连接后组合成为完整的资产。
资产类型:
资产经过资产管道后会生成相应的资产相关文件,包括: 1.资产源文件 2.资产配置文件(元数据文件) 3.资产目标文件
资产也分为外部和内部资产,外部资产由DCC导出,内部资产则通过引擎生成,例如材质球、动画控制器、蓝图、粒子等。 资产配置文件记录了资产在引擎中的配置信息和依赖关系。 资产目标文件是引擎根据资产源文件和配置信息生成的符合引擎使用格式的资产文件。
生成资产元数据文件和目标文件的目的是: 1.在保留原资产文件格式的前提下,生成引擎能使用的格式文件。 2.便于引擎获取每个资产文件的配置及依赖关系。 3.便于引擎统一管理资产,管理资产的目的是,提供例如建立资产数据库、查找、同步、打包等功能。
Unity引擎使用Mate文件存储资产的配置和依赖关系。当引擎导入资产时就会生成相应的Mate文件,并根据这个Mate文件在Library文件夹下生成目标文件,同时根据修改的Mate文件配置来调整或重新生成目标文件。
Unreal引擎则稍稍有些不同,资产源文件仍然会转换成资产目标文件,只是它把资产配置文件和资产目标文件合并在一个uasset文件中,这样导入后源文件就不再需要。如果你想要获取元数据,UE4也提供了python接口和蓝图接口。
运行时资产管理:
运行时资产管理通常包括,资产对象管理、资产对象映射管理。
资产从加载到实例化,在引擎内部必须有一个有效的管理机制,其职责为: 1.同一份资源只会存在一个副本 2.管理资产生命周期,确保不需要时卸载 3.处理复合资产,复合资产依赖多个资产组合而成。 4.维护引用,确保复合资产在内存中的引用关系正确。 5.资产接口,提供资产载入与卸载接口,包含同步和异步的载入方式。
基于这五个职责,资产在运行时的引擎中必须拥有信息为,资产地址,资产对象,资产对象ID(运行时ID)。
部分引擎对每个资源都配备了资产唯一ID(GUID),让资产地址与资产唯一ID有绑定关系,这使得引擎在资产迁移时能发挥更好的作用。
资产对象映射管理包括:
- 资产路径与资产唯一ID的映射关系
- 资产唯一ID与资产对象ID的映射关系
- 资产对象ID与资产对象的映射关系
有了这些映射关系,引擎就能通过资产路径查找到资产对象,从而保证不重复加载,并维护好各资产之间的引用关系。
在UE4和Unity上也同样做了这种类型的资产映射关系的管理。
资产加载和卸载
资产加载与卸载接口必不可少,通常引擎都会定制一些依赖数据,例如前面提到的资产配置文件(也可以说是资产元数据),多个资产组合达成资源包时,资源包之间的依赖关系也同样需要有数据来维护。
引擎都会有资源加载的统一接口,包括加载和卸载,同步和异步,资源包和非资源包形式。
图
引擎的资产加载和卸载框架各引擎之间稍有不同,不过总体差不了太多,或直接IO调用,或用开启线程后做IO调用,然后通过存储资产对象与映射关系来搭建资产加载和释放的框架,由于各个引擎接口都不一样,不做详细介绍。
资源包数据格式
通常引擎都会提供类似AssetBundle的资源组合包,便于外部资源下载和更新。
在UE4和Unity上都有相同的功能,只是命名不同,UE4为Pak,Unity为AssetBundle。
Pak或AssetBundle中存放着多个复合资产,通过引擎接口加载指定资产。
图
为了方便理解,我把数据格式从头往下画。实际中的Pak和AssetBundle数据格式要倒一下,头信息在最底部,资产数据块在最前头。
资源包数据格式: 1.文件头信息与数据块拆分 2.可整体压缩或部分压缩 3.通过依赖配置加载外部资源包
资源包可以通俗的认为是一个多文件的组合,它可以自己做压缩,也可以让资产压缩后再组合成文件。
通常资源包的数据格式由文件头和数据块两部分组成。文件头信息中包含了资产信息和偏移量,通过加载文件头,就能知道资产在文件中的位置、类型、名称、大小等。
这种资产组织方式使得我们通过差量方式更新资产成为可能。
图
根据资源包的数据格式特点,可以规划差量更新步骤: 1.打差量包,包头保持完整,数据块则只加入差量部分 2.下载差量包 3.合并两个资源包文件 4.合并时使用差量包头作为文件头 5.合并时提取原资源包中的不更新部分和差量包中的更新部分放入新的资源包文件中 6.更新完成生成新的资源文件
这部分内容Unity和Unreal并无太大差异。
资产规范
资产规范目标: 规范命名,防止程序报错,方便自动化检测,方便筛选找寻。 优化内容,每个项目资源都应该有存在目的。 减短路径,路径简短易寻。
图
网上有同学分享的很详细了,这里就不赘述,参考《UE4工程规范》:
https://github.com/skylens-inc/ue4-style-guide/blob/master/README.md#12-%E8%B5%84%E6%BA%90%E7%B1%BB%E5%9E%8B%E8%A1%A8-
参考资料:
《游戏引擎架构》叶劲峰 译
《游戏引擎原理与实践》 程东哲 著
《vmath》
https://github.com/BlackMATov/vmath.hpp#Matrix-Transform-3D
《从虚函数表到RTTI》
https://zhuanlan.zhihu.com/p/150579874
《虚幻引擎4文档》
https://docs.unrealengine.com/4.27/zh-CN/Basics/AssetsAndPackages/AssetMetadata/
《UE4工程规范》
https://github.com/skylens-inc/ue4-style-guide/blob/master/README.md#12-%E8%B5%84%E6%BA%90%E7%B1%BB%E5%9E%8B%E8%A1%A8-
《链接、装载与库 - 内核运行库》
https://mp.weixin.qq.com/s?__biz=MzU1ODY1ODY2NA==&mid=2247484809&idx=1&sn=89091ecce47229ebf10e4855c0ccceca&chksm=fc22608ecb55e998cdff3952057e6d1c6f099463797d57458ec1f4d37a432d46dc3c5bfcfbb5&token=557108361&lang=zh_CN#rd
《链接、装载与库 - 静态链接》
http://luzexi.com/2021/06/20/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B029
《深度探索C++对象模型-总结》
http://luzexi.com/2020/11/20/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B014
感谢您的耐心阅读
Thanks for your reading
版权申明
本文为博主原创文章,未经允许不得转载:
Copyright attention
Please don't reprint without authorize.
微信公众号,文章同步推送,致力于分享一个资深程序员在北上广深拼搏中对世界的理解
QQ交流群: 777859752 (高级程序书友会)