遮罩,顾名思义是一种可以掩盖其它元素的控件。常用于修改其它元素的外观,或限制元素的形状。比如ScrollView或者圆头像效果都有用到遮罩功能。本系列文章希望通过阅读UGUI源码的方式,来探究遮罩的实现原理,以及通过Unity不同遮罩之间实现方式的对比,找到每一种遮罩的最佳使用场合。

本文是UGUI遮罩系列的第二篇,专门解读RectMask2D遮罩。第一篇是【UGUI源码分析】Unity遮罩之Mask详细解读,对Mask感兴趣的同学可以跳转过去看一下。后续还会有Rect Mask 2D和Mask对比分析的文章发出,敬请期待~

本文使用的源码与内置资源均基于Unity2019.4版本

RectMask2D

查阅Unity的官方文档,对RectMask2D有如下定义

RectMask2D 是一个类似于(Mask) 控件的遮罩控件。遮罩将子元素限制为父元素的矩形。与标准的遮罩控件不同,这种控件有一些限制,但也有许多性能优势。

工作流大致如下

  1. C#:找出父物体中所有RectMask2D覆盖区域的交集
  2. C#:所有继承MaskGraphic的子物体组件调用方法设置裁剪区域(SetClipRect)传递给Shader
  3. Shader:接收到矩形区域,片元着色器中判断像素是否在矩形区域内,不在则透明度设置为0
  4. Shader:丢弃掉alpha小于0.001的元素

RectMask2D的实现原理,概括起来就是先将那些不在其矩形范围内的元素透明度设置为0,然后通过Shader丢弃掉透明度小于0.001的元素。接下来我们通过阅读源码来查看它是如何实现这一流程的

源码

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就是用来设置裁剪矩形的方法。在探讨它的具体实现前,我们先来看下这个方法是何时被调用的

  1. CanvasUpdateRegistry是UI控件注册自己需要重建的地方,在每次画布开始绘制前会调用CanvasUpdateRegistry的PerformUpdate方法来重建所有注册的控件

  2. 在这之中也会触发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();
    }
    } // ...
    }
  3. 每个RectMask2D都会在OnEnable中将自己注册到ClipperRegistry中

    protected override void OnEnable()
    {
    base.OnEnable();
    m_ShouldRecalculateClipRects = true;
    ClipperRegistry.Register(this); // 注册自己
    MaskUtilities.Notify2DMaskStateChanged(this);
    }

然后我们来看RectMask2D的PerformClipping具体实现

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();
}
  1. 通过MaskUtilities.GetRectMasksForClip沿着层级结构往上找到所有的RectMask2D,然后利用Clipping.FindCullAndClipWorldRect计算这些RectMask2D所表示的矩形的交集,求出一个重叠矩形
  2. 遍历所有的被裁减/被遮掩对象,通过SetClipRect为它们设置裁剪矩形。这些被裁剪对象是通过RectMask2D的AddClippable方法注册进来的
  3. 值得一提的是,在方法的末尾还调用了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官网下载,下载时选择"Built in shaders"

UI-Default.shader的部分源码如下所示

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
}
}
}
  1. _ClipRect就是用来接收CanvasRenderer传递进来的裁剪矩形的
  2. 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;
}

而Unity2019.4版本实现类似逻辑的代码如下所示,在实现矩形裁剪算法的同时,还新增了对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

