系列
MaskableGraphic
BaseClass: Graphic
Interface: IClippable、IMaskable、IMaterialModifier
简介
前置:Graphic源码剖析
MaskableGraphic在 Graphic的基础上实现了裁剪与遮罩功能。
这主要是由 IClippable、IMaskable 两个接口来实现的。
在Graphic更新材质的流程中有提及Mask。Graphic 可以理解成由骨头和皮肤所组成,骨头即顶点信息所构建的网格(Mesh),皮肤则是依附于Mesh的材质和纹理。实际上Mesh是不可见的,对于可见物的处理(例如Mask遮罩剔除)都是针对于Material。
理解清楚IClippable与IMaskable相关的组件原理便是理解MaskableGraphic的关键
RectMask2D 矩形裁剪
BaseClass: UIBehaviour
Interface: IClipper、ICanvasRaycastFilter
Intro: 这是UGUI提供的不依赖于Graphic的裁剪组件,它的原理在于设置IClippable组件中canvasRenderer.EnableRectClipping 来实现矩形裁剪效果
说到 IClipper(裁剪者)与 IClippable(可裁剪对象),那必须以RectMask2D组件为例来讲解
RectMask2D的工作原理:
- RectMask2D是IClipper,当启动时(Enable)先向ClipperRegistry中注册自己,然后会调用其所有子节点下IClippable 组件的RecalculateClipping方法,将其添加进最近父节点中的RectMask2D中(这是为了避免各种嵌套带来的浪费)
// MaskableGraphic 中更新裁剪者的方法
private void UpdateClipParent()
{
var newParent = (maskable && IsActive()) ? MaskUtilities.GetRectMaskForClippable(this) : null;
// if the new parent is different OR is now inactive
if (m_ParentMask != null && (newParent != m_ParentMask || !newParent.IsActive()))
{
m_ParentMask.RemoveClippable(this);
UpdateCull(false);
}
// don't re-add it if the newparent is inactive
if (newParent != null && newParent.IsActive())
newParent.AddClippable(this);
m_ParentMask = newParent;
}
-
当Canvas进行刷新的时候(CanvasUpdateSystem),会调用所有启用中的IClipper,执行Cull 操作,遍历执行 IClipper.PerformClipping
ClipperRegistry.instance.Cull();
m_Clippers[i].PerformClipping();
-
PerformClipping : 目的在于更新IClippable中用于裁剪的Rect
首先会借助MaskUtilities、Clipping 寻找的最小的裁剪框clipRect
接着会遍历自身下所有的IClippable组件(由IClippable.RecalculateClipping 添加)设置clipRect
clipTarget.SetClipRect(clipRect, validRect) validRect:用于判断裁剪框是否可用(长宽>0)
canvasRenderer.EnableRectClipping(clipRect) MaskableGraphic 中设置裁剪框
最后会判断是否改变IClippable中cull的状态
canvasRenderer.cull = cull;
在此流程期间RectMask2D会优化处理过程:
1.记录上次的clipRect来判断裁剪矩形是否发生变化,从而省略没必要的重新裁剪。
m_LastClipRectCanvasSpace = clipRect;
2.裁剪层的子集合会因为父级的裁剪而被裁剪,因此可以传递无效的rect来避免重复的处理。
clipTarget.Cull(maskIsCulled ? Rect.zero : clipRect,maskIsCulled ? false : validRect)
IMaskable基于Material的遮罩
MaskableGraphic 中IMaskable实现:
- 当Enable时,若该物体自身含有Mask组件则会调用其子节点路径下所有IMaskable组件方法。
protected override void OnEnable()
{
base.OnEnable();
m_ShouldRecalculateStencil = true; //控制是否从新计算遮罩深度->改变遮罩材质
UpdateClipParent();
SetMaterialDirty();
if (GetComponent<Mask>() != null)
{
// 设置Mask遮罩状态
MaskUtilities.NotifyStencilStateChanged(this);
}
}
//IMaskable 接口方法
public virtual void RecalculateMasking()
{
m_ShouldRecalculateStencil = true;
SetMaterialDirty();
}
- 在Grahpic材质重建的过程中会调用其上所有IMaterialModifier组件方法来处理最终的渲染材质materialForRendering
//MaskableGraphic 中IMaterialModifier 组件方法
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
var toUse = baseMaterial;//来自Graphic的基础材质
if (m_ShouldRecalculateStencil)
{
var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
m_ShouldRecalculateStencil = false;
}
// 优化了遮罩处理,如果已经启用了Mask组件,则不必再次做重复的事情
Mask maskComponent = GetComponent<Mask>();
if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
{
//借助StencilMaterial生产一个新的遮罩材质,这里是使用list存储避免重复生成一样的材质
var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMat;
toUse = m_MaskMaterial;
}
return toUse;//返回新生成的遮罩材质
}