始于技术,但不限于技术,公众号的宗旨是帮助程序员玩转技术、玩转生活。
先前的评论:
背景
作为一名游戏开发者,我在过去10年中积累了很多项目的经验,同时我也想把自己在大公司的经历分享给大家。为此,我在2021年出版了一本书《Unity3D高级编程:主程序员笔记》。书中记录了我过去做过的所有项目的各个模块的技术点,包括框架、算法、原理、优化方案(博客:)。同时,通过这些经历,我深刻认识到引擎作为游戏开发的基础构建和底层逻辑的重要性。这些年来,我对引擎也有了自己的理解,近几年通过对引擎的解剖和分析,对其模块结构、流程、技术原理、实现方法等都有了深刻的理解。
最近突然有个想法,想系统地剖析一下游戏引擎,于是开始了这一行动。想通过这次分析过程更加深入地了解引擎,并将它们分享出来,于是决定写一系列的文章来分析引擎的各个模块,包括结构、流程、技术原理和实现方法。为了与工作相结合,发挥出最大的效益,我在分析引擎的同时,也把这些知识和技能运用到了自己的项目中。
过去几年,我一直在性能优化方面努力,包括性能工具开发、性能分析、性能优化、框架结构改造升级等。在工作中,我也深刻体会到理解引擎的架构、引擎各个模块运行原理对于优化工作的重要性。其实“引擎技术”并不单单指引擎,为了描述方便,我这里就称之为“引擎技术”。同时,为了大家能更好的理解引擎,我会用图来描述本系列文章中的原理、流程、结构,加速我和读者对知识的理解。
我将这个系列命名为《图形化游戏引擎》,希望用“图”来形象地诠释抽象的技术。写作的过程也是我的学习过程,通过学习和复习可以对引擎技术有更深入的理解。本系列文章将系统地分析游戏引擎技术,涵盖引擎的历史、架构、功能框架、模块结构、执行流程、策略机制、公式算法、硬件结构等。此外,如果时间允许,我也会用一些自制的引擎案例来完成引擎技术的实战。
目录
1.UGUI结构概述
2. UGUI图集及组件
2.1 Atlas算法
2.2 组件原理
3.UGUI输入机制
4. UGUI内核
4.1 UGUI核心结构
4.2 批处理算法及流程
4.2.1 元素排序
4.2.2 元素批次计算
4.2.3 批量网格生成
5. UGUI渲染与材质
5.1 渲染过程
5.2 渲染材质
摘要:UGUI内核由网格生成及批量计算、渲染提交、Canvas更新三部分组成。Canvas管理器管理每一个Canvas,所有的Canvas组成整个UGUI屏幕。Canvas驱动更新、批量、渲染。UI元素有Dirty标记机制来决定批量状态。渲染提交在Overlay模式和Scene模式下有所不同。UGUI的批量步骤大致分为排序、批量计算、批量网格生成、渲染提交四个步骤。每个Canvas在每一逻辑帧都会轮询一次这四个步骤。
关键词:Unity引擎、UI底层框架、UGUI分析
内容
4. UGUI内核
这里有两部分:核心结构和批处理算法
4.1 UGUI核心结构
UGUI Core由网格生成及批量计算、渲染提交、Canvas更新三部分组成,这三部分可以简单理解为批量、渲染、更新。
UGUI 的 C++ 部分可以很好的和 C# 部分分离,因为耦合性做的非常好。Core 部分只做渲染和批处理,其他业务逻辑都交给 C# 来完成,包括组件顶点生成、效果实现、输入监听、dirty mark 实现等。这样的做法很好的隔离了业务和引擎开发,同时又让两边很好的协调,这是一个非常棒的设计。
下面是一个结构图,将Core分为三个部分:
(UGUI Core组成图)
每个Canvas都由Canvas Manager进行管理,所有Canvas组成了整个UGUI屏幕游戏角色,同时Canvas在每一帧中运行逻辑和渲染,上图的三个部分都是由Canvas发起的游戏引擎技术,具体内容如下:
1)WillRenderCanvases网格生成:负责网格生成和批量计算。它会调用每个UI元素的Rebuild接口,在接口中会判断当前元素是否需要重建,是则重建,不是则继续。
2)WillRenderCanvases批量计算:UI元素的网格生成完成后,会开启批量计算逻辑,批量计算在多个线程中进行,批量计算完成后会筛选N个批次的UI元素游戏引擎技术,然后将同一批UI元素的网格进行合并。
3)提交渲染RenderOverlays:负责Overlay模式下的渲染,渲染时,batched后的内容会经过UGUI本地逻辑后直接提交给渲染线程。
4)提交渲染WorldScreenspaceCameraGeometry:负责将批处理的物体推入场景渲染队列。与Overlay模式不同的是,该模式的渲染流程步骤会将渲染好的物体推入场景渲染中,参与裁剪排序等逻辑。
5)UpdateCanvas:主要负责更新UI元素节点和Canvas信息,避免异步帧渲染逻辑。
以上就是UGUI Core的结构,主要由Canvas驱动进行更新、批处理和渲染。UI元素有Dirty标记机制来判断批处理状态。Overlay模式和Scene模式下渲染提交有所不同。最重要的是Core和业务通过C#虚拟机进行了很好的隔离,两者互不干扰,配合非常顺畅。
4.2 批处理算法及流程
UGUI的批量处理步骤大致分为排序、批量计算、批量网格生成、提交渲染四个步骤,每个Canvas在每一逻辑帧都会对这四个步骤轮询一次。
从排序到批量计算到网格生成到渲染提交,整个流程图如下:
(UGUI从批处理到渲染的流程-图)
UGUI 的网格批处理和渲染流程如上图所示,首先对各个 UI 元素进行排序,然后根据排序结果进行批处理计算,计算出来的批处理对象包含需要批处理的内容,然后将批处理对象中的内容进行合并生成批处理网格,最后将生成的批处理网格提交渲染。
这里有四个重点:排序算法、批处理算法、批处理网格生成算法、渲染提交流程,下面详细介绍一下。
4.2.1 元素排序
每个 Canvas 在每一帧更新时都会对元素进行批量计算,首先要判断元素是否发生了变化从而导致 Canvas 需要重建网格,其次要判断哪些 UI 元素可以组合在一起。
想象一下,如果我们要找到可以合批的元素。一般我们依靠搜索找到同一个图集中的所有元素,然后选择叠加层级相同的元素,然后选择材质球属性相同的元素。最后剩下的元素就是可以合批的元素。
UGUI 也是这么想的,为了达到筛选的目的,UGUI 在筛选批次元素之前,会先对元素进行排序,排序后的元素集合使得查找批次元素的效率更高。如下:
(UGUI元素排序图)
如上图,UI元素在同一个Canvas下是上下堆叠的,排序的时候Canvas会把所有的UI元素拿出来排序,排序的时候会比较很多属性,这些属性决定了元素在排序队列中的前后位置。
要排序的属性有很多,其中最重要的四个属性是深度、材质、纹理、renderIndex,它们的计算顺序为:
1.深度
2. 材料
3. 纹理
4. renderIndex,实例索引
Depth 最关键的是,它是 UI 元素在渲染时的堆叠位置,在比较排序时会把这个条件放在最前面,同时它的计算方式也不是简单的统计元素堆叠的层数,而是一个算法。
UGUI 使用深度等级来标识不同的批次,原因是很多 UI 元素叠加在一起会让批次划分变得复杂,同时我们知道 Alpha Blending 渲染会造成半透明贴图前后渲染关系混乱,因此各个叠加等级无法完全使用同一批渲染进行批次划分。
深度计算具体步骤如下:
(单元深度计算方法及步骤)
除了深度,上面提到的所有重要的batch属性都是通过比较ID来排序的。深度的计算是最复杂的,其结果取决于元素是否重叠。计算步骤如下:
1)首先,计算顺序按照父子节点的排列顺序,避免交叉计算时出现遗漏。
2)其次,有一个按坐标排列的Grid容器3D动画,该容器在计算的同时收集Rect包围框内的元素,也就是说,每个计算出来的元素都会被添加到Grid容器中。
3)然后每次计算都会按照元素Rect的边界框范围提取所有前面的元素,并计算检查这些前面的元素,提取相交元素当中最大的深度值作为参考值。
4)最后判断当前元素和所有重叠元素是否可以一起批处理,如果其中任何一个不能一起批处理,则当前元素的深度为最大深度+1,否则说明都可以一起批处理,深度为最大深度。
Grid容器类似下图:
(网格容器图)
Grid 会在坐标处创建一个有内容的列表,用于存储当前位置的 UI 元素。这样可以更高效地提取相交元素,只需要提取 Grid 容器中元素范围内的元素即可。例如 Image1 和 Image2 两个重叠的坐标网格中分别存储了 Image1 和 Image2 的实例,如果这两个坐标处有第三个 UI 元素,那么可以直接将 Image1 和 Image2 取出 Grid 容器。
4.2.2 元素批次计算
UGUI中对元素的批处理条件有5个,必须满足这5个数据才可以进行批处理,如下图:
(元素配料情况图)
批处理条件为:相同材质、相同纹理、相同纹理格式、相同裁剪实例、非强制非批处理元素。前三个条件是渲染状态下一次性提交多个数据的必要条件。后两个条件是因为UGUI有些功能必须强制不能批处理,比如强制非批处理的组件、带裁剪功能的组件Mask等,功能是为了保证功能范围内元素的渲染。
深度计算和排序完成后就开始批量计算了。批量计算会筛选出哪些元素可以批量计算,并将批量计算出来的元素放在一个批量对象实例中,供后续的网格合并和渲染使用。所以其实批量计算就是一个快速匹配的过程,如下图所示:
(UGUI批量计算图)
如上图,在计算批次时,由于之前已经完成排序,因此只需要从0开始将排序后的元素数组遍历到末尾即可,遇到批次中断时,则重新建立批次组合实例,继续遍历后续元素,直至末尾。
这样的 batch 计算,计算复杂度线性为 O(n),最终所有元素都会被放入 batch 实例中(即便可能只有一个元素是自己 batch 的,也会为其创建一个 batch 对象来存放),batch 对象会在后续的逻辑中等待生成 mesh 后,提交给渲染管线进行渲染。
批量计算的步骤如下:
(批量计算步骤-图)
批量计算步骤具体如下:
1)遍历整个已排序的元素数组,
2)依次计算当前元素是否可以和上一个元素进行批处理,批处理的条件是同材质、同纹理、同格式、同裁剪实例,并且不强制进行新的批处理
3)当遇到可以批处理的批次时,将元素放入批次对象中。批次对象存储了批次的着色器属性和顶点数据,这些数据将在批处理的网格中使用
4)当无法与前一个元素进行批量内容时,终止批量内容。
5)重新建立新的batch对象,继续遍历计算
6)直至所有元素对象分批计算完毕,最终得到分批对象集
简单来说,有序队列加速了批次计算,每次合并批次时,只判断前一个元素是否满足批次条件,当批次断掉后,继续遍历新的批次对象,批次对象中存储了常见的Shader属性和顶点数据,批次对象完成后,等待合并网格,提交渲染。
4.2.3 批量网格生成
批量计算完成后,UGUI还需要对批量处理的物体的网格进行批量处理,批量处理后的物体有元素集共同的数据,用于合并网格并提交渲染,以下是网格合并的流程图:
(批量网格生成)
如上图所示,批量计算完成后会立即进行批量网格生成,UGUI会把每个批量对象中需要绘制的网格数据放入事先申请的缓冲buffer中,这些数据包括Vertex、index、UV0、UV1、UV2、UV3、color、tangent等。
在新华书店、当当、京东、微信读书、西华大学、机械工业出版社等各大平台出版发行。
自我报告的经历: