一.前言
在Unity UGUI的性能优化中经常能看见,使用RectMask2D替换Mask,因为Mask会产生额外的Drwcall,而RectMask2D不会产生额外的Drwcall。但是RectMask2D的性能一定比Mask好吗?
二.Mask源码分析
public class Mask : UIBehaviour, ICanvasRaycastFilter, IMaterialModifier
Mask代码在190行左右。
Mask本身不具有绘制属性不继承Graphic,必须依赖带有Graphic的组件,节点如果没有如Image Text等组件Mask无法添加。
ICanvasRaycastFilter是表明Mask是射线可以投射的目标,ScreenPointToWorldPointInRectangle就是根据摄像机的位置和屏幕位置创建射线,根据RectTransform创建Plane,调用Plane.Raycast检测PM2.5与射线是否相交。
public static bool ScreenPointToWorldPointInRectangle(
RectTransform rect,
Vector2 screenPoint,
Camera cam,
out Vector3 worldPoint)
{
worldPoint = (Vector3) Vector2.zero;
Ray ray = RectTransformUtility.ScreenPointToRay(cam, screenPoint);
Plane plane = new Plane(rect.rotation * Vector3.back, rect.position);
float enter = 0.0f;
if ((double) Vector3.Dot(Vector3.Normalize(rect.position - ray.origin), plane.normal) != 0.0 && !plane.Raycast(ray, out enter))
return false;
worldPoint = ray.GetPoint(enter);
return true;
}
IMaterialModifier,得到修改的材质,在Canvas重绘时,如果Material是dirty会调用此方法更新材质,Mask实现的重点在GetModifiedMaterial上
Material GetModifiedMaterial(Material baseMaterial)
MaskEnabled()在获取材质之前确保物体active,有graphic渲染组件
FindRootSortOverrideCanvas找到节点上面最近是overrideSorting覆盖排序的的Canvas
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
if (!MaskEnabled())//MaskEnabled() { return IsActive() && graphic != null; }
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;
}
GetStencilDepth()向上寻找Mask,stencilDepth=父节点上Mask的数量,如果stencilDepth>=8,由于模板测试的值在0-255之间
怎么让层与层之间做的遮罩效果?
对应一般的只有一个Mask的情况
maskMaterial会完全替代模板缓冲区的值,设置为1,根据是否显示遮罩图片决定是否进行颜色写入
unmaskMaterial 在其子节点渲染之后,设置模板值为0
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;
}
被Mask裁剪的图形MaskableGraphic实现了GetModifiedMaterial接口,MaskableGraphic的子类有Image,Text等
MaskUtilities.FindRootSortOverrideCanvas的节点的第一个带有Canvas且overrideSorting为true的父节点
GetModifiedMaterial根据深度创建材质设置模板值,(1 << m_StencilValue) - 1后面解析
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
var toUse = baseMaterial;
if (m_ShouldRecalculateStencil)
{
if (maskable)
{
var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
m_StencilValue = MaskUtilities.GetStencilDepth(transform, rootCanvas);
//向上寻找Mask组件,返回深度
}
else
m_StencilValue = 0;
m_ShouldRecalculateStencil = false;
}
if (m_StencilValue > 0 && !isMaskingGraphic)
{
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;
}
继续分析Mask的GetModifiedMaterial方法
模板测试过程,模板值stencilID与readMask做&运算,结果与缓冲区模板值根据比较规则比较,写入时stencilID与writeMask做&运算后才能进行写入,更新缓冲区模板值
第2层Mask
若stencilDepth为1,desiredStencilBit 为2
desiredStencilBit | (desiredStencilBit - 1)为 2|1=>10|01为3,模板值为3,和readMask desiredStencilBit - 1(1)做与运算结果为1,和之前的第1层Mask写入的1等于,替换,写入时模板值3与writeMask desiredStencilBit | (desiredStencilBit - 1)即3做与运算为3
第3层Mask
desiredStencilBit | (desiredStencilBit - 1),若stencilDepth为2,desiredStencilBit 为4
desiredStencilBit | (desiredStencilBit - 1)为 4|3=>100|011为7,模板值为7,和readMask desiredStencilBit - 1(3)做与运算结果为3,和之前的第1层Mask写入的 3 相等,替换,写入时模板值7与writeMask desiredStencilBit | (desiredStencilBit - 1)即7做与运算为7
n层Mask
stencilID为2n-1-1,与readMask后2n-2-1(与上一层n-1写入的值2n-2-1一致),writeMask写入2n-1-1
int desiredStencilBit = 1 << stencilDepth;
var maskMaterial2 =
StencilMaterial.Add(baseMaterial, desiredStencilBit | (desiredStencilBit - 1), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, (readMask)desiredStencilBit - 1, (writeMask)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));
//mask的第2个材质,恢复之前的模板值
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial2;
graphic.canvasRenderer.popMaterialCount = 1;
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);
return m_MaskMaterial;
绘制顺序
第1Mask写入值为1,Mask1下的图形被裁剪,stencilID为1
第2层Mask2_1比较值为1,写入值为3,Mask2_1下的图形被裁剪,stencilID为3
第3层Mask3比较值为3,写入值为7,Mask3下的图形被裁剪,stencilID为5
深度迭代完成,向上执行回溯模板值
执行Mask3的第2个材质,写回之前的模板值3
执行Mask2_1的第2个材质,写回之前的模板值1
第2层Mask2_2比较值为1,写入值为3,Mask2下的图形被裁剪,模板值为3
执行Mask2_2的第2个材质,写回之前的模板值1
执行Mask的第2个材质,写回之前的模板值0
这样先执行Mask2_1对应区域模板值为3,Mask2_1下的图片裁剪,模板值回溯为1,
Mask2_2更改对应区域为3,Mask2_2下的图片裁剪
如果不回溯,Mask2_1对应区域模板值为3,接着Mask2_2对应区域模板值为3,那么Mask2_2图片会显示Mask2_1对应的区域
三.RectMask2D源码分析
基本原理
RectMask2D在C#层获取所有父节点的RectMask2D加入List,遍历数组得到canvasRect得到重叠的Rect传递给canvasRenderer.EnableRectClipping(rect),会将rect发生给顶点着色器,在片元着色器对片元根据Rect进行discard
public class RectMask2D : UIBehaviour, IClipper, ICanvasRaycastFilter
核心方法在PerformClipping
public virtual void PerformClipping()
{
if (ReferenceEquals(Canvas, null))
{
return;//如果父节点上没有Canvas不执行
}
//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);//得到该节点的所有ClipRect
m_ShouldRecalculateClipRects = false;//在有RectMask添加激活移除时会设置为true
}
// get the compound rects from
// the clippers that are valid
bool validRect = true;
Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
//找到重叠公共的Rect范围,即Rect.Min的最大值,Rect.Max的最小值,组成新的Rect
// 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);//canvas为3d空间的不执行裁剪,且clipRect与该节点Rect重叠
if (maskIsCulled)
{
clipRect = Rect.zero;
validRect = false;
}
if (clipRect != m_LastClipRectCanvasSpace)//clipRect没有改变,不执行
{
foreach (IClippable clipTarget in m_ClipTargets)
{
clipTarget.SetClipRect(clipRect, validRect);//设置RectMask2D的裁剪范围
}
foreach (MaskableGraphic maskableTarget in m_MaskableTargets)
{
maskableTarget.SetClipRect(clipRect, validRect);
//设置maskableTarget的裁剪范围,设置canvasRenderer.EnableRectClipping(clipRect)
maskableTarget.Cull(clipRect, validRect);
//public virtual void Cull(Rect clipRect, bool validRect)
//{
// var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true);
//每一个maskableTarget的Rect会与clipRect判断是否重叠
// UpdateCull(cull);//重绘给maskableTarget
// }
}
}
else if (m_ForceClip)//添加删除RectMask2D强制更新
{
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;
}
canvasRenderer.cull物体是否被完全剔除,为true将不会创建UI网格
clipRect.Overlaps(rootCanvasRect, true),clipRect和UI裁剪对象重叠,cull为false,不执行UpdateCull
public virtual void Cull(Rect clipRect, bool validRect)
{
var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true);
UpdateCull(cull);
}
private void UpdateCull(bool cull)
{
if (canvasRenderer.cull != cull)
{
canvasRenderer.cull = cull;
UISystemProfilerApi.AddMarker("MaskableGraphic.cullingChanged", this);
m_OnCullStateChanged.Invoke(cull);
OnCullingChanged();
}
}
性能对比
在滚动视图添加10×100=1000个图片
使用Mask的性能为
使用RectMask2D的性能为
如果滚动视图有多个渲染对象,推荐使用Mask
如果遮罩下有1-2个对象,可以使用RectMask2D