Unity 遮罩“绘制”图
前言
A Mask is not a visible UI control but rather a way to modify the appearance of a control’s child elements. the parent will be visible.
Mask 是一个不可见的用于控制子元素遮罩显示的一个组件,在 Unity UI 中分别有 Mask 和 RectMask2D 两种类型的 Mask 组件实现遮罩效果;这两个组件都有各自的使用场景,下面就来分析 Unity 中是如何应用这两个组件来为一副图像“绘制”遮罩效果。
IMaskable、IMaterialModifier、IClippable 和 IClipper
在分析这两个 Mask 组件之前,先来分别认识一下 IMaskable、IMaterialModifier、IClippable 以及 IClipper 接口。
IMaskable
实现了 IMaskable 接口的组件都可以被用来实现遮罩效果,它有一个方法 RecalculateMasking
用来计算当前元素及其子元素的 Mask。
IMaterialModifier
IMaterialModifier 接口通常用于一个 Graphic 在渲染之前修改其 Material,它也有一个方法需要实现 GetModifiedMaterial
,在这个方法中可以自定义修改渲染所用 Material 的实现方式。一个典型的使用就是 Graphic 在渲染时,会获取其对象上的所有的 IMaterialModifier,然后依次调用 GetModifiedMaterial
方法修改当前渲染所用的 Material。部分代码如下:
|
|
IClipper
实现了这个接口的组件能够接收到 Canvas 更新时裁剪的回调,它有一个需要实现的方法 PerformClipping
用于执行裁剪,这个方法在 Graphic Layout 重建之后以及在 Graphic 渲染重建之前执行。
IClippable
实现了这个接口的组件其所在的 UI 元素能够被父元素裁剪,这个父元素需绑定实现了 IClipper 接口的组件。
Mask
Mask 类继承自 UIBehaviour 类,因此它具有 Unity 完整生命周期回调,同时它实现了 ICanvasRaycastFilter 接口,所以也可作为 Unity Event System 中事件分发射线检测对象,还实现了 IMaterialModifier 接口,这样在当前对象上的 Graphic 被渲染的时候,就会通过 Mask 类中的 GetModifiedMaterial
方法来修改渲染所用的材质。
为对象绑定 Mask 组件,必须在同一个对象上绑定一个 Graphic,这是因为 Mask 组件依赖 Graphic 组件来实现遮罩。将 Mask 组件绑定到对象上,如下:
可以看到在 Unity 编辑器中有一个可编辑属性 Show Mask Graphic,这个属性对应了 Mask 类里面的 m_ShowMaskGraphic
变量,用来标记在遮罩渲染中是否输出遮罩所在的 Graphic。
下面从 Mask 类源码出发,来分析整个 Mask 组件实现遮罩效果的流程。首先看看类中的主要的成员变量:
属性 | 描述 |
---|---|
m_ShowMaskGraphic |
标记在遮罩渲染中是否输出遮罩所在的 Graphic |
m_Graphic |
Mask 关联的 Graphic |
m_MaskMaterial |
用于实现遮罩加入 Stencil Buffer 过后的 Material |
m_UnmaskMaterial |
用于取消遮罩恢复 Stencil Buffer 默认值过后的 Material |
m_RectTransform |
组件所在对象的 RectTransform |
再来看看类中的相关方法:
showMaskGraphic get/set 方法
用来获取或设置 m_ShowMaskGraphic
变量,当设置这个变量时,如果设置值改变会触发当前组件所在的 Graphic 重新构建操作,如下:
|
|
OnEnable 方法
在组件 OnEnable 的时候,回调用如下代码:
|
|
从代码中可以看出,首先设置了 Graphic 所在 CanvasRender 的 hasPopInstruction
值为 true
,并标记 Graphic 可以被重新构建;紧接着调用了 MaskUtilities 类的 NotifyStencilStateChanged
方法通知当前对象的所有绑定了实现了 IMaskable 接口组件的子元素计算它们的遮罩,MaskUtilities 类是实现遮罩效果常用的一个工具类,里面的方法在用到的时候会具体分析,先来看看 NotifyStencilStateChanged
方法如下:
|
|
代码很简单,通过遍历子元素上绑定的实现了 IMaskable 接口的组件,依次调用 RecalculateMasking
方法来计算遮罩相关数据。
OnDisable 方法
在 OnDisable 方法中,首先所在 Graphic 调用了
SetMaterialDirty
方法标记 Graphic 可以被重新构建;StencilMaterial 类是一个辅助类,管理用于实现遮罩效果的 Stencil Materil,其中具体的方法在用到时具体分析;第二步就是调用 StencilMaterial 类的
Remove
方法依次移除m_MaskMaterial
和m_UnmaskMaterial
;最后又一次调用 MaskUtilities 类的
NotifyStencilStateChanged
方法通知当前对象的所有绑定了实现了 IMaskable 接口组件的子元素再次计算它们的遮罩数据。
IsRaycastLocationValid 方法
这是 ICanvasRaycastFilter 接口实现的一个方法,用于 Unity Event System 中事件分发射线检测过程,在这个方法内部实现最终调用了 RectTransformUtility 类的静态方法 RectangleContainsScreenPoint
来确定当前所在的 RectTransform 是否包含了所指的屏幕上的点。
GetModifiedMaterial 方法
最后就是核心方法 GetModifiedMaterial
,它是实现遮罩的关键方法之一。
首先回顾一下一个 Graphic 重新构建的过程: 当可被重新构建标记之后,在 Canvas 的渲染过程中调用了 Graphic 的 Rebuild
方法,然后调用 UpdateMaterial
方法设置渲染 Graphic 用的 Material,此时获取 Material 如下:
|
|
Mask 需要和 Graphic 组件(具体来说是 MaskableGraphic 组件)一起绑定在同一个对象上才能发挥作用。当这个 MaskableGraphic 重新构建时,上面的代码会被调用用来获取渲染所用的材质,代码中遍历了当前对象上绑定的实现了 IMaterialModifier 接口的组件,然后调用其 GetModifiedMaterial
方法修改渲染用的 Material;这样的话 MaskableGraphic 和 Mask 组件的 GetModifiedMaterial
方法都会依次被调用。
下面就来看看这两个组件中对 GetModifiedMaterial
方法的实现吧!
- MaskableGraphic 类中的
GetModifiedMaterial
方法
一个 MaskableGraphic 可以同 Mask 组件一起绑定(如 Image 组件所在对象上绑定一个 Mask 组件)来实现遮罩效果,也可以作为一个单独的组件绑定在某个对象上,比如创建 Unity UI 中的 Image(Image 类继承自 MaskableGraphic)元素。在这两种不同情况下,GetModifiedMaterial
方法发挥的作用也不一样,具体来看看代码:
|
|
首先判断当前 MaskableGraphic 是否需要计算模板数据,需要就调用 MaskUtilities 类的 GetStencilDepth
计算模板深度 m_StencilValue
;GetStencilDepth
方法代码很简单,就是遍历计算对象的父元素看是否有绑定 Mask 组件,如果则 m_StencilValue
模板深度值加一。
若当前 MaskableGraphic 上绑定有 Mask 组件,那么当前方法也就执行完成了,返回的修改后的材质就是传入的材质(这种情况是作为 Mask 发挥作用的 Graphic 对象);
如果没有绑定 Mask 组件,那么会执行以下代码计算修改材质(这种情况常见场景是一个需要被遮罩的 Image,且位于 Mask 组件所在的对象之下):
|
|
从代码中可以看出,如果当前 MaskableGraphic 所在对象没有绑定 Mask 组件(或处于未激活状态)且 m_StencilValue
大于 0(其父元素中有 Mask 组件),就调用 StencilMaterial 类的 Add
方法修改传入的材质,这个方法流程如下:
首先检测当前渲染所用的 Shader 是否定义了用于模板测试的相关变量,如未定义则返回当前 Shder 不作任何修改;
如果模板测试所用变量都定义了,遍历 MaskableGraphic 类中对于当前需求材质缓存,若从缓存中找到对应材质直接返回这个材质;
否则就根据当前渲染的材质生成一个新的材质,这个新的材质包含模板测试相关的属性,并缓存。
第三步主要是设置了材质的 Shader 中模板测试相关数据,对应关系如下:
Shader variable | Define parameter | value | description |
---|---|---|---|
Ref(Stencil) | _Stencil | (1 << m_StencilValue) - 1 |
模板测试参考值 |
Pass(Stencil) | _StencilOp | StencilOp.Keep |
模板测试(和深度测试)通过后,根据此值对模板缓冲值进行处理 |
Comp(Stencil) | _StencilComp | CompareFunction.Equal |
模板测试参考值和模板缓冲值比较函数 |
ReadMask(Stencil) | _StencilReadMask | (1 << m_StencilValue) - 1 |
对模板测试参考值和模板缓冲值进行按位与操作后再使用 |
WriteMask(Stencil) | _StencilWriteMask | 0 |
写入模板缓冲值时对这个值按位与之后再写入 |
ColorMask(Pass) | _ColorMask | ColorWriteMask.All |
输出通道 |
UNITY_UI_ALPHACLIP |
_UseUIAlphaClip | (_StencilOp != StencilOp.Keep && writeMask > 0) ? 1 : 0 |
Shader 中是否使用透明度裁剪 |
这样就得到了修改之后包含模板测试的材质,如果设置成功了模板测试的相关数据,Unity 视图中会呈现出来,如下图:
上面模板测试条件为:
\[ StencilBufferValue \& ReadMask = StencilRef \& ReadMask \]
满足上述条件的像素就会通过测试,否则该像素不会被渲染。模板测试(和深度测试)通过后,模板缓冲值不会作任何处理(因为此处设置的 Pass 为 StencilOp.Keep
)。通过这段分析我们知道,位于 Mask 组件所在对象下的 MaskableGraphic,如果所用 Shader 满足条件则会进行模板测试(默认渲染 UI 的 Shader 就是满足这个条件的,稍后会分析这个 Shader)。
- Mask 类中的
GetModifiedMaterial
方法
另一个GetModifiedMaterial
方法就在 Mask 类中。上面讲到渲染前 MaskableGraphic 和 Mask 类中的这个方法依次被调用来修改材质, 但是与 Mask 相呼应的 MaskableGraphic 的GetModifiedMaterial
方法并未对当前渲染材质做任何修改,所以主要修改任务就来到了 Mask 中。下面就来看看其实现:
首先计算模板深度值,并限制了最大模板深度不能超过 8 层,如下:
|
|
紧接着如果当前 Mask 所在的模板深度为 0(父元素中没有任何 Mask 组件),使用如下代码修改材质的 Shader:
|
|
上面的代码中,首先依旧是调用 StencilMaterial 类的 Add
方法获取设置模板测试相关数据后的材质;其中模板测试条件是所有来到模板测试的像素全部通过,且对应的模板缓冲值都被设置为 1;
在修改完渲染所用材质之后,还更新了一个名为 m_UnmaskMaterial
的材质,其模板测试条件是所有来到模板测试的像素全部通过,且对应的模板缓冲值都被设置为 0,这个材质被 CanvasRender 通过调用 SetPopMaterial
方法设置到 CanvasRender 中,用于当前 Mask 所在对象的全部子元素渲染完成之后,重置模板缓冲区(这里会增加一个 drawCall)。
这两个材质的模板测试参数 ReadMask 和 WriteMask 都被设置为了 255,默认不影响模板测试参考值的读取以及模板缓冲值的读取和写入。如下图:
回顾一下前面讲到的内容,当 MaskableGraphic 作为 Mask 组件所在对象的子元素时,若此时这个 Mask 组件对象的模板深度值为 0,那么 MaskableGraphic 的模板深度值就是 1(渲染 Shader 中模板测试的参考值也是 1);由于父元素在子元素之前渲染(Unity UI 渲染顺序决定),在 MaskableGraphic 渲染之时模板缓冲区某些像素对应的模板缓冲值已经被置为 1,而另一些可能是默认值 0,此时 MaskableGraphic 的像素就只有在模板缓冲值是 1 的地方才能通过测试进而有可能被渲染出来。
看完了 Mask 所在的模板深度为 0的情况,再来看看 Mask 嵌套的问题。此时更新材质代码如下:
|
|
同样的这里计算了 m_MaskMaterial
和 m_UnmaskMaterial
两个材质,与前面计算过程类似,只是对应模板测试的参数有点不同。
对于 Stencil 参数的设置,来看看这个对比表格:
Build Stencil | Stencil Ref | Stencil Op | Stencil Comp | ReadMask | WriteMask |
---|---|---|---|---|---|
Normal MaskableGraphic | (1 << m_StencilValue) - 1 | Keep | Equal | (1 << m_StencilValue) - 1 | 0 |
Top Level Mask | 1 | Replace | Always | 255 | 255 |
Top Level UnMask | 1 | Zero | Always | 255 | 255 |
Other Level Mask | (1 << stencilDepth) | (1 << stencilDepth)- 1 | Replace | Equal | (1 << stencilDepth)- 1 | (1 << stencilDepth) | (1 << stencilDepth)- 1 |
Other Level UnMask | (1 << stencilDepth)- 1 | Replace | Equal | (1 << stencilDepth)- 1 | (1 << stencilDepth) | (1 << stencilDepth)- 1 |
其中 m_StencilValue
为其组件所在的模板深度值。
从表格中可以看出,当 Mask 所在组件的模板深度值大于 0 时其材质 Shader 模板测试参数的计算方式以及同其他情况下参数的对比。下面通过一个 Mask 嵌套的示例来分析整个流程,如下图:
其中 MaskPanel (0) 和 MaskPanel (1) 都是绑定了 Mask 组件的对象,MaskableGraphic 是一个普通的 Image,根据上面分析的过程可以知道这三个对象所在的模板深度 m_StencilValue
从上到下分别为 0、1 和 2。通过上面的表格能计算出渲染时模板测试的各个参数,得到如下表格:
Build Stencil | Stencil value | Stencil Ref | Stencil Op | Stencil Comp | ReadMask | WriteMask |
---|---|---|---|---|---|---|
MaskPanel (0) - Mask | 0 | 1 | Replace | Always | 255 | 255 |
MaskPanel (0) - UnMask | 0 | 1 | Zero | Always | 255 | 255 |
MaskPanel (1) - Mask | 1 | 3 | Replace | Equal | 1 | 3 |
MaskPanel (1) - UnMask | 1 | 1 | Replace | Equal | 1 | 3 |
MaskableGraphic | 2 | 3 | Keep | Equal | 3 | 0 |
从表格中可以总结以下几点:
从第二层(Stencil value 为 1)开始往下,每层渲染模板测试的 ReadMask 都是上一层的模板测试参考值,这是为了模板测试时能够完整的读取到之前 Stencil buffer 的值;
从第二层(Stencil value 为 1)开始往下(不包括非 Mask 的 UI 元素),其模板测试参数 WriteMask 都和模板测试参考值相同,这是为了能在模板测试通过时能够将当前模板测试参考值完整写入到 Stencil buffer 中;
从第二层(Stencil value 为 1)开始往下(不包括非 Mask 的 UI 元素),用于 UnMask 的模板测试参数 ReadMask 都和模板测试参考值相同。这是因为在重置 Stencil buffer 的时候是从下往上一个接一个 Mask 所在的 Shader 来渲染进行,此时模板测试参数 ReadMask 和模板测试参考值相同能够保证模板测试通过且让 Stencil buffer 的值恢复到模板测试参考值;
从第二层(Stencil value 为 1)开始往下(不包括非 Mask 的 UI 元素),用于 UnMask 的模板测试参数 WriteMask 与当前层 Mask 时模板测试参考值相同,用于 UnMask 的模板测试参考值与上一层 Mask 时模板测试参考值相同,综合这两个条件,在重置当前层的 Stencil buffer 的时候就能够将模板测试缓冲区的值重置到上一层 Mask 时模板测试参考值。
接着从上往下依次开始渲染,假设现在渲染的一块像素区域如下:
上图中每一个格子对应一个像素,初始状态每个像素上的 Stencil Buffer 值为 0,现在第一个 Mask 所在的 UI 元素 MaskPanel (0) 开始渲染(使用 UI-Default.shader
),渲染完成之后各个像素的 Stencil Buffer 值如下图:
从上图中可以看出,有一部分 Stencil Buffer 值如之前分析的那样被设置成了 1,但是不应该是根据模板测试参数所有的 Stencil Buffer 值都被设置成 1 吗?这里是因为在使用 UI-Default.shader
的时候,在模板测试之前会进行 Alpha 测试,部分像素在那一步就没有通过,因此还没来得及到模板测试就被 discard 了,所以会有部分像素的 Stencil Buffer 值为 0。
注意看上图中那些 Stencil Buffer 值被置为 1 的像素其颜色也输出了,这里 Mask 所在 Graphic 颜色能否输出就是之前所说的 Mask 类的 m_ShowMaskGraphic
决定的,当这个值被设置为 true
,Shader 中的 ColorMask 会被设置为 ColorWriteMask.All
,从而能输出所有颜色通道值,否则m_ShowMaskGraphic
被设置为 false
将不会有任何颜色通道值输出。
下面再来看看第二层 Mask 所在 MaskPanel (1) 的渲染,结果如下图:
计算过程大致如下: 将当前模板测试参考值和 Stencil Buffer 值都与 ReadMask 按位与操作后,如果这两个结果值相等则通过模板测试,紧接着若是深度测试也通过则将当前模板测试参考值与 WriteMask 进行按位与操作,得到的值写入 Stencil Buffer。
通过上述的过程,原来 Stencil Buffer 值为 1 的像素被设置成了 3;像素的颜色值能否输出也和自身 Mask 组件上的 m_ShowMaskGraphic
相关,若输出颜色通道值则最后像素颜色值根据 Shader 中 Blend 的设置(UI-Default.shader
默认 Blend SrcAlpha OneMinusSrcAlpha
)进行混合。
最后来看看 MaskableGraphic 的渲染,结果如下图:
到这一步,就很简单了。如上图所示,Stencil Buffer 值都没有发生改变,但是像素颜色发生了变化,这一渲染中模板测试的过程如下: 当前模板测试参考值是 3,而上一步仅部分 Stencil Buffer 值计算设置为 3,所以在这部分像素模板测试通过,像素颜色通道值通过混合后输出为新的值;而上一步 Stencil Buffer 值为 0 的像素这里模板测试失败,因此被 discard 无法渲染,至此一个 MaskableGraphic 遮罩效果就实现了。
前面分析 Mask 类的 GetModifiedMaterial
方法时讲到过,这个方法除了修改用于 Mask 的材质,还会修改用于 UnMask 的材质,下面就来看看 UnMask 的过程。
首先是 UI 元素 MaskPanel (1) UnMask 时渲染情况,如下图:
从上图中可以看出,根据 MaskPanel (1) 元素 UnMask 所用 Shader 的模板测试相关参数和条件方式,将 Stencil Buffer 值为 3 的像素的 Stencil Buffer 值更新为了 1,这个过程如下:
UnMask 所在的 Shader 的模板参考值以及 Stencil Buffer 值与当前计算所用的 ReadMask 进行按位与操作,若结果相等则模板测试通过(假设深度测试也通过),那么将当前使用的模板测试参考值和 WriteMask 进行按位与操作,得到的值写入 Stencil Buffer。
需要注意的是,UnMask 渲染流程中像素的颜色值没有发生任何变化,这是因为所有的 UnMask 使用材质的 Shader 中的 ColorMask 参数被设置为 0
,因此不会输出任何颜色通道值。
然后是 UI 元素 MaskPanel (0) UnMask 时渲染情况,如下图:
这个过程和上一步类似,只是使用的模板测试参数和计算方式不一样。经过 UnMask 流程最终所有像素对应的 Stencil buffer 值全部重置为 0。这里再强调一下,这里每一次 UnMask 都会增加一个 drawCall,因此项目中尽可能注意少用或者不用 Mask 实现遮罩。如下图是 Frame Debug 中 MaskPanel (1) 和 MaskPanel (0) 两个元素 UnMask 渲染时参数:
Unity UI 默认 Shader
上面基本上分析完了遮罩实现的流程,部分 Shader 的知识点也有分析,下面再来看看 Unity UI 默认 Shader 代码,首先是属性定义
|
|
上面的代码中除了定义常规的纹理、颜色等属性之外,还有用于模板测试的一些变量。 接下来看看模板测试的代码,如下:
|
|
代码中主要定义了模板测试参考值 Ref
、参考值与 Stencil buffer 值比较函数 Comp
、模板测试(和深度测试)通过对 Stencil buffer 值的处理方式 Pass
、写 Stencil buffer 值时的写入(按位与)掩码、模板测试参考值和 Stencil buffer 值读取(按位与)掩码 ReadMask
。
接着就是渲染管线部分配置,如下:
|
|
这里设置了 ZTest 方式、混合模式、ColorMask 等配置。
然后是顶点着色器和片元着色器的代码:
|
|
顶点着色器代码很简单,主要就是顶点变换、UV 坐标校正以及顶点颜色计算。下面主要来看看片元着色器部分代码:
|
|
代码中首先对纹理进行采样,然后有分两种情况对像素进行处理:
如果是
UNITY_UI_CLIP_RECT
就使用UnityGet2DClipping
函数判断当前点是否在遮罩区域_ClipRect
内,如果在这个方法就返回 1,否则返回 0;所以最终输出颜色的 Alpha 也就跟当前像素是否在遮罩区域类有关,在遮罩区域内 Alpha 值保持原来的值输出,否则 Alpha 值为 0。这种方式用于 RectMask2D 遮罩使用,下面会分析到。UNITY_UI_ALPHACLIP
这种方式就是我们分析的 Mask 遮罩实现的关键之一。Mask 组件所在的 MaskableGraphic 渲染时,如果某个像素纹理采样计算之后得到的 Alpha 值小于 0.001,那么在这里会使用clip
函数 discard 这个像素,这也就是为什么之前分析的时候讲到可能会有部分像素所在的 Stencil buffer 值为初始值 0 的原因。这样也就实现了用纹理 Alpha 值来控制遮罩。
RectMask2D
RectMask2D 和 Mask 相似,其主要区别在于 RectMask2D 直接实现矩形遮罩效果(将子元素显示区域控制在一个矩形框内)。
RectMask2D 类继承自 UIBehaviour 类,同样它具有 Unity 完整生命周期回调,同时它实现了 ICanvasRaycastFilter 接口,所以也可作为 Unity Event System 中事件分发射线检测对象,还实现了 IClipper 接口用于更新裁剪相关信息。下面就来分析一下 RectMask2D 类的代码。
OnEnable 方法
还是从 OnEnable
方法开始,首先调用 ClipperRegistry 类 Register
方法将自己注册,这样在 Canvas 执行更新时,自身就能收到回调从而调用 PerformClipping
方法;紧接着调用 MaskUtilities 类的 Notify2DMaskStateChanged
方法通知子元素重新计算裁剪相关的参数信息。
比如一个 MaskableGeaphic 元素位于 RectMask2D 下(MaskableGeaphic 实现了 IClippable 接口),当调用 Notify2DMaskStateChanged
方法时 MaskableGeaphic 的 RecalculateClipping
方法会被回调,从而 MaskableGeaphic 的父 RectMask2D 元素会被更新(具体是 MaskableGeaphic 的 RecalculateClipping
方法又调用其自身的 UpdateClipParent
方法来寻找最合适的父 RectMask2D 元素,找到后调用之前父 RectMask2D 的 RemoveClippable
方法将自身移除,然后调用新父 RectMask2D 的 AddClippable
将自身添加到 RectMask2D 中)。
OnDisable 方法
这个方法首先清除相关数据,然后从 ClipperRegistry 中反注册自己,最后再次调用 MaskUtilities 类的 Notify2DMaskStateChanged
方法通知子元素重新计算裁剪相关的参数信息。
IsRaycastLocationValid 方法
这个方法和 Mask 中的相同,这里不再分析。
AddClippable 方法
将一个 IClippable 添加到自身的跟踪列表中,用于后续的裁剪计算等。
RemoveClippable 方法
从自身的 IClippable 跟踪列表中移除一个 IClippable,代码如下:
|
|
从代码中可以看出在移除之前,首先调用了 IClippable 的 SetClipRect
方法使得这个 IClippable 不再计算 Rect 裁剪。比如 MaskableGeaphic 的 SetClipRect
就调用了 canvasRenderer.DisableRectClipping()
来禁用裁剪。
PerformClipping 方法
当 Canvas 执行更新 ClipperRegistry 类的 Cull
方法在 Layout Rebuild 之后、在 Graphic Rebuild 之前被调用,从而 RectMask2D 的 PerformClipping
被回调。下面就来分析这个方法的执行过程:
首先如果 m_ShouldRecalculateClipRects
为 true
,那么执行如下代码计算当前 RectMask2D 所在元素的所有父 RectMask2D 元素(包括自身):
|
|
接着调用 Clipping 类的 FindCullAndClipWorldRect
方法计算裁剪的矩形区域 clipRect
。代码如下:
|
|
上面的代码很简单,就是遍历上一步寻找到的所有的 RectMask2D 元素,然后通过调用 RectangularVertexClipper 的 GetCanvasRect
方法获取这个 RectMask2D 元素在 Canvas 空间下的 Rect,最终保留所有 RectMask2D 的重叠区域来作为矩形裁剪区域(如果是合法的) clipRect
。
接着判断上一步计算得到的重叠矩形裁剪区域 clipRect
是否和根 Canvas 所在矩形区域重叠,如果 Canvas Render Mode 是 RenderMode.ScreenSpaceXX
且 clipRect
和根 Canvas 所在矩形区域不重叠,那么将不会渲染其内容。代码如下:
|
|
最后就设置 IClippable 跟踪列表中子元素的裁剪区域。
- 如果当前计算的
clipRect
和上次的裁剪矩形区域不相等,那么执行以下代码:
|
|
上面的代码中调用 IClippable 的 SetClipRect
方法设置 ClipRect。这里以 MaskableGraphic 为例,最终调用的代码是 canvasRenderer.EnableRectClipping(clipRect)
,也就是渲染这个 MaskableGraphic 的 CanvasRender 会启用 Rect Clip 并设置渲染 Shader 中的 _ClipRect
变量的值,这样当渲染执行的时候就能够实现矩形遮罩效果。
对于 MaskableGraphic 还有一步,就是调用其 Cull
方法更新 CanvasRender 的 cull 值。
如果当前计算的
clipRect
和上次的裁剪矩形区域相等但是m_ForceClip
为true
,那么强制调用SetClipRect
方法设置 ClipRect。这里有一点小不同是对于最后 MaskableGraphic 的Cull
方法的调用,仅在当前 UI 元素有任何影响几何更新的变动时(CanvasRender 的hasMoved
被标记为true
)Cull
方法才会被调用。最后如果上面两种情况都不满足,那么就只会对绑定了 MaskableGraphic 组件的子元素执行
Cull
方法更新 CanvasRender 的 cull 值(这个元素所在 CanvasRender 的hasMoved
需标记为true
)。
至此 RectMask2D 实现遮罩的流程代码也就分析完了。
分析完 RectMask2D 的重要代码部分,可以看出其和 Mask 的不同之处在于 RectMask2D 并未使用模板测试来计算遮罩,因此相比之下 RectMask2D 会少一个 drawCall,因此相比使用 Mask 性能会好一些。但是 RectMask2D 实现遮罩效果被限制在一个矩形区域内,相比 Mask 遮罩就显得没有那么灵活,所以具体使用何种方式实现遮罩视项目需求而定。