目录
[4.4.1 UGUI 核心源码结构](#4.4.1 UGUI 核心源码结构)
[4.4.2 Culling 模块](#4.4.2 Culling 模块)
[4.4.3 Layout 布局模块](#4.4.3 Layout 布局模块)
[4.4.4 MaterialModifiers、SpecializedCollections、Utility](#4.4.4 MaterialModifiers、SpecializedCollections、Utility)
[4.4.5 VertexModifiers](#4.4.5 VertexModifiers)
[4.4.6 核心渲染类](#4.4.6 核心渲染类)
RegisterCanvasElementForGraphicRebuild()
[Mask 遮罩部分](#Mask 遮罩部分)
4.4.1 UGUI 核心源码结构
- Culling 裁剪
- Layer 布局
- MaterialModifiers 材质球修改器
- SpecializedCollections 收集
- Utility 实用工具
- Vertexmodifiers 顶点修改器
4.4.2 Culling 模块
Culling 里是对模型裁剪的工具类,大都用在了 Mask (遮罩)上,只有 Mask 才有裁剪的需求。(这里的 Mask 是指 RectMask2D)
cs
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的核心函数
cs
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, 它们的共同关键代码如下:
cs
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 相对比较重要,它是基础类,也存放了核心算法。
cs
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()
cs
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;
}
重构时的逻辑
cs
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 函数 进行重构,最后做一些清理的事务。
执行网格构建函数
cs
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 容器对象池,将所有使用过的废弃的数据都存储在里对象池的容器中,当需要时再拿旧的继续使用。
cs
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() 函数。
cs
protected override void OnPopulateMesh(VertexHelper toFill)
其实 CanvasRenderer 和 Canvas 才是合并网格的关键,但 CanvasRenderer 和 Canvas 并没有开源出来。
推测:合并部分是每次重构时获取 Canvas 下面所有的 CanvasRenderer 实例,将它们的网格合并起来。
关键还是要看如何减少重构次数、提高内存和提高 CPU 的使用效率。
Mask 遮罩部分
核心部分
cs
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
核心部分源码
cs
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 会先计算并设置裁切的范围,再对所有子节点调用裁切操作。
cs
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
cs
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
if (validRect)
canvasRenderer.EnableRectClipping(clipRect);
else
canvasRenderer.DisableRectClipping();
}
最后操作是在 CanvasRenderer 中进行的。推测:计算两个四边形的相交点,再组合成裁切后的内容。
所有核心部分都围绕着如何构建网格、谁将重构,以及如何裁切来进行的。很多性能关键在于,如何减少重构次数,以及提高内存和 CPU 的使用效率。