首先来看新的算法是如何实现矩形裁剪的

  1. 判断点是否在矩形内,主要是依据_ClipRect和IN.mask.xy。_ClipRect.xy是矩形左下角坐标,_ClipRect.zw是矩形右上角坐标,_ClipRect.zw - _ClipRect.xy就是一条从左下角指向右上角的向量,记为A。mask.xy经过如下所示代码进行转换,表示的是点到矩形左下角的向量B与点到矩形右上角的向量C之和,记为D

    v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw
    // 可以看做是
    (v.vertex.xy - clampedRect.xy) + (v.vertex.xy - clampedRect.zw)

    以点在矩形外为例,对应向量情况如下图所示。A - D得到的向量的xy分量,一定有一个是负值。像下图这种情况,A的x分量是小于D的x分量的。这很好理解,因为如果一个点在矩形外的话,它要么在整个矩形的左侧或右侧,要么在上侧或下侧,点到矩形左下角和右上角的向量在x或y方向上一定有一个是同向的。在矩形的左侧和右侧时,点到矩形左下角和右上角的向量x方向上距离之和一定是大于矩形的宽度的。在矩形的上侧和下侧时,点到矩形左下角和右上角的向量y方向上距离之和一定是大于矩形的高度的。

  2. 因此如果点在矩形外,saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy))得到的值一定小于0。saturate是把对应值限制到[0,1]之间。即小于0的值均为0,大于1的值均为1。从而将在矩形外的片元透明度设置为0,实现裁剪效果

  3. 如果点在矩形内,点到矩形左下角和右上角的向量一定是反向的,两个向量相加得到的向量D,它的xy分量一定小于矩形的宽度和高度。所以saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy))得到的值一定是正值,在矩形内的片元透明度大于0,可以显示出来

Unity2019.4的矩形裁剪算法和老版本不同的一个原因应该就是为了能够对Softness进行处理,我们再来看看RectMask2D的Softness是起什么作用的,又是如何起作用的

  1. 代码中_UIMaskSoftnessX,_UIMaskSoftnessY的值一定是大于0的,在RectMask2D的softness属性的set访问器中有做限制。因此在计算透明度的时候乘上IN.mask.zw不会改变结果的正负值,小于0的仍然是看不到,影响的只是能看到的片元的透明度

    public Vector2Int softness
    {
    get { return m_Softness; }
    set
    {
    m_Softness.x = Mathf.Max(0, value.x);
    m_Softness.y = Mathf.Max(0, value.y);
    MaskUtilities.Notify2DMaskStateChanged(this);
    }
    }
  2. _UIMaskSoftnessX,_UIMaskSoftnessY的值越大,IN.mask.zw的值越小。当softness的值不为0时,会起到降低透明度的作用

  3. 上面也提到,当点在矩形内时,点到矩形左下角和右上角的向量是反向的。而点越靠近矩形的中心,抵消的越彻底,两个向量之和的xy分量越小。saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy)) * IN.mask.zw)计算得到的透明度值越大。

    因此越靠近矩形中心的片元透明度越高,透明度由内到外逐渐递减,呈现一种缓慢变透明的遮罩效果,更加柔和。如下图所示,左侧是未设置softness的效果,右侧是设置softness为(10, 10)的效果

参考

【UGUI源码分析】Unity遮罩之RectMask2D详细解读的更多相关文章

  1. JVM源码分析之堆外内存完全解读

    JVM源码分析之堆外内存完全解读   寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...

  2. 【UGUI源码分析】Unity遮罩之Mask详细解读

    遮罩,顾名思义是一种可以掩盖其它元素的控件.常用于修改其它元素的外观,或限制元素的形状.比如ScrollView或者圆头像效果都有用到遮罩功能.本系列文章希望通过阅读UGUI源码的方式,来探究遮罩的实 ...

  3. HashMap源码分析(史上最详细的源码分析)

    HashMap简介 HashMap是开发中使用频率最高的用于映射(键值对 key value)处理的数据结构,我们经常把hashMap数据结构叫做散列链表: ObjectI entry<Key, ...

  4. Spring源码分析——BeanFactory体系之接口详细分析

    Spring的BeanFactory的继承体系堪称经典.这是众所周知的!作为Java程序员,不能错过! 前面的博文分析了Spring的Resource资源类Resouce.今天开始分析Spring的I ...

  5. Spring源码分析——BeanFactory体系之抽象类、类分析(二)

    上一篇分析了BeanFactory体系的2个类,SimpleAliasRegistry和DefaultSingletonBeanRegistry——Spring源码分析——BeanFactory体系之 ...

  6. Spring源码分析——BeanFactory体系之抽象类、类分析(一)

    上一篇介绍了BeanFactory体系的所有接口——Spring源码分析——BeanFactory体系之接口详细分析,本篇就接着介绍BeanFactory体系的抽象类和接口. 一.BeanFactor ...

  7. VueJs 源码分析 ---(一) 整体对 vuejs 框架的理解

    vue-2.x SourceCode vue 2.x 源码解析 关于vue,以及为何要来写这份源码解析的原因 笔者从最开始接触到 vue 应该还是在 15年 10月份左右,当时听说 前端圈中发生很多的 ...

  8. HDFS源码分析之FSImage文件内容(一)总体格式

    FSImage文件是HDFS中名字节点NameNode上文件/目录元数据在特定某一时刻的持久化存储文件.它的作用不言而喻,在HA出现之前,NameNode因为各种原因宕机后,若要恢复或在其他机器上重启 ...

  9. Spark源码分析之四:Stage提交

    各位看官,上一篇<Spark源码分析之Stage划分>详细讲述了Spark中Stage的划分,下面,我们进入第三个阶段--Stage提交. Stage提交阶段的主要目的就一个,就是将每个S ...

