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

Unity UGUI的核心渲染组件

目录

Graphic

Mask

RectMask2D

Graphic

在UGUI中常用的组件有Image、RawImage、Mask、RectMask2D、Text、InputField中,Image、RawImage、Text都继承自MaskableGraphic, MaskableGraphic 又继承自Graphic。所以Graphic是一个非常重要的类。让我们来对着Graphic的源码分析用作原理。

/// <summary>
/// Set all properties of the Graphic dirty and needing rebuilt.
/// Dirties Layout, Vertices, and Materials.
/// </summary>
public virtual void SetAllDirty()
{// Optimization: Graphic layout doesn't need recalculation if// the underlying Sprite is the same size with the same texture.// (e.g. Sprite sheet texture animation)if (m_SkipLayoutUpdate){m_SkipLayoutUpdate = false;}else{SetLayoutDirty();}if (m_SkipMaterialUpdate){m_SkipMaterialUpdate = false;}else{SetMaterialDirty();}SetVerticesDirty();
}/// <summary>
/// Mark the layout as dirty and needing rebuilt.
/// </summary>
/// <remarks>
/// Send a OnDirtyLayoutCallback notification if any elements are registered. See RegisterDirtyLayoutCallback
/// </remarks>
public virtual void SetLayoutDirty()
{if (!IsActive())return;LayoutRebuilder.MarkLayoutForRebuild(rectTransform);if (m_OnDirtyLayoutCallback != null)m_OnDirtyLayoutCallback();
}/// <summary>
/// Mark the vertices as dirty and needing rebuilt.
/// </summary>
/// <remarks>
/// Send a OnDirtyVertsCallback notification if any elements are registered. See RegisterDirtyVerticesCallback
/// </remarks>
public virtual void SetVerticesDirty()
{if (!IsActive())return;m_VertsDirty = true;CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);if (m_OnDirtyVertsCallback != null)m_OnDirtyVertsCallback();
}/// <summary>
/// Mark the material as dirty and needing rebuilt.
/// </summary>
/// <remarks>
/// Send a OnDirtyMaterialCallback notification if any elements are registered. See RegisterDirtyMaterialCallback
/// </remarks>
public virtual void SetMaterialDirty()
{if (!IsActive())return;m_MaterialDirty = true;CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);if (m_OnDirtyMaterialCallback != null)m_OnDirtyMaterialCallback();
}

SetAllDirty()方法将设置并通知元素重新布局、重新构建网格及材质球。该方法通知LayoutRebuilder布局管理类进行重新布局,在LayoutRebuilder.MarkLayout-ForRebuild()中,它调用CanvasUpdateRegistry.TryRegisterCanvasElementForLayoutRebuild()加入重构队伍,最终重构布局。

SetLayoutDirty()、SetVerticesDirty()、SetMaterialDirty()都调用了CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(),被调用时可以认为是通知它去重构网格,但它并没有立即重新构建。这边代码使用的也是标脏模式,这里就不详细解释该模式是什么有兴趣的去了解。当标脏后,是将需要重构的元件数据加入IndexedSet容器中,等待下次重构。

注意,CanvasUpdateRegistry只负责重构网格,并不负责渲染和合并。我们来看看CanvasUpdateRegistry的RegisterCanvasElementForGraphicRebuild()函数部分:

/// <summary>
/// Try and add the given element to the rebuild list.
/// Will not return if successfully added.
/// </summary>
/// <param name="element">The element that is needing rebuilt.</param>
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}/// <summary>
/// Try and add the given element to the rebuild list.
/// </summary>
/// <param name="element">The element that is needing rebuilt.</param>
/// <returns>
/// True if the element was successfully added to the rebuilt list.
/// False if either already inside a Graphic Update loop OR has already been added to the list.
/// </returns>
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;}return m_GraphicRebuildQueue.AddUnique(element);
}

InternalRegisterCanvasElementForGraphicRebuild()将元素放入重构队列中等待下一次重构。重构时的逻辑源码如下:

private static readonly Comparison<ICanvasElement> s_SortLayoutFunction = SortLayoutList;
private void PerformUpdate()
{UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);CleanInvalidItems();m_PerformingLayoutUpdate = true;m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++){UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);for (int j = 0; j < m_LayoutRebuildQueue.Count; j++){var rebuild = m_LayoutRebuildQueue[j];try{if (ObjectValidForUpdate(rebuild))rebuild.Rebuild((CanvasUpdate)i);}catch (Exception e){Debug.LogException(e, rebuild.transform);}}UnityEngine.Profiling.Profiler.EndSample();}for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)m_LayoutRebuildQueue[i].LayoutComplete();m_LayoutRebuildQueue.Clear();m_PerformingLayoutUpdate = false;UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Render);// now layout is complete do culling...UnityEngine.Profiling.Profiler.BeginSample(m_CullingUpdateProfilerString);ClipperRegistry.instance.Cull();UnityEngine.Profiling.Profiler.EndSample();m_PerformingGraphicUpdate = true;for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++){UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);for (var k = 0; k < m_GraphicRebuildQueue.Count; k++){try{var element = m_GraphicRebuildQueue[k];if (ObjectValidForUpdate(element))element.Rebuild((CanvasUpdate)i);}catch (Exception e){Debug.LogException(e, m_GraphicRebuildQueue[k].transform);}}UnityEngine.Profiling.Profiler.EndSample();}for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)m_GraphicRebuildQueue[i].GraphicUpdateComplete();m_GraphicRebuildQueue.Clear();m_PerformingGraphicUpdate = false;UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Render);
}

