欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

详解Unity中Mask和RectMask2D组件的对比与测试

程序员文章站 2022-03-05 21:21:26
组件用法mask组件可以实现遮罩的效果,将一个图像设为拥有mask组件图像的子物体,最后就会隐藏掉子图像和mask图像不重合的部分。例如:(蓝色的圆形名为mask,数字图片名为image)在“mask...

组件用法

mask组件可以实现遮罩的效果,将一个图像设为拥有mask组件图像的子物体,最后就会隐藏掉子图像和mask图像不重合的部分。例如:

详解Unity中Mask和RectMask2D组件的对比与测试详解Unity中Mask和RectMask2D组件的对比与测试

(蓝色的圆形名为mask,数字图片名为image)

在“mask”图片上添加mask组件后的结果(可以选择是否隐藏mask图像):

详解Unity中Mask和RectMask2D组件的对比与测试详解Unity中Mask和RectMask2D组件的对比与测试详解Unity中Mask和RectMask2D组件的对比与测试

rectmask2d的基本用法

rectmask2d的用法和mask大致相同,不过rectmask2d只能裁剪一个矩形区域,同时rectmask2d可以选择边缘虚化

详解Unity中Mask和RectMask2D组件的对比与测试详解Unity中Mask和RectMask2D组件的对比与测试详解Unity中Mask和RectMask2D组件的对比与测试

原理分析

mask的原理分析

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

那么,stencil buffer 究竟是什么呢?

1 stencilbuffer

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

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

详解Unity中Mask和RectMask2D组件的对比与测试

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

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

详解Unity中Mask和RectMask2D组件的对比与测试

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

详解Unity中Mask和RectMask2D组件的对比与测试

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

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

理解了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与运算。

2 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;
            }
            // 如果我们用了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;
       }
   }

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

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.replace, comparefunction.equal, 0, desiredstencilbit - 1, desiredstencilbit | (desiredstencilbit - 1));
           stencilmaterial.remove(m_unmaskmaterial);
           m_unmaskmaterial = unmaskmaterial2;
           graphic.canvasrenderer.popmaterialcount = 1;
           graphic.canvasrenderer.setpopmaterial(m_unmaskmaterial, 0);

           return m_maskmaterial;
       }

mask 组件调用了模板材质球构建了一个自己的材质球,因此它使用了实时渲染中的模板方法来裁切不需要显示的部分,所有在 mask 组件的子节点都会进行裁切。

我们可以说 mask 是在 gpu 中做的裁切,使用的方法是着色器中的模板方法。

rectmask2d的原理分析

rectmask2d的工作流大致如下:

①c#层:找出父物体中所有rectmask2d覆盖区域的交集(findcullandclipworldrect)
②c#层:所有继承maskgraphic的子物体组件调用方法设置剪裁区域(setcliprect)传递给shader
③shader层:接收到矩形区域_cliprect,片元着色器中判断像素是否在矩形区域内,不在则透明度设置为0(unityget2dclipping )
④shader层:丢弃掉alpha小于0.001的元素(clip (color.a - 0.001))

canvasupdateregistry.cs
        protected canvasupdateregistry()
        {
            canvas.willrendercanvases += performupdate;
        }
        private void performupdate()
        {
            //...略
            // 开始裁切mask2d
            clipperregistry.instance.cull();
            //...略
        }
clipperregistry.cs
        public void cull()
        {
            for (var i = 0; i < m_clippers.count; ++i)
            {
                m_clippers[i].performclipping();
            }
        }

rectmask2d会在onenable()方法中,将当前组件注册clipperregistry.register(this);

这样在上面clipperregistry.instance.cull();方法时就可以遍历所有mask2d组件并且调用它们的performclipping()方法了。

performclipping()方法,需要找到所有需要裁切的ui元素,因为image和text都继承了iclippable接口,最终将调用cull()进行裁切。

rectmask2d.cs
    protected override void onenable()
    {
        //注册当前rectmask2d裁切对象,保证下次rebuild时可进行裁切。
        base.onenable();
        m_shouldrecalculatecliprects = true;
        clipperregistry.register(this);
        maskutilities.notify2dmaskstatechanged(this);
    }
        public virtual void performclipping()
        {
            //...略
            bool validrect = true;
            rect cliprect = clipping.findcullandclipworldrect(m_clippers, out validrect);
            bool cliprectchanged = cliprect != m_lastcliprectcanvasspace;
            if (cliprectchanged || m_forceclip)
            {
                foreach (iclippable cliptarget in m_cliptargets)
                    //把裁切区域传到每个ui元素的shader中[划重点!!!]
                    cliptarget.setcliprect(cliprect, validrect);
                m_lastcliprectcanvasspace = cliprect;
                m_lastvalidcliprect = validrect;
            }
            foreach (iclippable cliptarget in m_cliptargets)
            {
                var maskable = cliptarget as maskablegraphic;
                if (maskable != null && !maskable.canvasrenderer.hasmoved && !cliprectchanged)
                    continue;
                // 调用所有继承iclippable的cull方法
                cliptarget.cull(m_lastcliprectcanvasspace, m_lastvalidcliprect);
            }
        }
maskablegraphic.cs
        public virtual void cull(rect cliprect, bool validrect)
        {
            var cull = !validrect || !cliprect.overlaps(rootcanvasrect, true);
            updatecull(cull);
        }
        private void updatecull(bool cull)
        {
            var cullingchanged = canvasrenderer.cull != cull;
            canvasrenderer.cull = cull;
            if (cullingchanged)
            {
                uisystemprofilerapi.addmarker("maskablegraphic.cullingchanged", this);
                m_oncullstatechanged.invoke(cull);
                setverticesdirty();
            }
        }

性能区分

mask组件需要依赖一个image组件,裁剪区域就是image的大小。

mask会在首尾(首=mask节点,尾=mask节点下的孩子遍历完后)drawcall,多个mask间如果符合合批条件这两个drawcall可以对应合批(mask1 的首 和 mask2 的首合;mask1 的尾 和 mask2 的尾合。首尾不能合)
mask内的ui节点和非mask外的ui节点不能合批,但多个mask内的ui节点间如果符合合批条件,可以合批。
具体来说:
新建一个场景,默认drawcall是2个;
现在添加一个mask,

详解Unity中Mask和RectMask2D组件的对比与测试

drawcall+3,mask导致2个drawcall(第1个和第3个,一头一尾),mask下的子节点image导致1个drawcall(中间的)
再看下rectmask2d的情况

详解Unity中Mask和RectMask2D组件的对比与测试

只有新增1个子节点image的drawcall, 而rectmask2d不会导致drawcall.

而这时增加一个mask,不要重叠:

详解Unity中Mask和RectMask2D组件的对比与测试

还是5个drawcall, 没有变化.
unity把2个mask进行了网格合并, 3个drawcall, 分别为[2个mask头]、[2个image]、[2个mask尾].

这里可以看出, mask之间是可以进行合并的, 从而不额外增加drawcall

而如果放到一起,

详解Unity中Mask和RectMask2D组件的对比与测试

**这是因为unity的合批需要同渲染层级(depth), 同材质, 同图集, 如果重叠了, depth就不同了, 6个drawcall分别为mask头、mask的image、mask尾、mask(1)头、mask(1)的image、mask(1)尾.

mask小结:

1.多个mask之间可以进行合批(头和头合批, 子对象和子对象合批, 尾和尾合批),需要同渲染层级(depth), 同材质, 同图集.
2.mask内外不能进行合批.
再试试rectmask2d
把rectmask2d复制一个出来, 然后把位置摆开.**

详解Unity中Mask和RectMask2D组件的对比与测试

drawcall为4, 因为rectmask2d本身不会导致drawcall, 所以rectmask2d之间不能进行合批.

rectmask2d小结:

1.rectmask2d本身不产生drawcall.
2.不同rectmask2d的子对象不能合批.

对比测试

下面放上我在手机端做的一个简单的对比测试:

详解Unity中Mask和RectMask2D组件的对比与测试

可以大致看出,在图像很大且cpu任务较重的的情况下,mask会对性能有明显的影响,而在图像数量较多时mask略好于rectmask2d
项目链接:https://git.woa.com/jnjnjnzhang/maskvsrectmask2d

注:测试场景中自带约60个batches。每个mask测试加入同样的20个mask。图像数量少的场景每个mask下挂一个图像,面积大情况下mask大小不变图像边长放大1000倍,数量多情况下每个mask下挂同样的100个图像。瓶颈为drawcall时,每个物体仅有简单的渲染,在物体上挂载了需要进行复杂运算的脚本。瓶颈为gpu时,去掉脚本,在场景中挂载了后处理渲染提高gpu负载。

参考文章

https://zhuanlan.zhihu.com/p/136505882

以上就是unity中mask和rectmask2d组件的对比与测试的详细内容,更多关于unity中mask和rectmask2d的资料请关注其它相关文章!