找回密码
 立即注册
查看: 548|回复: 0

[笔记] 【Unity源码学习】遮罩:Mask与Mask2D

[复制链接]
发表于 2020-11-25 09:31 | 显示全部楼层 |阅读模式
Mask

UGUI的裁切分为Mask和Mask2D两种
我们先来看Mask。它可以给Mask指定一张裁切图裁切子元素。我们给Mask指定了一张圆形图片,那么子节点下的元素都会被裁切在这个圆形区域中。
Mask的原理就是利用了StencilBuffer(模板缓冲),它里面记录了一个ID,被裁切元素也有StencilBuffer(模板缓冲)的ID,并且和Mask里的比较,相同才会被渲染
StencilBuffer

看起来好像挺简单的,那么背后的功臣——StencilBuffer,究竟是何方神圣呢?
简单来说,gpu为每个像素点分配一个称之为stencil buffer的1字节大小的内存区域,这个区域可以用于保存或丢弃像素的目的。我们举个简单的例子来说明这个缓冲区的本质。
如上图所示,我们的场景中有1个红色图片和1个绿色图片,黑框范围内是它们重叠部分。一帧渲染开始,首先绿色图片将它覆盖范围的每个像素颜色“画”在屏幕上,然后红色图片也将自己的颜色画在屏幕上,就是图中的效果了。
这种情况下,重叠区域内红色完全覆盖了绿色。接下来,我们为绿色图片添加Mask组件。于是变成了这样:
此时一帧渲染开始,首先绿色图片将它覆盖范围都涂上绿色,同时将每个像素的stencil buffer值设置为1,此时屏幕的stencil buffer分布如下:
然后轮到红色图片“绘画”,它在涂上红色前,会先取出这个点的stencil buffer值判断,在黑框范围内,这个值是1,于是继续画红色;在黑框范围外,这个值是0,于是不再画红色,最终达到了图中的效果。
所以从本质上来讲,stencil buffer是为了实现多个“绘画者”之间互相通信而存在的。由于gpu是流水线作业,它们之间无法直接通信,所以通过这种共享数据区的方式来传递消息,从而达到一些“不可告人”的目的。
unity shader

理解了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/替换Replace/置0 Zero/增加IncrementSaturate/减少DecrementSaturate等);
  7.         ReadMask [_StencilReadMask]//ReadMask/WriteMask表示取stencil buffer的值时用的mask(即可以忽略某些位);
  8.         WriteMask [_StencilWriteMask]
  9. }
复制代码
翻译一下就是:将stencil buffer的值与ReadMask与运算,然后与Ref值进行Comp比较,结果为true时进行Pass操作,否则进行Fail操作,操作值写入stencil buffer前先与WriteMask与运算。
UI/Default

