目录
4.4.4 MaterialModifiers、SpecializedCollections、Utility
RegisterCanvasElementForGraphicRebuild()
4.4.1 UGUI 核心源码结构
- Culling 裁剪
- Layer 布局
- MaterialModifiers 材质球修改器
- SpecializedCollections 收集
- Utility 实用工具
- Vertexmodifiers 顶点修改器
4.4.2 Culling 模块
Culling 里是对模型裁剪的工具类,大都用在了 Mask (遮罩)上,只有 Mask 才有裁剪的需求。(这里的 Mask 是指 RectMask2D)
public static Rect FindCullAndClipWorldRect(List<RectMask2D> rectMaskParents, out bool validRect) { if (rectMaskParents.Count == 0) { validRect = false; return new Rect(); } var compoundRect = rectMaskParents[0].canvasRect; for (var i = 0; i < rectMaskParents.Count; ++i) compoundRect = RectIntersect(compoundRect, rectMaskParents[i].canvasRect); var cull = compoundRect.width <= 0 || compoundRect.height <= 0; if (cull) { validRect = false; return new Rect(); } Vector3 point1 = new Vector3(compoundRect.x, compoundRect.y, 0.0f); Vector3 point2 = new Vector3(compoundRect.x + compoundRect.width, compoundRect.y + compoundRect.height, 0.0f); validRect = true; return new Rect(point1.x, point1.y, point2.x - point1.x, point2.y - point1.y); } private static Rect RectIntersect(Rect a, Rect b) { float xMin = Mathf.Max(a.x, b.x); float xMax = Mathf.Min(a.x + a.width, b.x + b.width); float yMin = Mathf.Max(a.y, b.y); float yMax = Mathf.Min(a.y + a.height, b.y + b.height); if (xMax >= xMin && yMax >= yMin) return new Rect(xMin, yMin, xMax - xMin, yMax - yMin); return new Rect(0f, 0f, 0f, 0f); }
上述代码中的函数为 Clipping 类里的函数,第一个函数 FindCullAndClipWorldRect() 的含义是计算 RectMask2D 重叠部分的区域。第二个函数 RectIntersect() 为第一个函数提供了计算服务,其含义是计算两个矩阵的重叠部分。
4.4.3 Layout 布局模块
- 横向布局
- 纵向布局
- 方格布局
- ContentSizeFitter 内容的自适应
- AspectRatioFitter 朝向的自适应,包括以长度、宽度、父节点、外层父节点为基准这四种类型的自适应
- CanvasScaler 操作 Canvas 整个画布针对不同屏幕进行的自适应调整
CanvasScaler的核心函数
protected virtual void HandleScaleWithScreenSize() { Vector2 screenSize = new Vector2(Screen.width, Screen.height); float scaleFactor = 0; switch (m_ScreenMatchMode) { case ScreenMatchMode.MatchWidthOrHeight: { //在取平均值之前,我们先取相对宽度和高度的对数 //然后将其转换到原始空间 //进出对数空间的原因是具有更好的行为 //如果一个轴的分辨率是两倍,而另一个轴的分辨率是一半,那么 widthOrHeight 的值为0.5时,它应该平整 //在正常空间中,平均值为(0.5 + 2) / 2 = 1.25 //在对数空间中,平均值是(-1 + 1) / 2 = 0 float logWidth = Mathf.Log(screenSize.x / m_ReferenceResolution.x, kLogBase); float logHeight = Mathf.Log(screenSize.y / m_ReferenceResolution.y, kLogBase); float logWeightedAverage = Mathf.Lerp(logWidth, logHeight, m_MatchWidthOrHeight); scaleFactor = Mathf.Pow(kLogBase, logWeightedAverage); break; } case ScreenMatchMode.Expand: { scaleFactor = Mathf.Min(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y); break; } case ScreenMatchMode.Shrink: { scaleFactor = Mathf.Max(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y); break; } } SetScaleFactor(scaleFactor); SetReferencePixelsPerUnit(m_ReferencePixelsPerUnit); }
在不同 ScreenMathMode 模式下 ,CanvasScaler 类对屏幕的适应算法包括优先匹配长或宽的、最小化固定拉伸及最大化固定拉伸三种数学计算方式。其中在优先匹配长或宽算法中介绍了使用 Log 和 Pow 来计算缩放比例可以表现的更好。
4.4.4 MaterialModifiers、SpecializedCollections、Utility
- IMaterialModifier 是一个接口类,是为 Mask 修改材质球所准备的,所用方法需要各自实现。
- IndexedSet 是一个容器,在很多核心代码上都可使用,它加速了移除元素的速度,并且加快了元素是否包含某个元素的判断操作。
- ListPool 是 List 容器对象池,ObjectPool 是普通对象池,很多代码上都用到了它们,它们让内存利用率更高。
- VertexHelper 特别重要,它是用来存储生成网格(Mesh)需要的所有数据。
在网格生成的过程中,由于顶点的生成频率非常高,因此 VertexHelper 在存储了网格的所有相关数据的同时,用上面提到的 ListPool 和 ObjectPool 做为对象池来生成和回收,使得数据被高效地重复利用,不过它并不负责计算和生成网格,网格的计算和生成由各自图形组件来完成,它只提供计算后的数据存储服务。
4.4.5 VertexModifiers
- VertexModifiers 模块的作用是作为顶点修改器。顶点修改器为效果制作提供了更多基础方法和规则。
- VertexModifiers 模块主要用于修改图形网格,在 UI 元素网格生成完毕后可对其进行二次修改。
- 其中 BaseMeshEffect 是抽象基类,提供所有在修改 UI 元素网格时所需的变量和接口。
- IMeshModifier 是关键接口,在渲染核心类 Graphic 中会获取所有拥有这个接口的组件,然后依次遍历并调用 ModifyMesh 接口来触发改变图像网格的效果。
当前在源码中拥有的二次效果包括 Outline(包边框)、Shadow(阴影)、PositionAsUV1(位置UV),都继承自 BaseMeshEffect 基类,并实现了关键接口 ModifyMesh。其中 Outline 继承自 Shadow, 它们的共同关键代码如下:
protected void ApplyShadowZeroAlloc(List<UIVertex> verts, Color32 color, int start, int end, float x, float y) { UIVertex vt; var neededCpacity = verts.Count * 2; if (verts.Capacity < neededCpacity) verts.Capacity = neededCpacity; for (int i = start; i < end; ++i) { vt = verts[i]; verts.Add(vt); Vector3 v = vt.position; v.x += x; v.y += y; vt.position = v; var newColor = color; if (m_UseGraphicAlpha) newColor.a = (byte)((newColor.a * verts[i].color.a) / 255); vt.color = newColor; verts[i] = vt; } }
ApplyShadowZeroAlloc() 的作用是在原有的网格顶点基础上加入新的顶点,这些新的顶点复制了原来的顶点数据,修改颜色并向外扩充,使得在原图形外渲染出外描边或者阴影。
4.4.6 核心渲染类
我们常用的组件 Image、RawImage、Mask、RectMask2D、Text、InputField 中,Image、RawImage、Text 都是继承自 MaskableGraphic ,而 MaskableGraphic 又继承自 Graphic 类,因此 Graphic 相对比较重要,它是基础类,也存放了核心算法。
public virtual void SetAllDirty() { SetLayoutDirty(); //布局需重构 SetVerticesDirty(); //顶点需重构 SetMaterialDirty(); //材质球需重构 } public virtual void SetLayoutDirty() { if (!IsActive()) //是否激活 return; LayoutRebuilder.MarkLayoutForRebuild(rectTransform); //标记重构节点 if (m_OnDirtyLayoutCallback != null) //重构标记回调通知 m_OnDirtyLayoutCallback(); } public virtual void SetVerticesDirty() { if (!IsActive()) //是否激活 return; m_VertsDirty = true; //设置重构标记 CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this); //将自己注册到重构队列中 if (m_OnDirtyVertsCallback != null) //回调通知 m_OnDirtyVertsCallback(); } public virtual void SetMaterialDirty() { if (!IsActive()) //是否激活 return; m_MaterialDirty = true;//标记重构 CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this); //将自己注册到重构队列中 if (m_OnDirtyMaterialCallback != null) //回调通知 m_OnDirtyMaterialCallback(); }
SetLayoutDirty、SetVerticesDirty、SetMaterialDirty 都调用了CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(),被调用时可以认为是通知它去重新重构网格,但它并没有立即重新构建,而是将需要重构的元件数据加入到 IndexedSet 容器中,等待下次重构。
注意:CanvasUpdateRegistry 只负责重构网格,并不负责渲染和合并。
RegisterCanvasElementForGraphicRebuild()
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element) { instance.InternalRegisterCanvasElementForGraphicRebuild(element); } public static bool TryRegisterCanvasElementForGraphicRebuild(ICanvasElement element) { return instance.InternalRegisterCanvasElementForGraphicRebuild(element); } private bool InternalRegisterCanvasElementForGraphicRebuild(ICanvasElement element) { if (m_PerformingGraphicUpdate) { Debug.LogError(string.Format("Trying to add {0} for graphic rebuild while we are already inside a graphic rebuild loop. This is not supported.", element)); return false; } if (m_GraphicRebuildQueue.Contains(element)) return false; m_GraphicRebuildQueue.Add(element); return true; }
重构时的逻辑
private static readonly Comparison<ICanvasElement> s_SortLayoutFunction = SortLayoutList; private void PerformUpdate() { CleanInvalidItems(); m_PerformingLayoutUpdate = true; //布局重构 m_LayoutRebuildQueue.Sort(s_SortLayoutFunction); 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(); //元素重构 m_PerformingGraphicUpdate = true; 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); } } } for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i) m_GraphicRebuildQueue[i].LayoutComplete(); instance.m_GraphicRebuildQueue.Clear(); m_PerformingGraphicUpdate = false; }
PerformUpdate 为 CanvasUpdateRegistry 在重构调用中的逻辑。先将需要重新布局的元素取出来,一个个调用 Rebuild 函数重构,再对布局后的元素进行裁剪,裁剪后对布局中每个需要重构的元素取出来调用 Rebuild 函数进行重构,最后做一些清理的事务。
执行网格构建函数
private void DoMeshGeneration() { if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0) OnPopulateMesh(s_VertexHelper); else s_VertexHelper.Clear(); // clear the vertex helper so invalid graphics dont draw. var components = ListPool<Component>.Get(); GetComponents(typeof(IMeshModifier), components); for (var i = 0; i < components.Count; i++) ((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper); ListPool<Component>.Release(components); s_VertexHelper.FillMesh(workerMesh); canvasRenderer.SetMesh(workerMesh); }
此段代码是 Graphic 构建网格的部分,先调用 OnPopulateMesh 创建自己的网格,然后调用所有需要修改网格的修改者(IMeshModifier),也就是效果组件(描边等效果组件)进行修改,最后放入 CanvasRenderer 。
其中 CanvasRenderer 是每个绘制元素都必须有的组件,它是画布与渲染的连接组件,通过 CanvasRenderer 才能把网格绘制到 Canvas 画布上去。
这里使用 VertexHelper 是为了节省内存和 CPU 消耗,它内部采用 List 容器对象池,将所有使用过的废弃的数据都存储在里对象池的容器中,当需要时再拿旧的继续使用。
public class VertexHelper : IDisposable { private List<Vector3> m_Positions = ListPool<Vector3>.Get(); private List<Color32> m_Colors = ListPool<Color32>.Get(); private List<Vector2> m_Uv0S = ListPool<Vector2>.Get(); private List<Vector2> m_Uv1S = ListPool<Vector2>.Get(); private List<Vector3> m_Normals = ListPool<Vector3>.Get(); private List<Vector4> m_Tangents = ListPool<Vector4>.Get(); private List<int> m_Indicies = ListPool<int>.Get(); }
组件中,Image、RawImage、Text 都 override(重写)了 OnPopulateMesh() 函数。
protected override void OnPopulateMesh(VertexHelper toFill)
其实 CanvasRenderer 和 Canvas 才是合并网格的关键,但 CanvasRenderer 和 Canvas 并没有开源出来。
推测:合并部分是每次重构时获取 Canvas 下面所有的 CanvasRenderer 实例,将它们的网格合并起来。
关键还是要看如何减少重构次数、提高内存和提高 CPU 的使用效率。
Mask 遮罩部分
核心部分
var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0); StencilMaterial.Remove(m_MaskMaterial); m_MaskMaterial = maskMaterial; var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0); StencilMaterial.Remove(m_UnmaskMaterial); m_UnmaskMaterial = unmaskMaterial; graphic.canvasRenderer.popMaterialCount = 1; graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0); return m_MaskMaterial;
Mask 组件调用了模板材质球构建了一个自己的材质球,因此它使用了实时渲染中的模板方法来裁切不需要显示的部分,所有在 Mask 组件后面的物体都会进行裁切。 可以说 Mask 是在 GPU 中做的裁切,使用的方法是着色器中的模板方法。
RectMask2D
核心部分源码
public virtual void PerformClipping() { // if the parents are changed // or something similar we // do a recalculate here if (m_ShouldRecalculateClipRects) { MaskUtilities.GetRectMasksForClip(this, m_Clippers); m_ShouldRecalculateClipRects = false; } // get the compound rects from // the clippers that are valid bool validRect = true; Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect); if (clipRect != m_LastClipRectCanvasSpace) { for (int i = 0; i < m_ClipTargets.Count; ++i) m_ClipTargets[i].SetClipRect(clipRect, validRect); m_LastClipRectCanvasSpace = clipRect; m_LastClipRectValid = validRect; } for (int i = 0; i < m_ClipTargets.Count; ++i) m_ClipTargets[i].Cull(m_LastClipRectCanvasSpace, m_LastClipRectValid); }
RectMask2D 会先计算并设置裁切的范围,再对所有子节点调用裁切操作。
MaskUtilities.GetRectMasksForClip(this, m_Clippers); //获取了所有有关联的 RectMask2D 遮罩范围 Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect); //计算了需要裁切的部分,实际上是计算了不需要裁切的部分,其他部分都进行裁切。 for (int i = 0; i < m_ClipTargets.Count; ++i) m_ClipTargets[i].SetClipRect(clipRect, validRect); //对所有需要裁切的UI元素,进行裁切操作。
SetClipRect
public virtual void SetClipRect(Rect clipRect, bool validRect) { if (validRect) canvasRenderer.EnableRectClipping(clipRect); else canvasRenderer.DisableRectClipping(); }
最后操作是在 CanvasRenderer 中进行的。推测:计算两个四边形的相交点,再组合成裁切后的内容。
所有核心部分都围绕着如何构建网格、谁将重构,以及如何裁切来进行的。很多性能关键在于,如何减少重构次数,以及提高内存和 CPU 的使用效率。