PerformUpdate为CanvasUpdateRegistry在重构调用时的逻辑。先将要重新布局的元素取出来,一个一个调用Rebuild函数重构,再对布局后的元素进行裁剪,裁剪后将布局中每个需要重构的元素取出来并调用Rebuild函数进行重构,最后做一些清理的事务。

我们再来看看Graphic的另一个重要的函数,即执行网格构建函数,代码如下:

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);
}

代码中先调用 OnPopulateMesh() 创建自己的网格,然后调用所有需要需修改网格的网格修饰器(IMeshModifier),通常效果组件(描边等效果组件-我之实现的 Unity Image 镜像 就是实现该接口)进行修改,最后放入CanvasRenderer

这里使用VertexHelper是为了节省内存和CPU,它内部采用List容器对象池,将所有使用过的废弃数据都存储在对象池的容器中。

组件中,Image、RawImage、Text都override(重写)了OnPopulateMesh()函数,这些都需要有自己自定义的网格样式来构建不同类型的画面。其实CanvasRendererCanvas才是合并网格的关键,但CanvasRenderer和Canvas并没有开源出来。

CanvasRenderer是每个绘制元素都必须有的组件,它是画布与渲染的连接组件,通过CanvasRenderer才能把网格绘制到Canvas画布上去。

虽然拿不到源码但是大致可以猜测这部分,无非就是每次重构时获取Canvas下面所有的CanvasRenderer实例,将它们的网格合并起来,仅此而已。因此关键还是要看如何减少重构次数、提高内存和提高CPU的使用效率。

Mask

Mask的遮罩功能是非常值得我们关注的部分

/// Stencil calculation time!
public virtual Material GetModifiedMaterial(Material baseMaterial)
{if (!MaskEnabled())return baseMaterial;var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);if (stencilDepth >= 8){Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);return baseMaterial;}int desiredStencilBit = 1 << stencilDepth;// if we are at the first level...// we want to destroy what is thereif (desiredStencilBit == 1){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;}//otherwise we need to be a bit smarter and set some read / write masksvar maskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit | (desiredStencilBit - 1), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));StencilMaterial.Remove(m_MaskMaterial);m_MaskMaterial = maskMaterial2;graphic.canvasRenderer.hasPopInstruction = true;var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.Replace, CompareFunction.Equal, 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));StencilMaterial.Remove(m_UnmaskMaterial);m_UnmaskMaterial = unmaskMaterial2;graphic.canvasRenderer.popMaterialCount = 1;graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);return m_MaskMaterial;
}

Mask组件调用模板材质球(baseMaterial)来构建一个自己的材质球(m_MaskMaterial),因此它使用了实时渲染中的模板方法来裁剪不需要显示的部分,所有在Mask组件后面的物体都会进行裁剪。可以说Mask是在GPU中做的裁剪,使用的方法是着色器中的模板方法。

RectMask2D

RectMask2D和Mask一样可以实现遮罩,但是工作原理并不一样让我们来看下RectMask2D的核心代码:

public virtual void PerformClipping()
{if (ReferenceEquals(Canvas, null)){return;}//TODO See if an IsActive() test would work well here or whether it might cause unexpected side effects (re case 776771)// 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 the mask is in ScreenSpaceOverlay/Camera render mode, its content is only rendered when its rect// overlaps that of the root canvas.RenderMode renderMode = Canvas.rootCanvas.renderMode;bool maskIsCulled =(renderMode == RenderMode.ScreenSpaceCamera || renderMode == RenderMode.ScreenSpaceOverlay) &&!clipRect.Overlaps(rootCanvasRect, true);if (maskIsCulled){// Children are only displayed when inside the mask. If the mask is culled, then the children// inside the mask are also culled. In that situation, we pass an invalid rect to allow callees// to avoid some processing.clipRect = Rect.zero;validRect = false;}if (clipRect != m_LastClipRectCanvasSpace){foreach (IClippable clipTarget in m_ClipTargets){clipTarget.SetClipRect(clipRect, validRect);}foreach (MaskableGraphic maskableTarget in m_MaskableTargets){maskableTarget.SetClipRect(clipRect, validRect);maskableTarget.Cull(clipRect, validRect);}}else if (m_ForceClip){foreach (IClippable clipTarget in m_ClipTargets){clipTarget.SetClipRect(clipRect, validRect);}foreach (MaskableGraphic maskableTarget in m_MaskableTargets){maskableTarget.SetClipRect(clipRect, validRect);if (maskableTarget.canvasRenderer.hasMoved)maskableTarget.Cull(clipRect, validRect);}}else{foreach (MaskableGraphic maskableTarget in m_MaskableTargets){//Case 1170399 - hasMoved is not a valid check when animating on pivot of the objectmaskableTarget.Cull(clipRect, validRect);}}m_LastClipRectCanvasSpace = clipRect;m_ForceClip = false;UpdateClipSoftness();
}

