简介
RectMask2D 是一个类似 Mask 控件的遮罩控件。遮罩将子元素限制为父元素的矩形。与标准的遮罩控件不同,这种控件有一些限制,但也有许多性能优势。
实现原理
概述
ReckMask2D 的工作流大致如下
- C#:找出父物体中所有RectMask2D覆盖区域的交集
- C#:所有继承MaskGraphic的子物体组件调用方法设置裁剪区域(SetClipRect)传递给Shader
- Shader:接收到矩形区域,片元着色器中判断像素是否在矩形区域内,不在则透明度设置为0
- Shader:丢弃掉alpha小于0.001的元素
概括起来就是先将那些不在其矩形范围内的元素透明度设置为0,然后通过Shader丢弃掉透明度小于0.001的元素。
深层剖析
我们可以从UGUI的源码中对RectMask2D进行分析。
UGUI中定义了两个接口,IClipper和IClippable,分别表示裁剪对象和被裁剪对象。RectMask2D实现了IClipper接口,MaskableGraphic则实现了IClippable接口。
/// <summary>
/// Interface that can be used to recieve clipping callbacks as part of the canvas update loop.
/// </summary>
public interface IClipper
{
void PerformClipping();
}
/// <summary>
/// Interface for elements that can be clipped if they are under an IClipper
/// </summary>
public interface IClippable
{
GameObject gameObject { get; }
void RecalculateClipping();
RectTransform rectTransform { get; }
void Cull(Rect clipRect, bool validRect);
void SetClipRect(Rect value, bool validRect);
void SetClipSoftness(Vector2 clipSoftness);
}
其中IClipper的PerformClipping就是用来设置裁剪矩形的方法。在探讨它的具体实现前,我们先来看下这个方法是何时被调用的:
- CanvasUpdateRegistry是UI控件注册自己需要重建的地方,在每次画布开始绘制前会调用CanvasUpdateRegistry的PerformUpdate方法来重建所有注册的控件
- 在这之中也会触发ClipperRegistry的Cull方法,ClipperRegistry是所有IClipper注册的地方,在ClipperRegistry的Cull方法中会调用所有注册者的PerformClipping方法
public class ClipperRegistry
{
// ...
readonly IndexedSet<IClipper> m_Clippers = new IndexedSet<IClipper>();
/// <summary>
/// Perform the clipping on all registered IClipper
/// </summary>
public void Cull()
{
for (var i = 0; i < m_Clippers.Count; ++i)
{
m_Clippers[i].PerformClipping();
}
}
// ...
}
- 每个RectMask2D都会在OnEnable中将自己注册到ClipperRegistry中
protected override void OnEnable()
{
base.OnEnable();
m_ShouldRecalculateClipRects = true;
ClipperRegistry.Register(this); // 注册自己
MaskUtilities.Notify2DMaskStateChanged(this);
}
然后我们来看RectMask2D的PerformClipping具体实现:
- 通过MaskUtilities.GetRectMasksForClip沿着层级结构往上找到所有的RectMask2D,然后利用Clipping.FindCullAndClipWorldRect计算这些RectMask2D所表示的矩形的交集,求出一个重叠矩形
- 遍历所有的被裁减/被遮掩对象,通过SetClipRect为它们设置裁剪矩形。这些被裁剪对象是通过RectMask2D的AddClippable方法注册进来的
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 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);
}
}
// ...
UpdateClipSoftness();
}
- 值得一提的是,在方法的末尾还调用了UpdateClipSoftness,这个方法比较简单,就是再遍历所有的被裁减/被遮掩对象一遍,调用它们的SetClipSoftness方法
public virtual void UpdateClipSoftness()
{
// ...
foreach (IClippable clipTarget in m_ClipTargets)
{
clipTarget.SetClipSoftness(m_Softness);
}
foreach (MaskableGraphic maskableTarget in m_MaskableTargets)
{
maskableTarget.SetClipSoftness(m_Softness);
}
}
实现裁剪的关键就在于SetClipRect和SetClipSoftness的实现了,对于MaskableGraphic,它默认实现的SetClipRect和SetClipSoftness方法如下所示:
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
if (validRect)
canvasRenderer.EnableRectClipping(clipRect);
else
canvasRenderer.DisableRectClipping();
}
public virtual void SetClipSoftness(Vector2 clipSoftness)
{
canvasRenderer.clippingSoftness = clipSoftness;
}
其中CanvasRenderer是挂在对象上的CanvasRenderer组件。由于Unity并未将CanvasRenderer开源,所以其内部实现我们无从知晓。根据Unity API文档可知,EnableRectClipping的作用是启用矩形裁剪。将对位于指定矩形外的几何形状进行裁剪(不渲染)。DisableRectClipping对应的就是禁用该裁剪。通过查阅资料,得知是使用Shader实现的矩形裁剪。查看UI默认使用的Shader是UI/Default,这是Unity的内置Shader,源码可以在Unity官网下载:
Shader "UI/Default"
{
Properties
{
// ...
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
}
SubShader
{
Pass
{
Name "Default"
CGPROGRAM
// ...
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
half4 mask : TEXCOORD2;
UNITY_VERTEX_OUTPUT_STEREO
};
sampler2D _MainTex;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float _UIMaskSoftnessX;
float _UIMaskSoftnessY;
v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
float4 vPosition = UnityObjectToClipPos(v.vertex);
OUT.worldPosition = v.vertex;
OUT.vertex = vPosition;
float2 pixelSize = vPosition.w;
pixelSize /= float2(1, 1) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
float2 maskUV = (v.vertex.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);
OUT.texcoord = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
OUT.mask = half4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy)));
OUT.color = v.color * _Color;
return OUT;
}
fixed4 frag(v2f IN) : SV_Target
{
half4 color = IN.color * (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd);
#ifdef UNITY_UI_CLIP_RECT
half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy)) * IN.mask.zw);
color.a *= m.x * m.y;
#endif
#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
return color;
}
ENDCG
}
}
}
- _ClipRect就是用来接收CanvasRenderer传递进来的裁剪矩形的
- UNITY_UI_CLIP_RECT是控制是否开启矩形裁剪的宏,经过测试验证,EnableRectClipping会定义宏,而DisableRectClipping会禁用该宏的定义
在老版本Unity中,主要逻辑就是通过UnityGet2DClipping判断片元是否在矩形内,如果不在则返回0,否则返回1。不在矩形内的片元透明度将被设置为0。然后通过clip将透明度小于0.001的片元丢弃掉:
#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif
#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
inline float UnityGet2DClipping (in float2 position, in float4 clipRect)
{
float2 inside = step(clipRect.xy, position.xy) * step(position.xy, clipRect.zw);
return inside.x * inside.y;
}
而在新版本Unity中,实现类似逻辑的代码如下所示,在实现矩形裁剪算法的同时,还新增了对Softness柔软度的处理:
// vs
OUT.mask = half4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy)));
// fs
#ifdef UNITY_UI_CLIP_RECT
half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy)) * IN.mask.zw);
color.a *= m.x * m.y;
#endif
我在实习时曾经写过一些UI相关的Shader,在写完后发现应用了Shader的UI元素会不受RectMask2D作用。本以为是模板缓冲的问题,后来经过查阅才知道RectMask2D并没有使用模板测试orz