UGUI核心源码剖析
- Unity
- 27天前
- 76热度
- 0评论
前言

Core模块主要为Culling(裁剪)、Layout(布局)、MaterialModifiers(材质球修改器)、SpecializedCollection(收集),Utility(实用工具)、vc(顶点修改器)。
一、Culling裁剪模块

Culling是对模型裁剪的工具类,大都用到Mask遮罩上,只要Mask才有裁剪的需求。Cliping类FindCullAndClipWorldRect就是将很多 RectMask2D 重叠部分,计算出它们的重叠部分的区域。
public static Rect FindCullAndClipWorldRect(List<RectMask2D> rectMaskParents, out bool validRect)
{
if (rectMaskParents.Count == 0)
{
validRect = false;
return new Rect();
}
Rect current = rectMaskParents[0].canvasRect;
Vector4 offset = rectMaskParents[0].padding;
float xMin = current.xMin + offset.x;
float xMax = current.xMax - offset.z;
float yMin = current.yMin + offset.y;
float yMax = current.yMax - offset.w;
var rectMaskParentsCount = rectMaskParents.Count;
for (var i = 1; i < rectMaskParentsCount; ++i)
{
current = rectMaskParents[i].canvasRect;
offset = rectMaskParents[i].padding;
if (xMin < current.xMin + offset.x)
xMin = current.xMin + offset.x;
if (yMin < current.yMin + offset.y)
yMin = current.yMin + offset.y;
if (xMax > current.xMax - offset.z)
xMax = current.xMax - offset.z;
if (yMax > current.yMax - offset.w)
yMax = current.yMax - offset.w;
}
validRect = xMax > xMin && yMax > yMin;
return validRect ? new Rect(xMin, yMin, xMax - xMin, yMax - yMin) : new Rect();
}
二、Layout布局模块

