【Unity Shader】关于Stencil的理解小记

写在前面:本文只介绍Stencil最基本的三种参数:Ref Comp Pass ,其他的略略带过。
碎碎念:之前用过Stencil,但是没有完全理解,原因在于虽然理解了每项的意思,但是实际的使用效果不如人意,最后得出的结果纯靠运气试出来的。昨晚按照渲染流程走了一遍,豁然开朗,感觉自己好瓜皮qaq

Stencil简介

Stencil是模板测试的意思,通常写在Pass里面最开头,CGPROGRAM之前。Stencil会在该场景生成一个Stencil缓冲区,缓冲值初始为0。在运行中,Stencil会通过读取每个像素的缓冲值,与Ref定义的参考值进行比较,若通过则对该像素缓冲值进行Pass后的操作,并渲染该像素,若不通过则进行Fail后的操作。
stencil完整语法格式如下:

stencil{
	Ref referenceValue
	ReadMask  readMask
	WriteMask writeMask
	Comp comparisonFunction
	Pass stencilOperation
	Fail stencilOperation
	ZFail stencilOperation
}

但一般来说,我只取Ref Comp Pass三项

Ref

Ref用来设定参考值,这个值将用来与模板缓冲中的值进行比较。
例:Ref 0意为“设定参考值0”。

Comp

comp等于英语compare,意思是将参考值与缓冲值相比。
例:Ref 0 Comp Less 意为“若参考值0小于缓冲值”,千万不要弄反了。

Pass

Pass意思是通过了就执行xx操作。
例:Pass keep 意思是若通过了就不改变缓冲区的值,并渲染该像素。
参数

Comp和Pass的可取值类型如上1

举例

现在正式进入我们的探索阶段,虽然上面讲得很清楚,但是实践下来可能会发现结果和自己理解的不一样(比如我)。那么现在我们就创建一个平面投影shader2,用这个来测试Stencil。

