Unity UGUI的核心渲染组件

embedded/2025/1/17 13:55:45/

目录

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


http://www.ppmy.cn/embedded/115505.html

相关文章

【AI大模型】LLM主流开源大模型介绍

目录 &#x1f354; LLM主流大模型类别 &#x1f354; ChatGLM-6B模型 2.1 训练目标 2.2 模型结构 2.3 模型配置(6B) 2.4 硬件要求 2.5 模型特点 2.6 衍生应用 &#x1f354; LLaMA模型 3.1 训练目标 3.2 模型结构 3.3 模型配置&#xff08;7B&#xff09; 3.4 硬件…

10 while和unitl循环结构语句

while和unitl循环结构语句 一、循环结构语句 ​ Shell编程中循环命令用于特定条件下决定某些语句重复执行的控制方式&#xff0c;有三种常用的循环语句&#xff1a;for、while和until。while循环和for循环属于“当型循环”&#xff0c;而until属于“直到型循环”。 二、详解…

当Navicat报错 Can not connect to MySQL server的解决方法!

今天运行数据库时突然弹出一个error&#xff1a; 原因&#xff1a;MySQL的服务没有打开&#xff0c;需要检查MySQL的开启状态即可。 具体做法&#xff1a; 1.右键“开始”&#xff0c;点击“计算机管理” 2. 选择“服务和应用程序”&#xff0c;并点击“服务” 3.在服务中找…

Spring Boot 整合 MyBatis 的详细步骤(两种方式)

1. Spring Boot 配置 MyBatis 的详细步骤 1、首先&#xff0c;我们创建相关测试的数据库&#xff0c;数据表。如下&#xff1a; CREATE DATABASE springboot_mybatis USE springboot_mybatisCREATE TABLE monster ( id int not null auto_increment, age int not null, birthda…

【STM32】TIM定时器定时中断与定时器外部时钟的使用

TIM定时器定时中断与定时器外部时钟的使用 一、TIM定时器简介1、TIM&#xff08;Timer&#xff09;定时器2、定时器类型3、高级定时器4、通用定时器5、基本定时器6、定时中断基本结构代码编写&#xff1a;定时中断/外部时钟定时中断 7、预分频器时序8、计数器时序9、计数器无预…

前端基于Rust实现的Wasm进行图片压缩的技术文档

在现代Web开发中&#xff0c;图片压缩是一个常见且重要的需求。随着WebAssembly&#xff08;Wasm&#xff09;技术的成熟&#xff0c;我们可以使用Rust语言编写高性能的图片压缩代码&#xff0c;并将其编译成Wasm模块在前端运行。相对于传统的后端压缩方案&#xff0c;可以减少…

关于http的206状态码和416状态码的意义、断点续传以及CORS使用Access-Control-Allow-Origin来允许跨域请求

一、关于http的206状态码和416状态码的意义及断点续传 HTTP 2xx范围内的状态码表明客户端发送的请求已经被服务器接受并且被成功处理了,HTTP/1.1 206状态码表示客户端通过发送范围请求头Range抓取到了资源的部分数据&#xff0c;一般用来解决大文件下载问题&#xff0c;一般CDN…

Flyway 与 Spring Boot 集成

Flyway 与 Spring Boot 集成 Flyway 是一个流行的数据库版本控制工具&#xff0c;它帮助开发者和运维人员管理数据库模式和数据变更。Flyway 可以自动执行数据库迁移脚本&#xff0c;确保数据库结构在不同环境中的一致性。Spring Boot 与 Flyway 的集成非常紧密&#xff0c;可…