设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间13 现在时间13 缓存数据 05|快乐缺点勇气 浪漫缺点诗意\你低头不说一句\你朝着灰色走去\你住进混沌深海\你开始无望等待|词曲/编混:陈粒

05|快乐缺点勇气 浪漫缺点诗意\你低头不说一句\你朝着灰色走去\你住进混沌深海\你开始无望等待|词曲/编混:陈粒 -- 光

查看: 1400|回复: 2

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

[复制链接]

  离线 

TA的专栏

  • 打卡等级:热心大叔
  • 打卡总天数:232
  • 打卡月天数:0
  • 打卡总奖励:3533
  • 最近打卡:2025-04-23 00:46:18
等级头衔

等級:晓枫资讯-上等兵

在线时间
0 小时

积分成就
威望
0
贡献
425
主题
385
精华
0
金钱
4773
积分
872
注册时间
2023-1-6
最后登录
2025-5-31

发表于 2023-2-26 10:06:59 | 显示全部楼层 |阅读模式
组件用法

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

(蓝色的圆形名为mask,数字图片名为image)
在“mask”图片上添加mask组件后的结果(可以选择是否隐藏mask图像):
110751yq6vhwzdbxwyjpwo.png
110751o1xyyvm1wvvskvvy.png
110752dbbux5vby5ssyyvh.png

RectMask2D的基本用法

RectMask2D的用法和mask大致相同,不过RectMask2D只能裁剪一个矩形区域,同时RectMask2D可以选择边缘虚化
110752dzemhjpepih3fhej.png
110752t0u50a0p5t0460rm.png
110753qzl633d3pqd3n2z2.png

原理分析

Mask的原理分析

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

简单来说,GPU为每个像素点分配一个称之为StencilBuffer的1字节大小的内存区域,这个区域可以用于保存或丢弃像素的目的。
我们举个简单的例子来说明这个缓冲区的本质。

110753opzcwpvmabejewcp.png


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

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

110753m208dt3qhh4zja8t.png


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

110754wh1iw44u11cjv1bh.png

然后轮到红色图片“绘画”,它在涂上红色前,会先取出这个点的stencil buffer值判断,在黑框范围内,这个值是1,于是继续画红色;在黑框范围外,这个值是0,于是不再画红色,最终达到了图中的效果。
所以从本质上来讲,stencil buffer是为了实现多个“绘画者”之间互相通信而存在的。由于gpu是流水线作业,它们之间无法直接通信,所以通过这种共享数据区的方式来传递消息。
理解了stencil的原理,我们再来看下它的语法。在unity shader中定义的语法格式如下
(中括号内是可以修改的值,其余都是关键字):
  1. Stencil
  2. {
  3.         Ref [_Stencil]//Ref表示要比较的值;0-255
  4.         Comp [_StencilComp]//Comp表示比较方法(等于/不等于/大于/小于等);
  5.         Pass [_StencilOp]// Pass/Fail表示当比较通过/不通过时对stencil buffer做什么操作
  6.                         // Keep(保留)
  7.                         // Replace(替换)
  8.                         // Zero(置0)
  9.                         // IncrementSaturate(增加)
  10.                         // DecrementSaturate(减少)
  11.         ReadMask [_StencilReadMask]//ReadMask/WriteMask表示取stencil buffer的值时用的mask(即可以忽略某些位);
  12.         WriteMask [_StencilWriteMask]
  13. }
复制代码
翻译一下就是:将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中的裁切参数。
  1. MaskableGraphic.cs
  2.         public virtual Material GetModifiedMaterial(Material baseMaterial)
  3.         {
  4.             var toUse = baseMaterial;
  5.             if (m_ShouldRecalculateStencil)
  6.             {
  7.                 var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);  //获取模板缓冲值
  8.                 m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
  9.                 m_ShouldRecalculateStencil = false;
  10.             }
  11.             // 如果我们用了Mask,它会生成一个mask材质,
  12.             Mask maskComponent = GetComponent<Mask>();
  13.             if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
  14.             {
  15.                 //设置模板缓冲值,并且设置在该区域内的显示,不在的裁切掉
  16.                 var maskMat = StencilMaterial.Add(toUse,  // Material baseMat
  17.                     (1 << m_StencilValue) - 1,            // 参考值
  18.                     StencilOp.Keep,                       // 不修改模板缓存
  19.                     CompareFunction.Equal,                // 相等通过测试
  20.                     ColorWriteMask.All,                   // ColorMask
  21.                     (1 << m_StencilValue) - 1,            // Readmask
  22.                     0);                                   //  WriteMas
  23.                 StencilMaterial.Remove(m_MaskMaterial);
  24.                 //并且更换新的材质
  25.                 m_MaskMaterial = maskMat;
  26.                 toUse = m_MaskMaterial;
  27.             }
  28.             return toUse;
  29.         }
复制代码
Image对象在进行Rebuild()时,UpdateMaterial()方法中会获取需要渲染的材质,并且判断当前对象的组件是否有继承IMaterialModifier接口,如果有那么它就是绑定了Mask脚本,接着调用GetModifiedMaterial方法修改材质上Shader的参数。
  1. Image.cs
  2.    protected virtual void UpdateMaterial()
  3.    {
  4.        if (!IsActive())
  5.            return;
  6.        //更新刚刚替换的新的模板缓冲的材质
  7.        canvasRenderer.materialCount = 1;
  8.        canvasRenderer.SetMaterial(materialForRendering, 0);
  9.        canvasRenderer.SetTexture(mainTexture);
  10.    }
  11.    public virtual Material materialForRendering
  12.    {
  13.        get
  14.        {
  15.            //遍历UI中的每个Mask组件
  16.            var components = ListPool<Component>.Get();
  17.            GetComponents(typeof(IMaterialModifier), components);
  18.            //并且更新每个Mask组件的模板缓冲材质
  19.            var currentMat = material;
  20.            for (var i = 0; i < components.Count; i++)
  21.                currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
  22.            ListPool<Component>.Release(components);
  23.            //返回新的材质,用于裁切
  24.            return currentMat;
  25.        }
  26.    }
复制代码
因为模板缓冲可以提供模板的区域,也就是前面设置的圆形图片,所以最终会将元素裁切到这个圆心图片中。
  1. Mask.cs        
  2.        /// Stencil calculation time!
  3.        public virtual Material GetModifiedMaterial(Material baseMaterial)
  4.        {
  5.            if (!MaskEnabled())
  6.                return baseMaterial;
  7.            var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
  8.            var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
  9.            // stencil只支持最大深度为8的遮罩
  10.            if (stencilDepth >= 8)
  11.            {
  12.                Debug.LogError("Attempting to use a stencil mask with depth > 8", gameObject);
  13.                return baseMaterial;
  14.            }
  15.            int desiredStencilBit = 1 << stencilDepth;
  16.            // if we are at the first level...
  17.            // we want to destroy what is there
  18.            if (desiredStencilBit == 1)
  19.            {
  20.                var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
  21.                StencilMaterial.Remove(m_MaskMaterial);
  22.                m_MaskMaterial = maskMaterial;

  23.                var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);
  24.                StencilMaterial.Remove(m_UnmaskMaterial);
  25.                m_UnmaskMaterial = unmaskMaterial;
  26.                graphic.canvasRenderer.popMaterialCount = 1;
  27.                graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);

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

  34.            graphic.canvasRenderer.hasPopInstruction = true;
  35.            var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.Replace, CompareFunction.Equal, 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
  36.            StencilMaterial.Remove(m_UnmaskMaterial);
  37.            m_UnmaskMaterial = unmaskMaterial2;
  38.            graphic.canvasRenderer.popMaterialCount = 1;
  39.            graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);

  40.            return m_MaskMaterial;
  41.        }
复制代码
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))
  1. CanvasUpdateRegistry.cs
  2.         protected CanvasUpdateRegistry()
  3.         {
  4.             Canvas.willRenderCanvases += PerformUpdate;
  5.         }
  6.         private void PerformUpdate()
  7.         {
  8.             //...略
  9.             // 开始裁切Mask2D
  10.             ClipperRegistry.instance.Cull();
  11.             //...略
  12.         }
  13. ClipperRegistry.cs
  14.         public void Cull()
  15.         {
  16.             for (var i = 0; i < m_Clippers.Count; ++i)
  17.             {
  18.                 m_Clippers[i].PerformClipping();
  19.             }
  20.         }
