Mask裁剪原理
Mask使用stencil来实现裁剪
要用Stencil方式来实现裁剪,主要涉及到2个步骤:
1) 裁剪区域设置stencil buffer: Mask在自己的GetModifiedMaterial函数中设置。
2) 被裁减物体将自己的stencil id与stencil buffer比较,如果相同,则绘制像素,否则丢弃像素。ugui中可被裁剪的ui都继承自MaskableGraphic,这部分的逻辑也是在MaskableGraphic的GetModifiedMaterial实现。
除此之外,Mask还做了一个额外的操作,就是在子节点渲染完毕后,恢复stencil buffer,这样就不会影响后续节点的渲染。
Mask.GetModifiedMaterial源码理解
public virtual Material GetModifiedMaterial(Material baseMaterial) { if (!MaskEnabled()) return baseMaterial; var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform); var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas); //Mask和Canvas之间还有几层Mask 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) //没有嵌套的Mask { var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0); StencilMaterial.Remove(m_MaskMaterial); m_MaskMaterial = maskMaterial; //---------- 恢复stencil buffer 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 stencilID = desiredStencilBit | (desiredStencilBit - 1); var writeMask = stencilID; var maskMaterial2 = StencilMaterial.Add(baseMaterial, stencilID, StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, //colorWriteMask desiredStencilBit - 1, //readMask writeMask //writeMask ); StencilMaterial.Remove(m_MaskMaterial); m_MaskMaterial = maskMaterial2; //---------- 恢复stencil buffer graphic.canvasRenderer.hasPopInstruction = true; var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.Replace, CompareFunction.Equal, 0, //colorWriteMask desiredStencilBit - 1, //readMask writeMask //writeMask ); StencilMaterial.Remove(m_UnmaskMaterial); m_UnmaskMaterial = unmaskMaterial2; graphic.canvasRenderer.popMaterialCount = 1; graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0); //---------- return m_MaskMaterial; }
里面主要涉及到2种情况
1) 没有嵌套的时候
Mask和Cavnas之间没有其他Mask,所以stencil参数会被设置为:StencilID:1, StencilComp: Always, StencilOp: Replace, StencilWriteMask: 255, StencilReadMask: 255
2) 存在嵌套的时候,比如嵌套3层
a) 最外层的Mask和Canvas之间没有其他Mask,所以stencil参数还是和上面的一样:StencilID:1, StencilComp: Always, StencilOp: Replace, StencilWriteMask: 255, StencilReadMask: 255
b) 第2层Mask和Canvas之间有1层Mask,stencil参数会被设置为:StencilID:3(0011b), StencilComp: Equal, StencilOp: Replace, StencilWriteMask: 3, StencilReadMask: 1(0001b)
注意这里的ReadMask,因为在绘制Mask_2的时候,stencil buffer的值已经被Mask设置为1,所以这边需要Mask的ID值1作为ReadMask,3&1=0011b&0001b=0001b,和stencil buffer中的值相同,此时就会执行Replace,将stencil buffer替换为3。
恢复stencil buffer时,会将stencil buffer恢复为Mask的stencilID值。
c) 第3层Mask和Canvas之间有2层Mask,stencil参数会被设置为:StencilID:7(0111b), StencilComp: Equal, StencilOp: Replace, StencilWriteMask: 7, StencilReadMask: 3(0011b)
这边会将Mask_2的ID值作为ReadMask,这样7&3才能等于3。
恢复stencil buffer时,会将stencil buffer恢复为Mask_2的stencilID值。
所以,可以看到StencilID的取值还是存在一些技巧的,使用bit的方式就是简洁高效。
MaskableGraphic.GetModifiedMaterial源码理解
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; //ui组件和Canvas之间有几层Mask 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 maskComponent = GetComponent<Mask>(); if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive())) { var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, //stencilID StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, //readMask 0 //writeMask, 不涉及写入, 所以直接总是0为也没事 ); StencilMaterial.Remove(m_MaskMaterial); m_MaskMaterial = maskMat; toUse = m_MaskMaterial; } return toUse; }
这个比较简单,只要让StencilID值和它的上层Mask相同就好,其他的2个参数是不变的:StencilOp: Keep, StencilComp: Equal。
最外层的Mask的StencilID值为1
第2层的StencilID为3
第3层的StencilID为7
GetModifiedMaterial什么时候被调用?
SetMaterialDirty的时候就会被调用到了,主要看下面两个函数:
1) Graphic.UpdateMaterial函数
protected virtual void UpdateMaterial() { if (!IsActive()) return; canvasRenderer.materialCount = 1; canvasRenderer.SetMaterial(materialForRendering, 0); canvasRenderer.SetTexture(mainTexture);
2) Graphic.materialForRendering函数
public virtual Material materialForRendering { get { var components = ListPool<Component>.Get(); GetComponents(typeof(IMaterialModifier), components); 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的子节点也还是不能被裁剪
就算把粒子的StencilID设置的和Mask一样,StencilComp:Equal, StencilOp:Keep也不行。
貌似是因为粒子和ugui渲染队列不同造成的,粒子就算作为Mask的子节点也不会和ugui在同一个渲染队列上渲染,也就造成了渲染粒子的时候,其实Mask已经把stencil buffer恢复了。
参考
<Unity-UGUI>使用Mask, 正确的裁减非默认材质UI对象_阆苑小书童的博客-CSDN博客_unity mask 多画布裁剪
【Unity源码学习】遮罩:Mask与Mask2D - 蚁丘 - 博客园 (cnblogs.com)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)