在学习和使用UGUI之前,首先理解UGUI的各个组成部分非常重要,UGUI是由多个类和组件构成的系统。本章主要介绍一些相关术语,这些术语将贯穿整个系统,然后还会涉及到一些UGUI底层的知识。
术语
Canvas - 画布组件
Canvas组件是将其下的各个排序好的图形(Image、Text)提供给渲染系统,然后再由渲染系统将之显示到屏幕或者三维世界中。
Canvas首先对它下面的网格进行批处理(Batch),然后生成相应的渲染指令,最后将指令发送到Unity的图形渲染系统。所有这些工作都是在C++代码中完成的,这个过程称为rebatch或者batch build。当Canvas包含的网格需要重新进行批处理时,那么认为这个Canvas是脏的(dirty)。
嵌套在其他Canvas下的Canvas称为子画布(Sub-Canvas),子画布将它的子物体与父画布隔离开来,一个脏的子物体不会强制父画布进行重构,反之也一样。当然存在一些例外情况,比如:父画布尺寸变化引起子画布尺寸变化,这个时候父画布和子画布都需要进行重构。
Canvas Renderer 组件
上面提到画布对网格进行批处理,Unity利用 Canvas Renderer 组件将网格信息提供给Canvas。
Graphic 组件
Graphic 组件是 UGUI 的 C# 库提供的,不涉及本机代码导入,它是所有 UGUI 可绘制网格的基类,大部分内置UI图形都是通过继承 MaskableGraphic 基类来实现的,该基类允许通过 IMaskable 接口实现遮罩功能,主要的子类包括 Image、Text,两者都有同名的组件。
Layout 组件
Layout 组件控制 RectTransform 的尺寸和位置,通常用于创建需要相对布局的复杂界面,Layout 组件仅依赖和影响 RectTransform 组件,而并不依赖于 Graphic 对象,可以单独使用。
Graphic 和 Layout 组件都依赖于 CanvasUpdateRegistry 类。该类跟踪需要更新的 Layout 组件和 Graphic 组件,当与它们相关的 Canvas 组件调用 willRenderCanvases 事件时,CanvasUpdateRegistry 类会根据需要触发更新。
Layout 和 Graphic 组件的这种更新过程称为重构(rebuild),后面将会详细介绍这一过程。
渲染细节
当搭建UI界面的时候,必须牢记一点,画布下的所有几何图形将在透明渲染队列(Transparent Queue)被绘制,这意味着,所有的图形总是从后向前进行 Alpha 混合。从性能的角度来看,有一点比较重要的需要知道,所有的栅格化之后的像素(图元)都会被采样计算,即使它被不透明的物体完全遮挡。在手机上,很高的 overdraw 将会超过 GPU 的填充率。
什么是overdraw?overdraw也就是过度绘制,是指在每个渲染周期内,屏幕上每个像素最理想只渲染一次,但是由于UI元素的重叠会导致像素会被渲染多次,每次渲染从CPU阶段到GPU阶段会消耗大量资源,如果这种情况比较严重,就会造成卡顿。
什么是填充率(fill rate)?填充率是指 GPU 每秒内能够渲染的像素的个数,单位是MPixel/S、GPixel/S,当每个渲染周期内需要渲染的像素超过GPU处理能力时(可能是场景太复杂、也可能是 overdraw 严重),就会造成丢帧。
批处理过程
批处理是通过 Canvas 组件将所有 UI 元素的网格进行合并,然后生成并发送渲染指令给 Unity图形渲染管线。批处理的结果将被缓存起来并重复利用(顶点缓存对象VBO),直到 Canvas 组件被标记为 dirty,这种情况一般发生在 Canvas 下的UI元素的网格发生了变化。
批处理过程中的网格来自其下的 CanvasRenderer 组件,但是不包括子画布下的物体。
网格合并过程中需要将网格按照深度进行排序、进行交叉测试、共享材质等计算。这个操作是多线程的,所以性能在不同的 CPU 上存在一定差异,这种差异在手机 CPU 和桌面 CPU 上尤其明显。
Rebuild 过程
在介绍Layout组件时,提过Layout和Graphic重新计算时,这个过程称为重构(Rebuild)。这个过程发生在 CanvasUpdateRegistry 类中,这是一个C#类,源码可以在 这里 找到。
在 CanvasUpdateRegistry 类中,比较核心的方法是 PerformUpdate,当 Canvas 组件调用 willRenderCanvases 事件时,PerformUpdate 就会被调用,willRenderCanvases 事件每帧都会调用一次。
PerformUpdate 方法的执行主要包括三个步骤:
- 脏的Layout组件必须进行重构(rebuild),通过调用 ICanvasElement.Rebuild 接口完成。
- 裁剪组件(如 Masks 组件)进行裁剪操作,通过调用 ClippingRegistry.Cull 方法完成。
- 脏的Graphic组件必须进行重构,也是通过 ICanvasElement.Rebuild 接口完成。
对于 Layout 和 Graphic 组件的重构过程,它被处理成多个阶段,Layout rebuild 过程包括 PreLayout、Layout、PostLayout;Graphic 的 rebuild 过程包括 PreRender、LatePreRender
Layout 组件重构
为了重新计算包含一个或者多个Layout组件的位置和尺寸,需要对 Layout 组件进行适当的层次排序,父物体的Layout组件可能会影响子物体的Layout组件的尺寸和位置,所以需要先计算父物体的 Layout 组件,要做到这一点,Unity会将Layout组件按照层次深度进行排序,高层级的(越接近根物体)会被排序到最前面进行优先计算。排序完成的Layout组件将会进行重构,这个过程完成了UI元素位置和大小的控制。如果需要了解Layout组件是如何影响各个UI元素的,可以用户手册的UI自动布局章节。
Graphic 组件重构
当Graphic组件重构时,Unity 将处理方法(PreRender)传递给 ICanvasElement.Rebuild 接口,Graphic 实现了ICanvasElement 接口,在重构的 PreRender 阶段执行下面两个步骤:
- 如果顶点数据(vertex data)被标记为脏的(例如组件的 RectTransform 尺寸发生变化),重新生成mesh 对象。
- 如果材质信息被标记为脏的(例如组件的材质或者贴图发生变化),就会更新 CanvasRenderer 的材质。
与Layout rebuild不同的是,Graphic rebuild 不需要按照特定顺序进行,因此不需要进行任何的排序。