复制代码
RectMask2D会在OnEnable()方法中,将当前组件注册ClipperRegistry.Register(this);
这样在上面ClipperRegistry.instance.Cull();方法时就可以遍历所有Mask2D组件并且调用它们的PerformClipping()方法了。
PerformClipping()方法,需要找到所有需要裁切的UI元素,因为Image和Text都继承了IClippable接口,最终将调用Cull()进行裁切。
  1. RectMask2D.cs
  2.     protected override void OnEnable()
  3.     {
  4.         //注册当前RectMask2D裁切对象,保证下次Rebuild时可进行裁切。
  5.         base.OnEnable();
  6.         m_ShouldRecalculateClipRects = true;
  7.         ClipperRegistry.Register(this);
  8.         MaskUtilities.Notify2DMaskStateChanged(this);
  9.     }
  10.         public virtual void PerformClipping()
  11.         {
  12.             //...略
  13.             bool validRect = true;
  14.             Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
  15.             bool clipRectChanged = clipRect != m_LastClipRectCanvasSpace;
  16.             if (clipRectChanged || m_ForceClip)
  17.             {
  18.                 foreach (IClippable clipTarget in m_ClipTargets)
  19.                     //把裁切区域传到每个UI元素的Shader中[划重点!!!]
  20.                     clipTarget.SetClipRect(clipRect, validRect);
  21.                 m_LastClipRectCanvasSpace = clipRect;
  22.                 m_LastValidClipRect = validRect;
  23.             }
  24.             foreach (IClippable clipTarget in m_ClipTargets)
  25.             {
  26.                 var maskable = clipTarget as MaskableGraphic;
  27.                 if (maskable != null && !maskable.canvasRenderer.hasMoved && !clipRectChanged)
  28.                     continue;
  29.                 // 调用所有继承IClippable的Cull方法
  30.                 clipTarget.Cull(m_LastClipRectCanvasSpace, m_LastValidClipRect);
  31.             }
  32.         }
  33. MaskableGraphic.cs
  34.         public virtual void Cull(Rect clipRect, bool validRect)
  35.         {
  36.             var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true);
  37.             UpdateCull(cull);
  38.         }
  39.         private void UpdateCull(bool cull)
  40.         {
  41.             var cullingChanged = canvasRenderer.cull != cull;
  42.             canvasRenderer.cull = cull;
  43.             if (cullingChanged)
  44.             {
  45.                 UISystemProfilerApi.AddMarker("MaskableGraphic.cullingChanged", this);
  46.                 m_OnCullStateChanged.Invoke(cull);
  47.                 SetVerticesDirty();
  48.             }
  49.         }
复制代码
性能区分

Mask组件需要依赖一个Image组件,裁剪区域就是Image的大小。
Mask会在首尾(首=Mask节点,尾=Mask节点下的孩子遍历完后)drawcall,多个Mask间如果符合合批条件这两个drawcall可以对应合批(mask1 的首 和 mask2 的首合;mask1 的尾 和 mask2 的尾合。首尾不能合)
Mask内的UI节点和非Mask外的UI节点不能合批,但多个Mask内的UI节点间如果符合合批条件,可以合批。
具体来说:
新建一个场景,默认drawcall是2个;
现在添加一个mask,
110754gti4d8z34993x91d.png

drawcall+3,Mask导致2个drawcall(第1个和第3个,一头一尾),Mask下的子节点Image导致1个drawcall(中间的)
再看下RectMask2D的情况
110755o7dzr8hofxeuoef8.png

只有新增1个子节点Image的drawcall, 而RectMask2D不会导致drawcall.
而这时增加一个mask,不要重叠:
110755fhdynwh69n6k77de.png

还是5个drawcall, 没有变化.
Unity把2个Mask进行了网格合并, 3个drawcall, 分别为[2个Mask头]、[2个Image]、[2个Mask尾].
这里可以看出, Mask之间是可以进行合并的, 从而不额外增加drawcall
而如果放到一起,
110756z6w1uqj9dw31z9kf.png

**这是因为Unity的合批需要同渲染层级(depth), 同材质, 同图集, 如果重叠了, depth就不同了, 6个drawcall分别为Mask头、Mask的Image、Mask尾、Mask(1)头、Mask(1)的Image、Mask(1)尾.
Mask小结:
1.多个Mask之间可以进行合批(头和头合批, 子对象和子对象合批, 尾和尾合批),需要同渲染层级(depth), 同材质, 同图集.
2.Mask内外不能进行合批.
再试试RectMask2D
把RectMask2D复制一个出来, 然后把位置摆开.**
110756czmd20h0b5oqfhhr.png

drawcall为4, 因为RectMask2D本身不会导致drawcall, 所以RectMask2D之间不能进行合批.
RectMask2D小结:
1.RectMask2D本身不产生drawcall.
2.不同RectMask2D的子对象不能合批.
对比测试

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

可以大致看出,在图像很大且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的资料请关注晓枫资讯其它相关文章!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
11
积分
2
注册时间
2024-11-9
最后登录
2024-11-9

发表于 2024-11-18 18:36:20 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
20
积分
20
注册时间
2022-12-25
最后登录
2022-12-25

发表于 2025-6-25 21:00:31 | 显示全部楼层
感谢楼主分享。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表