【UGUI源码分析】四、Canvas
Canvas是UI的最上层,负责管理下面子节点UI元素的布局、渲染、排序。Canvas类本身不在UGUI里面实现,包括Canvas, CanvasGroup, CanvasRenderer这几个,在UGUI实现的有CanvasScaler和GraphicRaycaster。本文包含Canvas相关组件和Canvas的更新(Canvas Update System)以及它是如何从最上层管控所有的组件布局和渲染更新的。
Canvas本身是一个抽象的容器,并不会渲染(不是RT!不是RT!不是RT!),用来容纳下面的UI。所有的UI元素都必须在一个Canvas GameObject下。
1. 基本设置
1) Canvas组件
Canvas有3中render mode
简单来说分3种
- Overlay
- 相机空间
- 世界空间
三者的具体介绍看官方文档。Overlay适合做一般的UI\HUD,相机空间适合做头盔显示器、VR,世界空间适合做场景中的交互UI。
Overlay和相机空间中,canvas都会永远朝向相机,世界空间的canvas不需要。稍微解释一下参数的含义
参数 | 适用于 | 含义 |
Pixel Perfect | Overlay \ Camera Space | 是否开启抗锯齿 |
Render Camera | Camera Space | canvas所属的相机空间,射线检测的相机 |
Plane Distance | Camera Space | canvas在相机空间中离相机的距离 |
Event Camera | World Space | 世界空间中,canvas的GraphicRaycaster射线检测的相机 |
Sort Order | Overlay | overlay类型的所有canvas的渲染顺序,决定了下面UI的渲染顺序 |
Sorting Layer | Camera Space \ World Space | 对于相机空间和世界空间,会给canvas分不同的sorting layer,不同的sorting layer 按次序先后渲染,同一个sorting order内,按Order in Layer值从小到大先后渲染(后渲染的在上面),一个canvas内,UI元素按照hierachy顺序先后渲染 |
Order in Layer | Camera Space \ World Space | 相同sorting layer时,由order in layer决定渲染顺序 |
Additional Shader Channels | Camera Space \ World Space | 控制下面的Graphic组件的Mesh数据除了position和UV0之外还可以包含哪些数据,对做UI特效非常必要,比如PositionAsUv1需要开启UV1 |
2) GraphicRaycaster组件
为了做射线检测,会同时添加一个GraphicRaycaster
参数 | 含义 |
Ignore Reversed Graphics | 忽略反转背对相机的UI元素的射线检测 |
Blocking Objects | 可以阻断射线检测的物体类型,2D\3D\所有,要求这类物体有Collider组件。在Camera Space 和World Space能体现出来,UI射线检测是否可以穿透3D物体 |
Blocking Mask | 可以阻断射线检测的物体的layer |
3) CanvasScaler组件
CanvasScaler不重要,后面布局系统再看。
4) CanvasGroup组件
CanvasGroup用来以canvas为单位一并管理很多canvas下的所有UI元素而不需要一个一个UI元素修改,比如设置透明度。Canvas Group参数改动时会给下面的UI元素(继承了UIBehaviour)发送OnCanvasGroupChanged消息。
参数 | 含义 |
Alpha | 这个group下的UI元素透明度基数,会和UI元素本身的alpha值相乘 |
Interactable | 这个group下的UI元素是否可交互(射线检测),取消勾选意味着是单纯的HUD |
Block Raycasts | 这个group下面的UI是否会阻断射线检测,在Camera Space或World Space的canvas可以阻断射线穿透UI到后面的3D物体 |
Ignore Parent Groups | 忽略父canvas group的设置,用这个canvas group的设置覆盖 |
Canvas Group的用途有
- 通过设置alpha,做UI整体的淡入淡出
- 设置一个canvas为纯粹显示、不可交互的HUD,避免下面的某个UI元素误勾选raycaster target
- 设置一个或几个UI元素不阻断射线检测
5) CanvasRenderer组件
CanvasRenderer用来渲染Graphic类型的UI元素(Image、RawImage、Text三个),每个元素都需要一个CanvasRenderer,其他UI元素不需要加CanvasRenderer组件,可以去掉。
没有可配置参数。CanvasRenderer会做UI基于canvas的剔除、网格提交、材质绑定、渲染,本质上和MeshRenderer做的内容类似。在总结Graphic的时候会涉及。
2. Canvas Update System
Canvas是UI的最上层,会管理下面UI元素的布局和渲染,也就是canvas update system。涉及Canvas类、CanvasUpdateRegistry类、ICanvasElement接口和CanvasUpdate枚举。核心是CanvasUpdateRegistry。
Canvas向外提供了一个事件willRenderCanvases,发生在管线渲染所有canvas之前(也就是每个逻辑帧的最后,仅在渲染之前,这个很重要,这也是为什么布局不会立即重建,而是要等到“下一帧”才会刷新的原因,后面的文章会提到)。CanvasUpdateRegistry向其中注册了一个PerformUpdate方法,这个方法是整个UGUI布局和渲染更新的入口。
1 2 3 4 5 |
protected CanvasUpdateRegistry() { // willRenderCanvases在渲染canvas之前调用,执行PerformUpdate来Rebuild处理m_LayoutRebuildQueue和m_GraphicRebuildQueue Canvas.willRenderCanvases += PerformUpdate; } |
CanvasUpdateRegistry维护了两个缓存队列,分别缓存需要更新布局和渲染(网格、材质)的UI元素
1 2 |
private readonly IndexedSet<ICanvasElement> m_LayoutRebuildQueue = new IndexedSet<ICanvasElement>(); private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>(); |
当一个UI元素需要更新布局和渲染时就加入对应的队列中,在PerformUpdate方法中对它们进行处理。
与布局和渲染更新有关的方法定义在接口ICanvasElement中
1 2 3 4 5 6 7 8 9 10 11 |
public interface ICanvasElement { Transform transform { get; } void Rebuild(CanvasUpdate executing); void LayoutComplete(); void GraphicUpdateComplete(); // due to unity overriding null check // we need this as something may not be null // but may be destroyed bool IsDestroyed(); } |
主要是Rebuild(CanvasUpdate executing)方法,这个方法负责此UI元素的布局或者渲染更新。CanvasUpdate枚举是PerformUpdate的不同阶段
1 2 3 4 5 6 7 8 9 10 11 12 |
// canvas的不同阶段,用来设置layout、graphic做不同的事情,具体看CanvasUpdateRegistry // prelayout -> layout -> postlayout -> prerender -> laterender =>=>=> RENDER CANVAS public enum CanvasUpdate { Prelayout = 0, Layout = 1, PostLayout = 2, PreRender = 3, LatePreRender = 4, MaxUpdateValue = 5 } |
在UI元素实现的Rebuild方法中,switch不同的阶段做不同处理。之所以要分阶段,是因为UI无论布局还是渲染的更新都需要按照一定顺序和阶段,才能保证结果正确。
PerformUpdate()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
// 在渲染canvas之前对canvas下的元素进行rebuild // 进行 prelayout -> layout -> postlayout -> prerender -> LatePreRender 流程 private void PerformUpdate() { UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout); CleanInvalidItems(); m_PerformingLayoutUpdate = true; // 重建layout // 根据父节点从少到多排序,先布局父节点少的,由少到多,由内到外 m_LayoutRebuildQueue.Sort(s_SortLayoutFunction); // prelayout -> layout -> postlayout for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++) // 不同的阶段 { for (int j = 0; j < m_LayoutRebuildQueue.Count; j++) { var rebuild = instance.m_LayoutRebuildQueue[j]; try { if (ObjectValidForUpdate(rebuild)) rebuild.Rebuild((CanvasUpdate)i); } catch (Exception e) { Debug.LogException(e, rebuild.transform); } } } // 布局完成 for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i) m_LayoutRebuildQueue[i].LayoutComplete(); // 清空队列,重置状态 instance.m_LayoutRebuildQueue.Clear(); m_PerformingLayoutUpdate = false; // 做剔除+裁剪 // now layout is complete do culling... ClipperRegistry.instance.Cull(); // 重建graphic m_PerformingGraphicUpdate = true; // prerender -> lateprerender for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++) { for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++) { try { var element = instance.m_GraphicRebuildQueue[k]; if (ObjectValidForUpdate(element)) element.Rebuild((CanvasUpdate)i); } catch (Exception e) { Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform); } } } // graphic设置mesh和材质完成 for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i) m_GraphicRebuildQueue[i].GraphicUpdateComplete(); // 清空队列,重置状态 instance.m_GraphicRebuildQueue.Clear(); m_PerformingGraphicUpdate = false; // 到了这里布局和网格、材质都设置完毕,后面canvas会渲染 // profiler可以不用管 UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout); } |
在PerformUpdate()方法中会调用UI元素的Rebuild方法,完成后调用两个Complate方法。
UI元素在必要的时候主动调用Register方法加入队列。以Graphic为例,在以下几种情况下会加入m_GraphicRebuildQueue
- OnRectTransformDimensionsChange,即大小改变的时候
- OnTransformParentChanged,即父节点层级改变的时候
- OnDidApplyAnimationProperties,即通过animation clip改变属性值的时候
- 设置材质的时候
- OnEnable
- OnValidate
- Reset
这些情况下,Graphic会调用CanvasUpdateRegistry的Register方法注册进队列,之后就会在管线的willRenderCanvases阶段执行Rebuild。
3. 自定义和扩展
综上所述,如果要自定义UI元素,至少要继承UIBehaviour(或者至少MonoBehaviour)并实现ICanvasElement接口,这样才能完美地更新UI元素的布局和渲染。
官方文档
https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/class-Canvas.html
https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/HOWTO-UIWorldSpace.html
https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/HOWTO-UIMultiResolution.html
https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/script-CanvasScaler.html