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

先看最终的效果图:

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

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

 

github:https://github.com/luckyWjr/Demo

 

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-2550设定参考值 referenceValue
CompCompareFunctionAlways比较方法,即拿 referenceValue 和 stencilBufferValue 进行比较
PassStencilOpkeep当模板测试和深度测试都通过时,进行的处理操作
ReadMask0-255255读取的时候将该值 maskValue 与 referenceValue stencilBufferValue 分别进行按位与(&)操作
WriteMask0-255255写入的时候将该值与 referenceValue stencilBufferValue 分别进行按位与(&)操作
FailStencilOpkeep当模板测试和深度测试都失败时,进行的处理操作
ZFailStencilOpkeep当模板测试通过,深度测试失败时,进行的处理操作

UnityEngine.Rendering.CompareFunction:

Disabled模板测试或深度测试不可用
Never模板测试或深度测试 永远不通过
Less模板测试或深度测试 小于 则通过
Equal模板测试或深度测试 等于 则通过
LessEqual模板测试或深度测试 小于等于 则通过
Greater模板测试或深度测试 大于 则通过
NotEqual模板测试或深度测试 不等于 则通过
GreaterEqual模板测试或深度测试 大于等于 则通过
Always模板测试或深度测试 永远通过

UnityEngine.Rendering.StencilOp:

Keep保持当前的stencilBufferValue
Zero将值设为0
ReplacereferenceValue代替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
        }
    }
}

 

  • 18
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值