Unity 利用 Stencil(模版测试) 实现mask的遮挡效果 可遮挡粒子和自定义遮挡形状

先看最终的效果图:

美术提了一个在按钮上加粒子特效的需求,因此需要做下裁剪的功能,防止粒子特效超框(如上图)。一开始想的是用之前的方法,给shader一个按钮位置以及长宽的Vector,计算按钮区域来计算遮挡。但是发现那个按钮的图片是一个梯形的(如图),空白的部分也不能出现粒子,这就麻烦了。

后来查了下发现shader有一个Stencil的功能,叫做模板缓存,可以实现我们想要的需求(不过似乎比较耗费性能)。这个也是Mask组件的实现原理,不过mask可以遮挡UI组件,对粒子无法产生效果。因此需要我们自己来处理下。

UGUI自带的Mask功能

首先先来看看UGUI的Mask功能,首先创建一个Image,取名ImageMak,用于放遮挡的图片,然后我们再创建一个Image作为其子控件,用于放需要被遮挡的图片,如图:

然后我们在ImageMask上添加Mask组件即可,即可实现遮挡的效果。你会发现两个Image的UI/Default Shader的几个Sentcil参数发生了变化,这也是我们后面要重点讲的。

Show Mask Graphic:决定是否显示作为Mask的Image。原理是将Shader中的ColorMask的值设为0,即不输出颜色。

Sentcil

这一篇,我们首先具体讲讲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<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:

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
        }
    }
}
发布了71 篇原创文章 · 获赞 160 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/wangjiangrong/article/details/102551120
今日推荐