前言

UGUI的裁切分为Mask和Mask2D两种

目录

  • Mask原理分析
  • RectMask2D原理分析
  • RectMask2D和Mask的性能区分

 

一、Mask原理分析

Mask:IMaskable,IMaterialModifier

我们先来看Mask。它可以给Mask指定一张裁切图裁切子元素。我们给Mask指定了一张圆形图片,那么子节点下的元素都会被裁切在这个圆形区域中。

unity中mask镂空效果 unity mask原理_unity中mask镂空效果

Mask的实现原理:

1. Mask会赋予Image一个特殊的材质,这个材质会给Image的每个像素点进行标记,将标记结果存放在一个缓存内(这个缓存叫做 Stencil Buffer)
2. 当子级UI进行渲染的时候会去检查这个 Stencil Buffer内的标记,如果当前覆盖的区域存在标记(即该区域在Image的覆盖范围内),进行渲染,否则不渲染

1.1 StencilBuffer

看起来好像挺简单的,那么背后的功臣——StencilBuffer,究竟是何方神圣呢?

简单来说,GPU为每个像素点分配一个称之为StencilBuffer的1字节大小的内存区域,这个区域可以用于保存或丢弃像素的目的。

我们举个简单的例子来说明这个缓冲区的本质。

unity中mask镂空效果 unity mask原理_Image_02

如上图所示,我们的场景中有1个红色图片和1个绿色图片,黑框范围内是它们重叠部分。一帧渲染开始,首先绿色图片将它覆盖范围的每个像素颜色“画”在屏幕上,然后红色图片也将自己的颜色画在屏幕上,就是图中的效果了。

这种情况下,重叠区域内红色完全覆盖了绿色。接下来,我们为绿色图片添加Mask组件。于是变成了这样:

unity中mask镂空效果 unity mask原理_unity中mask镂空效果_03

此时一帧渲染开始,首先绿色图片将它覆盖范围都涂上绿色,同时将每个像素的stencil buffer值设置为1,此时屏幕的stencil buffer分布如下:

unity中mask镂空效果 unity mask原理_unity中mask镂空效果_04

然后轮到红色图片“绘画”,它在涂上红色前,会先取出这个点的stencil buffer值判断,在黑框范围内,这个值是1,于是继续画红色;在黑框范围外,这个值是0,于是不再画红色,最终达到了图中的效果。

所以从本质上来讲,stencil buffer是为了实现多个“绘画者”之间互相通信而存在的。由于gpu是流水线作业,它们之间无法直接通信,所以通过这种共享数据区的方式来传递消息,从而达到一些“不可告人”的目的。

1.2 Unity Shader

理解了stencil的原理,我们再来看下它的语法。在unity shader中定义的语法格式如下

(中括号内是可以修改的值,其余都是关键字):

Stencil
{
	Ref [_Stencil]//Ref表示要比较的值;0-255
	Comp [_StencilComp]//Comp表示比较方法(等于/不等于/大于/小于等);
	Pass [_StencilOp]// Pass/Fail表示当比较通过/不通过时对stencil buffer做什么操作
			// Keep(保留)
			// Replace(替换)
			// Zero(置0)
			// IncrementSaturate(增加)
			// DecrementSaturate(减少)
	ReadMask [_StencilReadMask]//ReadMask/WriteMask表示取stencil buffer的值时用的mask(即可以忽略某些位);
	WriteMask [_StencilWriteMask]
}

翻译一下就是:将stencil buffer的值与ReadMask与运算,然后与Ref值进行Comp比较,结果为true时进行Pass操作,否则进行Fail操作,操作值写入stencil buffer前先与WriteMask与运算。

1.2.1 UI/Default

最后,我们来看下Unity渲染UI组件时默认使用的Shader——UI/Default(略去了一些不相关内容):