Layout主要功能是布局方面的,包括横向布局,纵向布局,方格布局等。
除了布局内容之外,其余还有3个文件,CanvasScaler,AspectRatioFitter,ContentSizeFitter是调整自适应功能。
ContentSizeFitter处理的是内容自适应,AspectRatioFitter是朝向自适应,其中包括以长度为基准,以宽度为基准,以父节点为基准,以外层父节点为基准的四种自适应方式。
CanvasScaler操作的是Canvas整个画布针对不同的屏幕进行自适应调整。一共有三种ScreenMathMode模式,不同模式对应屏幕不同的适应算法。包括优先匹配长或宽,最小化固定拉伸,最大化固定拉伸
// 处理不同的ScaleMode
protected virtual void Handle()
{
if (m_Canvas == null || !m_Canvas.isRootCanvas)
return;
if (m_Canvas.renderMode == RenderMode.WorldSpace)
{
HandleWorldCanvas();
return;
}
switch (m_UiScaleMode)
{
case ScaleMode.ConstantPixelSize: HandleConstantPixelSize(); break;
case ScaleMode.ScaleWithScreenSize: HandleScaleWithScreenSize(); break;
case ScaleMode.ConstantPhysicalSize: HandleConstantPhysicalSize(); break;
}
}
// 处理缩放根据ScreenSize
protected virtual void HandleScaleWithScreenSize()
{
Vector2 screenSize = m_Canvas.renderingDisplaySize;
// Multiple display support only when not the main display. For display 0 the reported
// resolution is always the desktops resolution since its part of the display API,
// so we use the standard none multiple display method. (case 741751)
int displayIndex = m_Canvas.targetDisplay;
if (displayIndex > 0 && displayIndex < Display.displays.Length)
{
Display disp = Display.displays[displayIndex];
screenSize = new Vector2(disp.renderingWidth, disp.renderingHeight);
}
float scaleFactor = 0;
switch (m_ScreenMatchMode)
{
case ScreenMatchMode.MatchWidthOrHeight:
{
// We take the log of the relative width and height before taking the average.
// Then we transform it back in the original space.
// the reason to transform in and out of logarithmic space is to have better behavior.
// If one axis has twice resolution and the other has half, it should even out if widthOrHeight value is at 0.5.
// In normal space the average would be (0.5 + 2) / 2 = 1.25
// In logarithmic space the average is (-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);
}
三、VertexModifiers顶点修改器类

VertexModifiers主要用于修改图形网格,尤其是在UI元素网格生成完毕后进行二次修改。
其中BaseMeshEffect是抽象基类, 提供所有在修改UI元素网格时所需要的变量和接口。
IMeshModifier是关键接口,渲染核心类Graphic中会获取所有拥有这个接口的组件,然后依次遍历并调用ModifyMesh接口来触发改变图像网格的效果。
Outline(包边框),Shadow(阴影),PositionAsUV1(位置UV) 都继承了 BaseMeshEffect 基类,并实现了关键接口 ModifyMesh。其中 Outline 继承自 Shadow。
核心代码:在原有的Mesh顶点基础上,加入新的顶点,这些新的顶点复制了原来的顶点数据,修改颜色并向外扩充,使得原图形外渲染出外描边或者阴影
protected void ApplyShadowZeroAlloc(List<UIVertex> verts, Color32 color, int start, int end, float x, float y)
{
UIVertex vt;
var neededCapacity = verts.Count + end - start;
if (verts.Capacity < neededCapacity)
verts.Capacity = neededCapacity;
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;
}
}
四、工具类

MaterialModifiers(材质球修改器), SpecializedCollections(特殊收集器), Utility(使用工具)是其他模块所依赖的工具。
IMaterialModifier 是一个接口类,为Mask 遮罩修改材质球所准备的,但所用方法都需要各自实现
IndexedSet 是一个容器,在很多核心代码上都有使用,它加速了移除元素的速度,以及加速了元素包含判断。
ListPool是List容器对象池,ObjectPool是普通对象池,很多代码上都用到了它们,对象池让内存利用率更高。
VertexHelper 特别重要,它是用来存储生成 Mesh 网格需要的所有数据,由于在Mesh生成的过程中顶点的生成频率非常高,因此 VertexHelper 存储了 Mesh 的所有相关数据的同时,用上面提到的ListPool和ObjectPool做为对象池来生成和销毁,使得数据高效得被重复利用,不过它并不负责计算和生成 Mesh,计算和生成由各自图形组件来完成,它只为它们提供计算后的数据存储服务。
五、核心渲染类
我们常用的Image、RawImage、Mask、RectMask2D、Text、InputField中,Image,RawImage,Text都是继承了MaskableGraphic,而MaskableGraphic又继承自Graphic类。除了这几个类外, CanvasUpdateRegistry是存储和管理所有课绘制元素大的管理类。其中Graphic类非常重要,是基础类也存在核心算法。
5.1 Graphic.SetAllDirty()
- SetLayoutDirty: 设置布局脏数据
- SetMaterialDirty: 设置材质球脏数据
- SetVerticesDirty:设置顶点脏数据
- SetRaycastDirty:设置射线脏数据
SetLayoutDirty、SetMaterialDirty、SetVerticesDirty都调用了CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild,它被调用时可以认为是通知它去重新重构Mesh,但它并没有立即重新构建,而是将需要重构的元件数据加入到IndexedSet容器中,等待下次重构。
public virtual void SetAllDirty()
{
if (m_SkipLayoutUpdate)
{
m_SkipLayoutUpdate = false;
}
else
{
SetLayoutDirty();
}
if (m_SkipMaterialUpdate)
{
m_SkipMaterialUpdate = false;
}
else
{
SetMaterialDirty();
}
SetVerticesDirty();
SetRaycastDirty();
}
// 设置布局脏数据
public virtual void SetLayoutDirty()
{
if (!IsActive())
return;
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
if (m_OnDirtyLayoutCallback != null)
m_OnDirtyLayoutCallback();
}
// 设置材质球脏数据
public virtual void SetMaterialDirty()
{
if (!IsActive())
return;
m_MaterialDirty = true;
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyMaterialCallback != null)
m_OnDirtyMaterialCallback();
}
// 设置顶点脏数据
public virtual void SetVerticesDirty()
{
if (!IsActive())
return;
m_VertsDirty = true;
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyVertsCallback != null)
m_OnDirtyVertsCallback();
}
// 设置raycast响应事件脏数据
public void SetRaycastDirty()
{
if (m_RaycastTargetCache != m_RaycastTarget)
{
if (m_RaycastTarget && isActiveAndEnabled)
GraphicRegistry.RegisterRaycastGraphicForCanvas(canvas, this);
else if (!m_RaycastTarget)
GraphicRegistry.UnregisterRaycastGraphicForCanvas(canvas, this);
}
m_RaycastTargetCache = m_RaycastTarget;
}
5.2 CanvasUpdateRegistry() 重构Mesh
CanvasUpdateRegistry:负责重构Mesh网格。
// IndexedSet:重构数据
private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>();
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;
}
return m_GraphicRebuildQueue.AddUnique(element);
}
5.3 Graphic.PerformUpdate()
重构调用中的逻辑,先将需要重新布局的元素取出来一个个调用Rebuild函数,在对布局的元素进行裁剪,裁剪后对布局中每个需要重构的元素取出来调用Rebuild函数进行重构。
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);
}
5.4 Graphic.DoMeshGeneration()
DoMeshGeneration是元素重构Rebuild会调用到的,先调用OnPopulateMesh(pɒpjuleɪt发音:pɒpjuleɪt)创建自己的Mesh网格,然后调用所有需要修改 Mesh 的修改者(IMeshModifier)也就是网格后处理组件(描边等效果组件)进行修改,最后放入 CanvasRenderer。
其中 CanvasRenderer 是每个绘制元素都必须有的组件,它是画布与渲染的连接组件,通过 CanvasRenderer 我们才能把网格绘制到 Canvas 画布上去。
Imge,RawImag,Text都override重写OnpopulateMesh函数。
protected override void OnPopulateMesh(VertexHelper toFill)
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);
}
六、Mask和RectMask2D
6.1 Mask
Mask 组件调用了模板材质球构建了一个自己的材质球,使用了实时渲染中的模板方法来裁切不需要显示的部分,在 Mask 组件后面的物体都会进行裁切。Mask 是在 GPU 中做的裁切,使用的方法是着色器中的模板方法。
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;
6.2 RectMask2D
RectMask2D 会先计算并设置裁切的范围,再对所有子节点调用裁切操作。
public virtual void PerformClipping()
{
if (m_ShouldRecalculateClipRects)
{
// 获取了所有有关联的 RectMask2D 遮罩
MaskUtilities.GetRectMasksForClip(this, m_Clippers);
m_ShouldRecalculateClipRects = false;
}
bool validRect = true;
// 获取了所有有关联的 RectMask2D 遮罩范围
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);
}
// 裁切源码
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
if (validRect)
canvasRenderer.EnableRectClipping(clipRect);
else
canvasRenderer.DisableRectClipping();
}