Shader "Custom/PlanarShadow"
{
    
    
    Properties
    {
    
    
        _Tint("_Tint", Color) = (1,1,1,1)
        _MainTex("_MainTex (albedo)", 2D) = "white" {
    
    }

        [Header(Alpha)]
	    [Toggle(_CLIPPING)] _Clipping ("Alpha Clipping", Float) = 1
        _Cutoff("_Cutoff (Alpha Cutoff)", Range(0.0, 1.0)) = 0.5 // alpha clip threshold
        
        [Header(Shadow)]
        // _GroundHeight("_GroundHeight", Range(-100, 100)) = 0
        _GroundHeight("_GroundHeight", Float) = 0
        _ShadowColor("_ShadowColor", Color) = (0,0,0,1)
	    _ShadowFalloff("_ShadowFalloff", Range(0,1)) = 0.05

        // Blending state
        [HideInInspector] _SrcBlend("__src", Float) = 1.0
        [HideInInspector] _DstBlend("__dst", Float) = 0.0
        [HideInInspector] _ZWrite("__zw", Float) = 1.0
        [HideInInspector] _Cull("__cull", Float) = 2.0
    }
    SubShader
    {
    
    
	Pass {
    
    
		CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag

			#include "Lighting.cginc"
			
			struct a2v {
    
    
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};
			
			struct v2f {
    
    
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;

			};
			
			v2f vert(a2v v) {
    
    
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {
    
    

				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

				fixed3 diffuse = _LightColor0.rgb * max(0, dot(worldNormal, worldLightDir));
				return fixed4(ambient + diffuse , 1.0);
			}
			
			ENDCG
	}
        
        // Planar Shadows平面阴影
        Pass
        {
    
    
            Name "PlanarShadow"

            //用使用模板测试以保证alpha显示正确
            Stencil
            {
    
    
                Ref 0
                Comp Gequal
                Pass DecrWrap
                // Pass Keep
                // Fail Keep
                // ZFail Keep
            }

            Cull Off

            //透明混合模式
            Blend SrcAlpha OneMinusSrcAlpha

            //关闭深度写入
            ZWrite off

            //深度稍微偏移防止阴影与地面穿插
            Offset -1 , 0

            CGPROGRAM
            #pragma shader_feature _CLIPPING
            #pragma shader_feature _ALPHATEST_ON
            #pragma shader_feature _ALPHAPREMULTIPLY_ON

            #include "UnityCG.cginc"

            #pragma vertex vert
            #pragma fragment frag
            
            float _GroundHeight;
            float4 _ShadowColor;
            float _ShadowFalloff;
            half4 _Tint;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Clipping;
            half _Cutoff;

            struct appdata
            {
    
    
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
    
    
                float4 vertex : SV_POSITION;
                float4 color : COLOR;
                float2 uv : TEXCOORD0;
            };

            float3 ShadowProjectPos(float4 vertPos)
            {
    
    
                float3 shadowPos;

                //得到顶点的世界空间坐标
                float3 worldPos = mul(unity_ObjectToWorld , vertPos).xyz;

                //灯光方向
                // float3 lightDir = normalize(_LightDir.xyz);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

                //阴影的世界空间坐标(低于地面的部分不做改变)
                shadowPos.y = min(worldPos .y , _GroundHeight);
                shadowPos.xz = worldPos .xz - lightDir.xz * max(0 , worldPos .y - _GroundHeight) / lightDir.y; 

                return shadowPos;
            }

            float GetAlpha (v2f i) {
    
    
                float alpha = _Tint.a * tex2D(_MainTex, i.uv.xy).a;
                return alpha;
            }

            v2f vert (appdata v)
            {
    
    
                v2f o;

                //得到阴影的世界空间坐标
                float3 shadowPos = ShadowProjectPos(v.vertex);

                //转换到裁切空间
                o.vertex = UnityWorldToClipPos(shadowPos);

                //得到中心点世界坐标
                float3 center = float3(unity_ObjectToWorld[0].w , _GroundHeight , unity_ObjectToWorld[2].w);
                //计算阴影衰减
                float falloff = 1-saturate(distance(shadowPos , center) * _ShadowFalloff);

                //阴影颜色
                o.color = _ShadowColor;
                o.color.a *= falloff;
                
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                if (_Clipping)
                {
    
    
                    float alpha = GetAlpha(i);
                    i.color.a *= step(_Cutoff, alpha);
                }
                return i.color;
            }
            ENDCG
        }
    }
}

在场景中加入多个物体,为它们挂载上Shader,瞎摆一通使它们的阴影重叠。使用不同的Stencil设置,会出现不同的阴影重叠效果,从上到下分别为:无阴影、无重叠阴影、重叠阴影。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我用Ref 0试了所有的CompPass参数,结果如下:
在这里插入图片描述
当然,Comp没有包括Alwaysnever,因为它俩的情况比较简单,Always就是“无论如何都渲染”,never就是“无论如何都不渲染”。
我们简单分析一下上面的表格,在分析之前我们要明确三点:

  1. 需要把渲染顺序考虑进去
  2. 缓冲值实际上是8位二进制数,范围在0-255之间
  3. IncrWrap\DecrWrap 中,0-1=255,255+1=0IncrSat\DecrSat 中,0-1=0,255+1=255

首先,如果Comp后面的值是NotEqual/Greater/Less,那么都会得到无阴影的结果。可是阴影明明有重叠的范围,如果写了Pass IncrWrap ,为什么在选择Less的时候不会显示重叠区?那么我们过一遍测试流程,首先第一个物体进行模板测试,它的所有缓冲值都是0,那么比较的结果就是全部不通过,缓冲区没有任何更改,所以第二个物体进入测试的时候,和第一个物体阴影重叠部分的缓冲值仍然为0,这样比较下来依然是全部不通过,层层递推下来就完全没有通过的,最后的结果就是全部不显示。

然后,经过刚刚的推导,表上大部分的情况都可以顺下来了。还有一个问题是,为什么IncrWrapDecrWrap 的结果相同呢?这就是因为Wrap的特性,0-1=255,255+1=0。设Ref 0 Comp Lequal,当Pass DecrWrap时,第一个全部渲染后Stencil缓冲值-1,该阴影所在区域,其像素的Stencil缓冲值均等于255,因此第二个进入测试时,非重叠部分缓冲值为0,重叠部分缓冲值为255,全部比较通过,渲染并将缓冲值减1;第三个进入测试时,有两次重叠的部分缓冲值为254,一次重叠部分缓冲值为255,无重叠部分缓冲值为0,它的测试也是全部通过的;当Pass IncrWrap时,第一个全部渲染后Stencil缓冲值+1,那么第二个测试时,与第一个阴影重叠的部分全部比较通过,全部渲染,接下来的也是如此。

最后一个问题,为什么DecrWrapDecrSat 的结果不同呢?我们先考虑DecrSat,它在测试通过的时候,缓冲值-1,但前面说到,IncrSat\DecrSat 中,0-1=0,所以缓冲值依然为0,那么不通过的时候,如果不写Fail,则默认为keep,所以还是缓冲值还是0,如此算下来,DecrSat=zeroDecrWrap前面已经推导过,这里不再赘述。

写在后面:还是写了蛮多的,都是因为我的蠢问题> <估计没有人会像我这么傻哈哈哈


  1. https://gameinstitute.qq.com/community/detail/127404 ↩︎

  2. https://zhuanlan.zhihu.com/p/31504088 ↩︎

猜你喜欢

转载自blog.csdn.net/alicelise/article/details/124381749