先看最终的效果图:
美术提了一个在按钮上加粒子特效的需求,因此需要做下裁剪的功能,防止粒子特效超框(如上图)。一开始想的是用之前的方法,给shader一个按钮位置以及长宽的Vector,计算按钮区域来计算遮挡。但是发现那个按钮的图片是一个梯形的(如图),空白的部分也不能出现粒子,这就麻烦了。
后来查了下发现shader有一个Stencil的功能,叫做模板缓存,可以实现我们想要的需求(不过似乎比较耗费性能)。这个也是Mask组件的实现原理,不过mask可以遮挡UI组件,对粒子无法产生效果。因此需要我们自己来处理下。
首先先来看看UGUI的Mask功能,首先创建一个Image,取名ImageMak,用于放遮挡的图片,然后我们再创建一个Image作为其子控件,用于放需要被遮挡的图片,如图:
然后我们在ImageMask上添加Mask组件即可,即可实现遮挡的效果。你会发现两个Image的UI/Default Shader的几个Sentcil参数发生了变化,这也是我们后面要重点讲的。
Show Mask Graphic:决定是否显示作为Mask的Image。原理是将Shader中的ColorMask的值设为0,即不输出颜色。
这一篇,我们首先具体讲讲Sentcil的一些属性,由于内容较多具体的内容实现留到下一篇再详细讲解。
官方文档链接:https://docs.unity3d.com/Manual/SL-Stencil.html
Sentcil,模板缓存可以用于实现每像素的保存或丢弃。
SubShader {
Stencil {
Ref 1
Comp Always
Pass Replace
ReadMask 255
WriteMask 255
Fail Keep
ZFail Replace
}
}
对应参数的含义:
假设当前像素缓存的值为stencilBufferValue,即为缓存中ref的值
参数 | 值 | 默认值 | 含义 |
Ref |
0-255 | 0 | 设定参考值 referenceValue |
Comp | CompareFunction | Always | 比较方法,即拿 referenceValue 和 stencilBufferValue 进行比较 |
Pass | StencilOp | keep | 当模板测试和深度测试都通过时,进行的处理操作 |
ReadMask | 0-255 | 255 | 读取的时候将该值 maskValue 与 referenceValue 和 stencilBufferValue 分别进行按位与(&)操作 |
WriteMask | 0-255 | 255 | 写入的时候将该值与 referenceValue 和 stencilBufferValue 分别进行按位与(&)操作 |
Fail | StencilOp | keep | 当模板测试和深度测试都失败时,进行的处理操作 |
ZFail | StencilOp | keep | 当模板测试通过,深度测试失败时,进行的处理操作 |
UnityEngine.Rendering.CompareFunction:
Disabled | 模板测试或深度测试不可用 |
Never | 模板测试或深度测试 永远不通过 |
Less | 模板测试或深度测试 小于 则通过 |
Equal | 模板测试或深度测试 等于 则通过 |
LessEqual | 模板测试或深度测试 小于等于 则通过 |
Greater | 模板测试或深度测试 大于 则通过 |
NotEqual | 模板测试或深度测试 不等于 则通过 |
GreaterEqual | 模板测试或深度测试 大于等于 则通过 |
Always | 模板测试或深度测试 永远通过 |
UnityEngine.Rendering.StencilOp:
Keep | 保持当前的stencilBufferValue |
Zero | 将值设为0 |
Replace | referenceValue代替stencilBufferValue |
IncrementSaturate | 将值+1,若值为255则不变(不溢出) |
DecrementSaturate | 将值-1,若值为0则不变(不溢出) |
Invert | 按位取反,即若为0,则变为255 |
IncrementWrap | 将值+1,若值为255则变为0(溢出) |
DecrementWrap | 将值-1,若值为0则变为255(溢出) |
模板缓存测试方法:
if( (referenceValue & maskValue) comparisonFunction (stencilBufferValue & maskValue) ){
通过测试,保留像素
}
else{
丢弃像素
}
利用手动修改Sentcil参数,实现最上面Mask效果。首先为了方便修改UGUI Image中material的shader值,我们需要UI/Default Shader的源文件(文章末尾提供源码),然后将其改个名字创建两个Material,引用该shader,分别挂载在两个Imager上,如图:
1. 此时两张图片的Shader初始值为Ref 0,Comp Always,Pass Keep,ReadMask 255,由于Comp Always,所以像素全部都可以通过,因此此时的效果图为:我们暂时定义梯形的白色图片为图1,正方形的女生图片为图2。
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。效果图如下
即在重合部分(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://docs.unity3d.com/ScriptReference/SpriteMeshType.html
设置后的效果如下:
4. 此时的效果和我们需要的正好相反,我们只需要将图2的ref值设为1,即让重合部分为(1&255)==(1&255),就可达到我们的最终效果了。
搞清楚原理后,那么为什么Mask不能遮挡粒子特效呢,其实仅仅只是因为我们的粒子特效shader没有Sentcil功能,或者其值不对而已。
因此对于我们自己的粒子特效,若没有Sentcil值,我们可以手动为其添加,和UI/Default一样即可
在Properties中添加
_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中添加
Stencil {
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
然后进行相应的赋值即可,就好发现粒子特效也能成功的裁剪,是不是很爽!
我们也可以使用代码来赋值,例如先定义一个ShaderConfig类用于定义Shader我们需要的几个属性
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的值
public class StencilMask : MonoBehaviour
{
void Start()
{
Renderer[] array = GetComponentsInChildren();
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.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);
}
}
大功告成!!!!
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
}
}
}