从代码中可以看出ReckMask2D会先计算并设置clipRect裁剪范围,在对所有子节点设置裁剪操作。

使用

MaskUtilities.GetRectMasksForClip(this, m_Clippers);

来获取所有有关联的RectMask2D 范围,然后由

Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);

获得所有需要裁剪的对象,实际上是计算出不需要裁剪的部分,剩下的都进行最后的裁剪:

foreach (IClippable clipTarget in m_ClipTargets)
{clipTarget.SetClipRect(clipRect, validRect);
}

对所有需要裁剪的UI元素进行裁剪操作。其中SetClipRect裁剪操作的源码如下:

/// <summary>
/// See IClippable.SetClipRect
/// </summary>
public virtual void SetClipRect(Rect clipRect, bool validRect)
{if (validRect)canvasRenderer.EnableRectClipping(clipRect);elsecanvasRenderer.DisableRectClipping();
}

最后的操作是在CanvasRenderer中进行的,前面我们说CanvasRenderer的看不了源码。但可以很容易想到这里面的操作是什么,即计算两个四边形的相交点,再组合成裁剪后的内容。至此我们对UGUI的核心渲染流程有了一定的认识。其实并没有高深的算法或者技术,所有核心部分都围绕着如何构建网格、谁将重构,以及如何裁剪来进行的。很多性能的关键在于,如何减少重构次数,以及提高内存和CPU的使用效率。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Linux 内存管理机制概述
  • 整合多方大佬博客以及视频 一文读懂 servlet
  • 数值计算 --- 平方根倒数快速算法(中)
  • 邮件安全治理
  • CVE-2024-2389 未经身份验证的命令注入
  • (PySpark)RDD实验实战——取一个数组的中间值
  • 树和二叉树的概念以及结构
  • Flink难点和高阶面试题:Flink的状态管理机制如何保证数据处理的准确性和完整性
  • 解决Mac下Vscode编译运行C语言程序会自动生成DSYM文件夹的问题
  • spring-boot-maven-plugin插件打包和java -jar命令执行原理
  • C语言中数据类型
  • Java ETL - Apache Beam 简介
  • CQRS模型解析
  • Git换行符自动转换参数core.autocrlf的用法
  • 第一个Web项目(java+servlet+jsp)
  • 8年软件测试工程师感悟——写给还在迷茫中的朋友
  • ES6, React, Redux, Webpack写的一个爬 GitHub 的网页
  • Java 23种设计模式 之单例模式 7种实现方式
  • java B2B2C 源码多租户电子商城系统-Kafka基本使用介绍
  • Java应用性能调优
  • Joomla 2.x, 3.x useful code cheatsheet
  • Ruby 2.x 源代码分析:扩展 概述
  • Sublime text 3 3103 注册码
  • 笨办法学C 练习34:动态数组
  • 前端
  • 网络应用优化——时延与带宽
  • 微信开源mars源码分析1—上层samples分析
  • 我建了一个叫Hello World的项目
  • 吴恩达Deep Learning课程练习题参考答案——R语言版
  • LevelDB 入门 —— 全面了解 LevelDB 的功能特性
  • ​520就是要宠粉,你的心头书我买单
  • #1015 : KMP算法
  • (04)Hive的相关概念——order by 、sort by、distribute by 、cluster by
  • (C++20) consteval立即函数
  • (Oracle)SQL优化技巧(一):分页查询
  • (Redis使用系列) Springboot 实现Redis消息的订阅与分布 四
  • (Redis使用系列) Springboot 使用redis实现接口Api限流 十
  • (ZT)北大教授朱青生给学生的一封信:大学,更是一个科学的保证
  • (多级缓存)多级缓存
  • (二刷)代码随想录第16天|104.二叉树的最大深度 559.n叉树的最大深度● 111.二叉树的最小深度● 222.完全二叉树的节点个数
  • (附源码)springboot电竞专题网站 毕业设计 641314
  • (含笔试题)深度解析数据在内存中的存储
  • (十七)Flink 容错机制
  • (算法)大数的进制转换
  • (一)基于IDEA的JAVA基础12
  • (原創) 如何將struct塞進vector? (C/C++) (STL)
  • (转) Android中ViewStub组件使用
  • (转)重识new
  • .gitignore文件_Git:.gitignore
  • .NET Core 控制台程序读 appsettings.json 、注依赖、配日志、设 IOptions
  • .NET Core 中的路径问题
  • .NET MAUI学习笔记——2.构建第一个程序_初级篇
  • .NET 动态调用WebService + WSE + UsernameToken
  • .NET 给NuGet包添加Readme
  • .Net 路由处理厉害了