随机推荐

  1. buu 不一样的flag

    一.查壳 二.拖入ida,分析 从这里和51到53行的代码,基本判断这是一个迷宫题,并且是5行5列的一个迷宫.我当时感觉到一个奇怪的地方是 第一个,我自己想明白是因为可能是int型,数字占了4个字节, ...

  2. 如何使用powershell操作json对象

    读取Json文件 $dataTransformerDirPath为读取Json文件的目录 点击查看代码 # 读取文件 UTF8-NOBOM function ReadFile($path) { ret ...

  3. 备战-Java 并发

    备战-Java 并发 谁念西风独自凉,萧萧黄叶闭疏窗 简介:备战-Java 并发. 一.线程的使用 有三种使用线程的方法: 实现 Runnable 接口: 实现 Callable 接口: 继承 Thr ...

  4. [刘阳Java]_大型电商网站架构技术演化历程

    今年的双十一已经过去一段,作为技术小咖啡,我们先说一下大型电商网站的特点:高并发,大流量,高可用,海量数据.下面就说说大型网站的架构演化过程,它的技术架构是如何一步步的演化的 1. 早期的网站架构 初 ...

  5. SpringBoot整合Shiro实现权限控制

    目录 1.SpringBoot整合Shiro 1.1.shiro简介 1.2.代码的具体实现 1.2.1.Maven的配置 1.2.2.整合需要实现的类 1.2.3.项目结构 1.2.4.ShiroC ...

  6. P3209-平面图判定

    平面图 平面图就是所有点的连边不相交的图.(当然是在你尽量想让它不相交的情况下).这一点可以大概理解成拓扑图的性质,即每连一条边就会将某个区域进行分割--很明显,如果两个点分别处在两个不可达的区域,它 ...

  7. Qt 入门 ---- 如何在程序窗口显示图片?

    步骤: 1. 选择资源(准备图片) 2. 加载资源(导入图片) 3. 使用资源(显示图片) 具体操作流程: ① 从网上寻找合适的图片素材,下载到本地,在项目根目录下创建一个images文件夹存储程序中 ...

  8. Docker隔离技术

    前言 Docker系列文章: 此篇是Docker系列的第九篇,之前的文章里面或多或少的提到Docker的隔离技术,但是没有很清楚的去聊这个技术,但是经过这么多文章大家一定对Docker使用和概念有了一 ...

  9. Find-set-root-ignore-floppies-ignore-cd /bootmgr 解决办法(用win 7安装盘)

    出现标题此种现象,一般是mgr引导程序丢失有关,现在排除bootloader 选择是硬盘引导, 然后修改为cd盘引导,并重启,在win7安装程序启动后,选择修复系统. 打开相应的commnd,并执行如 ...

  10. phpcms开发使用

    二次开发入口文件: 1.dirname(__FILE___) 函数返回的是脚本所在在的路径 2.__FILE__ 当前运行文件的完整路径和文件名.如果用在被包含文件中,则返回被包含的文件名. 3.DI ...