最后,我们来看下Unity渲染UI组件时默认使用的Shader——UI/Default(略去了一些不相关内容):
  1. Shader "UI/Default"
  2. {
  3.         Properties
  4.         {
  5.                 [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
  6.                 _Color ("Tint", Color) = (1,1,1,1)
  7.                
  8.                 _StencilComp ("Stencil Comparison", Float) = 8
  9.                 _Stencil ("Stencil ID", Float) = 0
  10.                 _StencilOp ("Stencil Operation", Float) = 0
  11.                 _StencilWriteMask ("Stencil Write Mask", Float) = 255
  12.                 _StencilReadMask ("Stencil Read Mask", Float) = 255
  13.                 _ColorMask ("Color Mask", Float) = 15
  14.                 [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
  15.         }
  16. ···
  17. }
复制代码
以及我们代码中调用的StencilMaterial.Add的内部实现
  1. StencilMaterial.cs
  2.         public static Material Add(Material baseMat, int stencilID, StencilOp operation, CompareFunction compareFunction, ColorWriteMask colorWriteMask, int readMask, int writeMask)
  3.         {
  4. ···省略
  5.             var newEnt = new MatEntry();
  6.             newEnt.count = 1;
  7.             newEnt.baseMat = baseMat;
  8.             newEnt.customMat = new Material(baseMat);
  9.             newEnt.customMat.hideFlags = HideFlags.HideAndDontSave;
  10.             newEnt.stencilId = stencilID;
  11.             newEnt.operation = operation;
  12.             newEnt.compareFunction = compareFunction;
  13.             newEnt.readMask = readMask;
  14.             newEnt.writeMask = writeMask;
  15.             newEnt.colorMask = colorWriteMask;
  16.             newEnt.useAlphaClip = operation != StencilOp.Keep && writeMask > 0;
  17.             newEnt.customMat.name = string.Format("Stencil Id:{0}, Op:{1}, Comp:{2}, WriteMask:{3}, ReadMask:{4}, ColorMask:{5} AlphaClip:{6} ({7})", stencilID, operation, compareFunction, writeMask, readMask, colorWriteMask, newEnt.useAlphaClip, baseMat.name);
  18.             newEnt.customMat.SetInt("_Stencil", stencilID);
  19.             newEnt.customMat.SetInt("_StencilOp", (int)operation);
  20.             newEnt.customMat.SetInt("_StencilComp", (int)compareFunction);
  21.             newEnt.customMat.SetInt("_StencilReadMask", readMask);
  22.             newEnt.customMat.SetInt("_StencilWriteMask", writeMask);
  23.             newEnt.customMat.SetInt("_ColorMask", (int)colorWriteMask);
  24.             // left for backwards compatability
  25.             if (newEnt.customMat.HasProperty("_UseAlphaClip"))
  26.                 newEnt.customMat.SetInt("_UseAlphaClip", newEnt.useAlphaClip ? 1 : 0);
  27.             if (newEnt.useAlphaClip)
  28.                 newEnt.customMat.EnableKeyword("UNITY_UI_ALPHACLIP");
  29.             else
  30.                 newEnt.customMat.DisableKeyword("UNITY_UI_ALPHACLIP");
  31.             m_List.Add(newEnt);
  32.             return newEnt.customMat;
  33.         }
复制代码
了解了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.                 //获取模板缓冲值
  9.                 m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
  10.                 m_ShouldRecalculateStencil = false;
  11.             }
  12.             // if we have a enabled Mask component then it will
  13.             // generate the mask material. This is an optimisation
  14.             // it adds some coupling between components though :(
  15.             Mask maskComponent = GetComponent<Mask>();
  16.             if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
  17.             {
  18.                 //设置模板缓冲值,并且设置在该区域内的显示,不在的裁切掉
  19.                 var maskMat = StencilMaterial.Add(toUse,  // Material baseMat
  20.                     (1 << m_StencilValue) - 1, // 参考值
  21.                     StencilOp.Keep, // 不修改模板缓存
  22.                     CompareFunction.Equal,  // 相等通过测试
  23.                     ColorWriteMask.All, // ColorMask
  24.                     (1 << m_StencilValue) - 1,// Readmask
  25.                     0);//  WriteMas
  26.                 StencilMaterial.Remove(m_MaskMaterial);
  27.                 //并且更换新的材质
  28.                 m_MaskMaterial = maskMat;
  29.                 toUse = m_MaskMaterial;
  30.             }
  31.             return toUse;
  32.         }
复制代码
Image对象在进行Rebuild()时,UpdateMaterial()方法中会获取需要渲染的材质,并且判断当前对象的组件是否有继承IMaterialModifier接口,如果有那么它就是绑定了Mask脚本,接着调用上面提到的GetModifiedMaterial方法修改材质上Shader的参数。
  1. Image.cs
  2.    public virtual void Rebuild(CanvasUpdate update)
  3.     {
  4.         if (canvasRenderer.cull)
  5.             return;
  6.         switch (update)
  7.         {
  8.             case CanvasUpdate.PreRender:
  9.                 if (m_VertsDirty)
  10.                 {
  11.                     //开始更新网格
  12.                     UpdateGeometry();
  13.                     m_VertsDirty = false;
  14.                 }
  15.                 if (m_MaterialDirty)
  16.                 {
  17.                     //开始更新材质
  18.                     UpdateMaterial();
  19.                     m_MaterialDirty = false;
  20.                 }
  21.                 break;
  22.         }
  23.     }
  24.     public virtual Material materialForRendering
  25.     {
  26.         get
  27.         {
  28.             //遍历UI中的每个Mask组件
  29.             var components = ListPool<Component>.Get();
  30.             GetComponents(typeof(IMaterialModifier), components);
  31.             //并且更新每个Mask组件的模板缓冲材质
  32.             var currentMat = material;
  33.             for (var i = 0; i < components.Count; i++)
  34.                 currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
  35.             ListPool<Component>.Release(components);
  36.             //返回新的材质,用于裁切
  37.             return currentMat;
  38.         }
  39.     }
  40.     protected virtual void UpdateMaterial()
  41.     {
  42.         if (!IsActive())
  43.             return;
  44.         //更新刚刚替换的新的模板缓冲的材质
  45.         canvasRenderer.materialCount = 1;
  46.         canvasRenderer.SetMaterial(materialForRendering, 0);
  47.         canvasRenderer.SetTexture(mainTexture);
  48.     }