Shader "UI/Default"
{
	Properties
	{
		[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
		_Color ("Tint", Color) = (1,1,1,1)
		
		_StencilComp ("Stencil Comparison", Float) = 8
		_Stencil ("Stencil ID", Float) = 0
		_StencilOp ("Stencil Operation", Float) = 0
		_StencilWriteMask ("Stencil Write Mask", Float) = 255
		_StencilReadMask ("Stencil Read Mask", Float) = 255

		_ColorMask ("Color Mask", Float) = 15

		[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
	}
···
}

1.3 mask的源码实现

了解了stencil,我们再来看mask的源码实现

由于裁切需要同时裁切图片和文本,所以Image和Text都会派生自MaskableGraphic。

如果要让Mask节点下的元素裁切,那么它需要占一个DrawCall,因为这些元素需要一个新的Shader参数来渲染。

如下代码所示,MaskableGraphic实现了IMaterialModifier接口, 而StencilMaterial.Add()就是设置Shader中的裁切参数。

MaskableGraphic.cs
        public virtual Material GetModifiedMaterial(Material baseMaterial)
        {
            var toUse = baseMaterial;

            if (m_ShouldRecalculateStencil)
            {
                var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
                //获取模板缓冲值
                m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
                m_ShouldRecalculateStencil = false;
            }

            // if we have a enabled Mask component then it will
            // generate the mask material. This is an optimisation
            // it adds some coupling between components though :(
            // 如果我们用了Mask,它会生成一个mask材质,
            Mask maskComponent = GetComponent<Mask>();
            if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
            {
                //设置模板缓冲值,并且设置在该区域内的显示,不在的裁切掉
                var maskMat = StencilMaterial.Add(toUse,  // Material baseMat
                    (1 << m_StencilValue) - 1,            // 参考值
                    StencilOp.Keep,                       // 不修改模板缓存
                    CompareFunction.Equal,                // 相等通过测试
                    ColorWriteMask.All,                   // ColorMask
                    (1 << m_StencilValue) - 1,            // Readmask
                    0);                                   //  WriteMas
                StencilMaterial.Remove(m_MaskMaterial);
                //并且更换新的材质
                m_MaskMaterial = maskMat;
                toUse = m_MaskMaterial;
            }
            return toUse;
        }

Image对象在进行Rebuild()时,UpdateMaterial()方法中会获取需要渲染的材质,并且判断当前对象的组件是否有继承IMaterialModifier接口,

如果有那么它就是绑定了Mask脚本,接着调用上面提到的GetModifiedMaterial方法修改材质上Shader的参数。

Image.cs 
    protected virtual void UpdateMaterial()
    {
        if (!IsActive())
            return;
        //更新刚刚替换的新的模板缓冲的材质
        canvasRenderer.materialCount = 1;
        canvasRenderer.SetMaterial(materialForRendering, 0);
        canvasRenderer.SetTexture(mainTexture);
    }

    public virtual Material materialForRendering
    {
        get
        {
            //遍历UI中的每个Mask组件
            var components = ListPool<Component>.Get();
            GetComponents(typeof(IMaterialModifier), components);
            //并且更新每个Mask组件的模板缓冲材质
            var currentMat = material;
            for (var i = 0; i < components.Count; i++)
                currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
            ListPool<Component>.Release(components);
            //返回新的材质,用于裁切
            return currentMat;
        }
    }

因为模板缓冲可以提供模板的区域,也就是前面设置的圆形图片,所以最终会将元素裁切到这个圆心图片中。

1.3.1 Mask.GetModifiedMaterial

Mask.cs        
        /// Stencil calculation time!
        public virtual Material GetModifiedMaterial(Material baseMaterial)
        {
            if (!MaskEnabled())
                return baseMaterial;

            var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
            var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
            // stencil只支持最大深度为8的遮罩
            if (stencilDepth >= 8)
            {
                Debug.LogError("Attempting to use a stencil mask with depth > 8", gameObject);
                return baseMaterial;
            }

            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;
            }

            //otherwise we need to be a bit smarter and set some read / write masks
            var maskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit | (desiredStencilBit - 1), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
            StencilMaterial.Remove(m_MaskMaterial);
            m_MaskMaterial = maskMaterial2;

            graphic.canvasRenderer.hasPopInstruction = true;
            var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.