当前位置: 首页 > news >正文

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

目录

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 = 0float 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);elses_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 hereif (m_ShouldRecalculateClipRects){MaskUtilities.GetRectMasksForClip(this, m_Clippers);m_ShouldRecalculateClipRects = false;}// get the compound rects from// the clippers that are validbool 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);elsecanvasRenderer.DisableRectClipping();
}

        最后操作是在 CanvasRenderer 中进行的。推测:计算两个四边形的相交点,再组合成裁切后的内容。

所有核心部分都围绕着如何构建网格、谁将重构,以及如何裁切来进行的。很多性能关键在于,如何减少重构次数,以及提高内存和 CPU 的使用效率。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • ctfshow-web入门-sql注入(web171-web175)
  • 【QT】qss
  • 01 LVS负载均衡群集
  • Android进程保活:如何让app一直运行
  • DynamicDataSource多数据源的管理,动态新增切换数据源
  • Linux 内核源码分析---文件系统关联与字符设备操作
  • windows环境下安装docker与jenkins进行单机简易安装
  • PHP智能问诊导诊平台-计算机毕业设计源码75056
  • 数据库分表
  • 【探索Linux】P.44(数据链路层 —— 以太网的帧格式 | MAC地址 | MTU | ARP协议)
  • 免费【2024】springboot 大学校园旧物捐赠网站的设计与实现
  • 跟《经济学人》学英文:2024年08月03日这期 GPT, Claude, Llama? How to tell which AI model is best
  • pfx如何配置到nginx中
  • RS485 CAN SPI IIC UART RS232这些通信协议传输距离、传输速度对比给出比较顺序-笔记(面试必备)
  • 二叉树的中序遍历 - 力扣(LeetCode)C语言
  • [原]深入对比数据科学工具箱:Python和R 非结构化数据的结构化
  • 2017前端实习生面试总结
  • Angular js 常用指令ng-if、ng-class、ng-option、ng-value、ng-click是如何使用的?
  • iBatis和MyBatis在使用ResultMap对应关系时的区别
  • Js基础——数据类型之Null和Undefined
  • laravel 用artisan创建自己的模板
  • Lucene解析 - 基本概念
  • Median of Two Sorted Arrays
  • nginx 配置多 域名 + 多 https
  • Spark学习笔记之相关记录
  • uni-app项目数字滚动
  • Web设计流程优化:网页效果图设计新思路
  • 阿里中间件开源组件:Sentinel 0.2.0正式发布
  • 百度地图API标注+时间轴组件
  • 分享自己折腾多时的一套 vue 组件 --we-vue
  • 记一次和乔布斯合作最难忘的经历
  • 深入体验bash on windows,在windows上搭建原生的linux开发环境,酷!
  • Oracle Portal 11g Diagnostics using Remote Diagnostic Agent (RDA) [ID 1059805.
  • 数据库巡检项
  • ​520就是要宠粉,你的心头书我买单
  • #前后端分离# 头条发布系统
  • (10)STL算法之搜索(二) 二分查找
  • (Redis使用系列) Springboot 实现Redis消息的订阅与分布 四
  • (附源码)计算机毕业设计ssm本地美食推荐平台
  • (每日一问)操作系统:常见的 Linux 指令详解
  • (区间dp) (经典例题) 石子合并
  • (十八)SpringBoot之发送QQ邮件
  • (四)JPA - JQPL 实现增删改查
  • (转)ABI是什么
  • (转)Linux整合apache和tomcat构建Web服务器
  • (转)项目管理杂谈-我所期望的新人
  • ... fatal error LINK1120:1个无法解析的外部命令 的解决办法
  • .NET CF命令行调试器MDbg入门(二) 设备模拟器
  • .NET Core IdentityServer4实战-开篇介绍与规划
  • .Net Core 微服务之Consul(三)-KV存储分布式锁
  • .net 程序发生了一个不可捕获的异常
  • .Net 垃圾回收机制原理(二)
  • .NET 使用 ILRepack 合并多个程序集(替代 ILMerge),避免引入额外的依赖
  • .NET 同步与异步 之 原子操作和自旋锁(Interlocked、SpinLock)(九)
  • .net开发引用程序集提示没有强名称的解决办法