复制代码
因为模板缓冲可以提供模板的区域,也就是前面设置的圆形图片,所以最终会将元素裁切到这个圆心图片中。
Mask.GetModifiedMaterial
  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 中做的裁切,使用的方法是着色器中的模板方法
Mask2D

接着我们再来看看Mask2D的原理,在前面介绍Canvas.willRenderCanvases()时在PerformUpdate方法中会调用ClipperRegistry.instance.Cull();来处理界面中所有的Mask2D裁切。
  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.         }
复制代码
Mask2D会在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.             //TODO See if an IsActive() test would work well here or whether it might cause unexpected side effects (re case 776771)
  13.             // if the parents are changed
  14.             // or something similar we
  15.             // do a recalculate here
  16.             //重新计算裁切区域
  17.             if (m_ShouldRecalculateClipRects)
  18.             {
  19.                 MaskUtilities.GetRectMasksForClip(this, m_Clippers);
  20.                 m_ShouldRecalculateClipRects = false;
  21.             }
  22.             // get the compound rects from
  23.             // the clippers that are valid
  24.             bool validRect = true;
  25.             Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
  26.             bool clipRectChanged = clipRect != m_LastClipRectCanvasSpace;
  27.             if (clipRectChanged || m_ForceClip)
  28.             {
  29.                 foreach (IClippable clipTarget in m_ClipTargets)
  30.                     //把裁切区域传到每个UI元素的Shader中[划重点!!!]
  31.                     clipTarget.SetClipRect(clipRect, validRect);
  32.                 m_LastClipRectCanvasSpace = clipRect;
  33.                 m_LastValidClipRect = validRect;
  34.             }
  35.             foreach (IClippable clipTarget in m_ClipTargets)
  36.             {
  37.                 var maskable = clipTarget as MaskableGraphic;
  38.                 if (maskable != null && !maskable.canvasRenderer.hasMoved && !clipRectChanged)
  39.                     continue;
  40.                 clipTarget.Cull(m_LastClipRectCanvasSpace, m_LastValidClipRect);
  41.             }
  42.         }
  43. MaskableGraphic.cs
  44.         public virtual void Cull(Rect clipRect, bool validRect)
  45.         {
  46.             var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true);
  47.             UpdateCull(cull);
  48.         }
  49.         private void UpdateCull(bool cull)
  50.         {
  51.             var cullingChanged = canvasRenderer.cull != cull;
  52.             canvasRenderer.cull = cull;
  53.             if (cullingChanged)
  54.             {
  55.                 UISystemProfilerApi.AddMarker("MaskableGraphic.cullingChanged", this);
  56.                 m_OnCullStateChanged.Invoke(cull);
  57.                 SetVerticesDirty();
  58.             }
  59.         }
复制代码
RectMask2D会将RectTransform的区域作为_ClipRect传入Shader中。
Stencil Ref 的值是0 表示它并没有使用模板缓冲比较,
如果只是矩形裁切,RectMask2D不需要一个无效的渲染用于模板比较,所以RectMask2D的效率会比Mask要高
在Shader的Frag处理像素中,被裁切掉的区域是通过UnityGet2DClipping()根据_ClipRect比较当前像素是否在裁切区域中,如果不在,将Color.a变成了透明。
  1. Shader "UI/Default"
  2. {
  3.     //...略
  4.     fixed4 frag(v2f IN) : SV_Target
  5.     {
  6.         half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
  7.         //根据_ClipRect比较当前像素是否在裁切区域中,如果不在颜色将设置成透明
  8.         #ifdef UNITY_UI_CLIP_RECT
  9.         color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
  10.         #endif
  11.         #ifdef UNITY_UI_ALPHACLIP
  12.         clip (color.a - 0.001);
  13.         #endif
  14.         return color;
  15.     }
  16. }
复制代码
参考

Unity mask的分析与理解

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-17 15:37 , Processed in 0.100654 second(s), 29 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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