Unity 利用 Stencil(模版测试) 实现mask的遮挡效果 可遮挡粒子和自定义遮挡形状 agile Posted on Apr 24 2020 先看最终的效果图: ![](https://img-blog.csdnimg.cn/20191016181828205.gif) 美术提了一个在按钮上加粒子特效的需求,因此需要做下裁剪的功能,防止粒子特效超框(如上图)。一开始想的是用[之前的方法](https://blog.csdn.net/wangjiangrong/article/details/79483257),给shader一个按钮位置以及长宽的Vector,计算按钮区域来计算遮挡。但是发现那个按钮的图片是一个梯形的(如图),空白的部分也不能出现粒子,这就麻烦了。 后来查了下发现shader有一个Stencil的功能,叫做模板缓存,可以实现我们想要的需求(不过似乎比较耗费性能)。这个也是Mask组件的实现原理,不过mask可以遮挡UI组件,对粒子无法产生效果。因此需要我们自己来处理下。 ### UGUI自带的Mask功能 首先先来看看UGUI的Mask功能,首先创建一个Image,取名ImageMak,用于放遮挡的图片,然后我们再创建一个Image作为其子控件,用于放需要被遮挡的图片,如图: ![](https://img-blog.csdnimg.cn/2019101711212366.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dhbmdqaWFuZ3Jvbmc=,size_16,color_FFFFFF,t_70) 然后我们在ImageMask上添加Mask组件即可,即可实现遮挡的效果。你会发现两个Image的UI/Default Shader的几个Sentcil参数发生了变化,这也是我们后面要重点讲的。 Show Mask Graphic:决定是否显示作为Mask的Image。原理是将Shader中的ColorMask的值设为0,即不输出颜色。 ![](https://img-blog.csdnimg.cn/20191017113113414.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dhbmdqaWFuZ3Jvbmc=,size_16,color_FFFFFF,t_70) ### Sentcil 这一篇,我们首先具体讲讲Sentcil的一些属性,由于内容较多具体的内容实现留到下一篇再详细讲解。 官方文档链接:[https://docs.unity3d.com/Manual/SL-Stencil.html](https://docs.unity3d.com/Manual/SL-Stencil.html) Sentcil,模板缓存可以用于实现每像素的保存或丢弃。 ```C# SubShader { Stencil { Ref 1 Comp Always Pass Replace ReadMask 255 WriteMask 255 Fail Keep ZFail Replace } } ``` 对应参数的含义: 假设当前像素缓存的值为*stencilBufferValue,即为缓存中ref的值 ![Jietu20200424-085344.png](https://tools.nxcloud.club:12500/images/2020/04/24/Jietu20200424-085344.png) UnityEngine.Rendering.CompareFunction: ![Jietu20200424-085355.png](https://tools.nxcloud.club:12500/images/2020/04/24/Jietu20200424-085355.png) UnityEngine.Rendering.StencilOp: ![Jietu20200424-085410.png](https://tools.nxcloud.club:12500/images/2020/04/24/Jietu20200424-085410.png) 模板缓存测试方法: ```C# if( (referenceValue & maskValue) comparisonFunction (stencilBufferValue & maskValue) ){ 通过测试,保留像素 } else{ 丢弃像素 } ``` ### 实验 利用手动修改Sentcil参数,实现最上面Mask效果。首先为了方便修改UGUI Image中material的shader值,我们需要UI/Default Shader的源文件(文章末尾提供源码),然后将其改个名字创建两个Material,引用该shader,分别挂载在两个Imager上,如图: ![](https://img-blog.csdnimg.cn/20191017180938567.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dhbmdqaWFuZ3Jvbmc=,size_16,color_FFFFFF,t_70) **1. **此时两张图片的Shader初始值为Ref 0,Comp Always,Pass Keep,ReadMask 255,由于Comp Always,所以像素全部都可以通过,因此此时的效果图为:我们暂时定义梯形的白色图片为图1,正方形的女生图片为图2。 ![](https://img-blog.csdnimg.cn/20191017181647279.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dhbmdqaWFuZ3Jvbmc=,size_16,color_FFFFFF,t_70) **2. **由于我们需要裁减图2,所以图2的Comp肯定不能为总是通过的Always值,我们将其改为3,即Equal。发现效果并没有产生变化,根据公式(referenceValue & maskValue) comparisonFunction (stencilBufferValue & maskValue),我们可以转化为(0&255)==(stencilBufferValue&255)为true,因此stencilBufferValue=0,而stencilBufferValue即为缓冲区ref的值,可以推断出ref的默认值为0。 **3. **此时我们需要修改缓冲区ref的值,由于图1先渲染,因此图2和图1重合的部分,图2的缓冲区ref的值即为图1的ref值。我们将图1的ref值改为1,同时需要将Pass的值改为2即Replace,这样图1的ref的值2就会代替图1的缓冲区ref的值0。效果图如下 ![](https://img-blog.csdnimg.cn/2019101718411739.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dhbmdqaWFuZ3Jvbmc=,size_16,color_FFFFFF,t_70) 即在重合部分(0&255)==(1&255)为false,所以像素丢弃了。 但是有个问题就是,为什么我梯形的左边明明是透明的,但是为什么没有显示出图2。由于即使透明,但是这个点依旧存在像素,所以依旧存在为1的缓存ref值。 解决方法有两种: 1.勾选Shader的Use Alpha Clip属性,通过源码我们可以发现,其实是执行了**clip (color.a - 0.001);**操作,即若该像素的alpha值小于0.001,则丢弃该像素。丢弃了该像素后,则缓存的ref值变为默认的0。 2.当Image Type为Simple时,勾选Use Sprite Mesh。图片的Mesh Type要选为Tight,这样生成图片网格的时候会尽可能裁剪多余的像素,由于是尽可能嘛,因此可能存在裁剪有偏差的问题,因此我们可以点击Sprite Editor,选择Custom Outline在里面进行设置,如图 ![](https://img-blog.csdnimg.cn/20191017185141386.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dhbmdqaWFuZ3Jvbmc=,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/2019101718541039.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dhbmdqaWFuZ3Jvbmc=,size_16,color_FFFFFF,t_70) [https://docs.unity3d.com/ScriptReference/SpriteMeshType.html](https://docs.unity3d.com/ScriptReference/SpriteMeshType.html) 设置后的效果如下: ![](https://img-blog.csdnimg.cn/20191017185448729.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dhbmdqaWFuZ3Jvbmc=,size_16,color_FFFFFF,t_70) **4. **此时的效果和我们需要的正好相反,我们只需要将图2的ref值设为1,即让重合部分为(1&255)==(1&255),就可达到我们的最终效果了。 ### 遮挡粒子特效 搞清楚原理后,那么为什么Mask不能遮挡粒子特效呢,其实仅仅只是因为我们的粒子特效shader没有Sentcil功能,或者其值不对而已。 因此对于我们自己的粒子特效,若没有Sentcil值,我们可以手动为其添加,和UI/Default一样即可 在Properties中添加 ```C# _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 ``` 在SubShader中添加 ```C# Stencil { Ref [_Stencil] Comp [_StencilComp] Pass [_StencilOp] ReadMask [_StencilReadMask] WriteMask [_StencilWriteMask] } ``` 然后进行相应的赋值即可,就好发现粒子特效也能成功的裁剪,是不是很爽! 我们也可以使用代码来赋值,例如先定义一个ShaderConfig类用于定义Shader我们需要的几个属性 ```C# public class ShaderConfig { public static int _StencilComp = Shader.PropertyToID("_StencilComp"); public static int _Stencil = Shader.PropertyToID("_Stencil"); public static int _StencilOp = Shader.PropertyToID("_StencilOp"); public static int _StencilReadMask = Shader.PropertyToID("_StencilReadMask"); public delegate Shader GetFunction(string name); public static GetFunction Get = Shader.Find; public static string uiEffectShader = "Custom/FGUI_FX/Particles/Additive"; public static Shader GetShader(string name) { Shader shader = Get(name); if (shader == null) { Debug.LogWarning("FairyGUI: shader not found: " + name); //shader = Shader.Find("UI/Default"); } shader.hideFlags = HideFlags.DontSaveInEditor; return shader; } } ``` 然后写一个组件挂载图1上即可,用于设置Shader的Stencil的值 ```C# public class StencilMask : MonoBehaviour { void Start() { Renderer[] array = GetComponentsInChildren<Renderer>(); foreach (var ps in array) { ps.sharedMaterial.SetFloat(ShaderConfig._StencilComp, (int)UnityEngine.Rendering.CompareFunction.Equal); ps.sharedMaterial.SetFloat(ShaderConfig._Stencil, 1); ps.sharedMaterial.SetFloat(ShaderConfig._StencilOp, (int)UnityEngine.Rendering.StencilOp.Keep); ps.sharedMaterial.SetFloat(ShaderConfig._StencilReadMask, 1); } Image image; image = GetComponent<Image>(); image.material.SetInt(ShaderConfig._StencilComp, (int)UnityEngine.Rendering.CompareFunction.Always); image.material.SetInt(ShaderConfig._Stencil, 1); image.material.SetInt(ShaderConfig._StencilOp, (int)UnityEngine.Rendering.StencilOp.Replace); image.material.SetInt(ShaderConfig._StencilReadMask, 255); } } ``` 大功告成!!!! ### UI/Default Shader: ```C# Shader "Custom/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 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Stencil { Ref [_Stencil] Comp [_StencilComp] Pass [_StencilOp] ReadMask [_StencilReadMask] WriteMask [_StencilWriteMask] } Cull Off Lighting Off ZWrite Off ZTest [unity_GUIZTestMode] Blend SrcAlpha OneMinusSrcAlpha ColorMask [_ColorMask] Pass { Name "Default" CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #include "UnityCG.cginc" #include "UnityUI.cginc" #pragma multi_compile_local _ UNITY_UI_CLIP_RECT #pragma multi_compile_local _ UNITY_UI_ALPHACLIP struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; float4 worldPosition : TEXCOORD1; UNITY_VERTEX_OUTPUT_STEREO }; sampler2D _MainTex; fixed4 _Color; fixed4 _TextureSampleAdd; float4 _ClipRect; float4 _MainTex_ST; v2f vert(appdata_t v) { v2f OUT; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); OUT.worldPosition = v.vertex; OUT.vertex = UnityObjectToClipPos(OUT.worldPosition); OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex); OUT.color = v.color * _Color; return OUT; } fixed4 frag(v2f IN) : SV_Target { half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color; #ifdef UNITY_UI_CLIP_RECT color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); #endif #ifdef UNITY_UI_ALPHACLIP clip (color.a - 0.001); #endif return color; } ENDCG } } } ``` lua中的字符串操作(模式匹配) Box Collider 2D盒子碰撞器