《Unity3D高级编程 主程手记》第四章 用户界面(四) UGUI 核心源码

avatar
作者
猴君
阅读量:0

目录

4.4.1 UGUI 核心源码结构

4.4.2 Culling 模块

4.4.3 Layout 布局模块

CanvasScaler的核心函数

4.4.4 MaterialModifiers、SpecializedCollections、Utility

4.4.5 VertexModifiers

4.4.6 核心渲染类

RegisterCanvasElementForGraphicRebuild()

重构时的逻辑

执行网格构建函数

 Mask 遮罩部分

核心部分

RectMask2D

核心部分源码

SetClipRect  


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 的使用效率。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!