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);
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);
}
代码中先调用 OnPopulateMesh() 创建自己的网格,然后调用所有需要需修改网格的网格修饰器(IMeshModifier),通常效果组件(描边等效果组件-我之实现的 Unity Image 镜像 就是实现该接口)进行修改,最后放入CanvasRenderer。
这里使用VertexHelper是为了节省内存和CPU,它内部采用List容器对象池,将所有使用过的废弃数据都存储在对象池的容器中。
组件中,Image、RawImage、Text都override(重写)了OnPopulateMesh()函数,这些都需要有自己自定义的网格样式来构建不同类型的画面。其实CanvasRenderer和Canvas才是合并网格的关键,但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 there
if (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 masks
var 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 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 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 object
maskableTarget.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);
else
canvasRenderer.DisableRectClipping();
}
最后的操作是在CanvasRenderer中进行的,前面我们说CanvasRenderer的看不了源码。但可以很容易想到这里面的操作是什么,即计算两个四边形的相交点,再组合成裁剪后的内容。至此我们对UGUI的核心渲染流程有了一定的认识。其实并没有高深的算法或者技术,所有核心部分都围绕着如何构建网格、谁将重构,以及如何裁剪来进行的。很多性能的关键在于,如何减少重构次数,以及提高内存和CPU的使用效率。