【转载】Unity Shader: 理解 Stencil Buffer 及其实战案例

947 阅读9分钟

原文链接

版权声明:本文为CSDN博主「liu_if_else」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:blog.csdn.net/liu_if_else…

正文

本文示例项目 Github 连接:UnityStencilBufferUses

最近有两次被人问到 stencil buffer 的用法,回答的含糊其辞,这两天研究了下它并总结出此文。

stencil buffer 在 OpenGL/Unity 渲染管线中的角色

为什么叫 stencil 模板

stencil 是印刷工业中的版面模子,模子上抠出需要的图案,然后将模子盖在要被印刷的材质上,对洞涂或喷绘颜色。

如果将屏幕上所有像素想象成一串连续的 0 组成的矩形,那么 stencil buffer 的作用就是将某些 0 变为 1, 2, 3 ... 255在每个 pass 中可以决定只渲染某个特定 stencil 值的像素并抛弃对其他非该值像素的操作,就像一块模板一样扣住了所有像素,并只对当前 stencil 值的洞洞进行喷绘。

stencil 与 depth

stencil buffer 与 depth buffer一样,都是缓冲区,存在于显存内的某一片区域中。据 wikipedia 上解释,目前的显卡架构中,stencil buffer 与 depth buffer 是在一起的,比如在 depth/stencil 缓冲区某个 32 位的区域中,有 24 位记录着像素 A 的 depth 数据,紧接着 8 位记录着像素 A 的 stencil 数据。也许就是由于它们连接如此紧密,在 stencil test 中可以获取到 Z test 的结果。在 Unity 中新建一个 RenderTexture 也可以通过设定深度值的位数来选择开启/关闭 stencil buffer。

stencil 测试在管线中的位置与它的写入与读取

在 OpenGL 渲染管线中,在片段着色器 fragment shader 之后, blending 混融 之前 有三个测试操作环节:

  1. Scissor Test (Unity 好像用不了)
  2. Stencil Test
  3. Z-Test

Stencil Test 环节,可通过使用关键字 Comp 读 stencil 值并与 Ref 值进行比较,通过 Keep, Zero, Incr … 对 stencil 进行写入。

所有关键字说明请看 Unity 官网

Stencil {
    //当前像素 stencil 值与 0 进行比较
    Ref 0           // 0-255
    //测试条件:测试是否相等
    Comp Equal     // default:always
    //如果测试通过对此 stencil 值进行的写入操作:保持当前 stencil 值
    Pass keep       // default:keep
    //如果测试失败对此 stencil 值进行的写入操作:保持当前 stencil 值
    Fail keep       // default:keep
    //如果深度测试失败对此 stencil 值进行的写入操作:循环递增
    ZFail IncrWrap  // default:keep
}

使用 stencil buffer 进行描边

代码

Shader "Unlit/StentilOutline"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100
                
                Stencil {
                     Ref 0          //0-255
                     Comp Equal     //default:always
                     Pass IncrSat   //default:keep
                     Fail keep      //default:keep
                     ZFail keep     //default:keep
                }

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// sample the texture
				fixed4 col = tex2D(_MainTex, i.uv);
				// apply fog
				UNITY_APPLY_FOG(i.fogCoord, col);
                                //return fixed4(1,1,0,1);
                                return col;
			}
			ENDCG
		}

                Pass
                {
                    CGPROGRAM
                    #pragma vertex vert
                    #pragma fragment frag
                    // make fog work
                    #pragma multi_compile_fog

                    #include "UnityCG.cginc"

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

                    struct v2f
                    {
                        float2 uv : TEXCOORD0;
                        UNITY_FOG_COORDS(1)
                        float4 vertex : SV_POSITION;
                    };

                    sampler2D _MainTex;
                    float4 _MainTex_ST;

                    v2f vert (appdata v)
                    {
                        v2f o;
                        o.vertex=v.vertex+normalize(v.normal)*0.01f;
                        o.vertex = UnityObjectToClipPos(o.vertex);
                        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                        UNITY_TRANSFER_FOG(o,o.vertex);
                        return o;
                    }

                    fixed4 frag (v2f i) : SV_Target
                    {
                        // sample the texture
                        fixed4 col = tex2D(_MainTex, i.uv);
                        // apply fog
                        UNITY_APPLY_FOG(i.fogCoord, col);
                        return fixed4(1,1,1,1);
                    }
                    ENDCG
                }
	}
}

说明

Stencil {
     Ref 0          //0-255
     Comp Equal     //default:always
     Pass IncrSat   //default:keep
     Fail keep      //default:keep
     ZFail keep     //default:keep
}

buffer 的值在当前帧结束前是 不清除 的,所以它可以跨越不同的 shader 与 pass。Stencil 结构写在 Subshader 中,那么下面的所有 pass 中的 stencil test 都按此运行。

理想环境下,第一个 pass 渲染前屏幕上所有像素的 stencil 值都是 0,在该 pass 的 fragment shader 结束后,所有进行了渲染的像素都通过了 Ref 0Comp Equal 的测试,并执行 Pass IncrSat 将 stencil 值加 1

...
o.vertex=v.vertex+normalize(v.normal)*0.01f;
...

第二个 pass 中,将顶点进行进行了放大。进行同样的 stencil 测试,上一个 pass 渲染过的像素 stencil 值已经变为 1,无法通过 Ref 0+Comp Equal 测试,那么现在只会在放大后的即 stencil 值仍然为 0 的区域进行渲染。

...
return fixed4(1,1,1,1);
...

第二个 pass 通过测试的像素给予描边颜色。

效果

  • 图1:使用 StencilPerPassOutline.shader image.png
  • 图2:使用 StencilOutline.shader image.png

使用 stencil buffer 进行多边形填充

这个效果与 Unity 官网中介绍 stencil 的第一个 example shader 类似,通过 stencil 值对几何体交叉区域进行判定与渲染。

代码

Shader "Unlit/PolygonsBeta"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        CGINCLUDE
        #include "UnityCG.cginc"
        struct appdata
        {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
        };

        struct v2f
        {
            float2 uv : TEXCOORD0;
            UNITY_FOG_COORDS(1)
            float4 vertex : SV_POSITION;
        };

        sampler2D _MainTex;
        float4 _MainTex_ST;
        
        v2f vert (appdata v)
        {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uv = TRANSFORM_TEX(v.uv, _MainTex);
            UNITY_TRANSFER_FOG(o,o.vertex);
            return o;
        }
        ENDCG

        Pass
        {
            Stencil {
                Ref 0           //0-255
                Comp always     //default:always
                Pass IncrWrap       //default:keep
                Fail keep       //default:keep
                ZFail IncrWrap  //default:keep
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return fixed4(0,0,0,0);
            }
            ENDCG
        }
        
        Pass
        {
            Stencil {
                Ref 2           //0-255
                Comp Equal     //default:always
                Pass keep       //default:keep
                Fail keep       //default:keep
                ZFail keep  //default:keep
            }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            
            #include "UnityCG.cginc"

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return fixed4(0.2,0.2,0.2,1);
            }
            ENDCG
        }

        Pass
        {
            Stencil {
                Ref 3          //0-255
                Comp equal     //default:always
                Pass keep   //default:keep
                Fail keep      //default:keep
                ZFail keep  //default:keep
            }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            
            #include "UnityCG.cginc"

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return fixed4(0.6,0.6,0.6,1);
            }
            ENDCG
        }

        Pass
        {
            Stencil {
                Ref 4          //0-255
                Comp equal     //default:always
                Pass keep   //default:keep
                Fail keep      //default:keep
                ZFail keep  //default:keep
            }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            
            #include "UnityCG.cginc"

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return fixed4(1,1,1,1);
            }
            ENDCG
        }
    }
}

说明

第一个 pass 渲染一个几何体,不论任何情况都通过测试并对它所覆盖的像素区域 stencil 值加 1后三个 pass 分别只对 stencil 值为 2, 3, 4 的区域进行渲染。

效果

  • 图3:使用 PolygonsBeta.shader image.png
  • 图4:使用 polygons.shader image.png

上图是结合此 shader 与以前文章里的阿基米德螺旋线算法放飞想象力的结果-_-。感觉用这招做 Logo 潜力好大…

用 stencil buffer 进行反射区域限定

代码

Shader "Unlit/TwoPassReflection"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" "Queue"="Geometry" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// sample the texture
				fixed4 col = tex2D(_MainTex, i.uv);
				// apply fog
				UNITY_APPLY_FOG(i.fogCoord, col);
				return col;
			}
			ENDCG
		}

                Pass
                {
                    Stencil {
                        Ref 1          //0-255
                        Comp Equal     //default:always
                        Pass keep      //default:keep
                        Fail keep      //default:keep
                        ZFail keep     //default:keep
                    }
                    ZTest Always
                    CGPROGRAM
                    #pragma vertex vert
                    #pragma fragment frag
                    // make fog work
                    #pragma multi_compile_fog

                    #include "UnityCG.cginc"

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

                    struct v2f
                    {
                        float2 uv : TEXCOORD0;
                        UNITY_FOG_COORDS(1)
                        float4 vertex : SV_POSITION;
                    };

                    sampler2D _MainTex;
                    float4 _MainTex_ST;

                    v2f vert (appdata v)
                    {
                        v2f o;
                        v.vertex.xyz=reflect(v.vertex.xyz,float3(-1.0f,0.0f,0.0f));
                        v.vertex.xyz=reflect(v.vertex.xyz,float3(0.0f,1.0f,0.0f));
                        v.vertex.x+=1.5f;
                        o.vertex = UnityObjectToClipPos(v.vertex);
                        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                        UNITY_TRANSFER_FOG(o,o.vertex);
                        return o;
                    }

                    fixed4 frag (v2f i) : SV_Target
                    {
                        // sample the texture
                        fixed4 col = tex2D(_MainTex, i.uv);
                        // apply fog
                        UNITY_APPLY_FOG(i.fogCoord, col);
                        return col;
                    }
                    ENDCG
                }
	}
}

Shader "Unlit/Mirror"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" "Queue"="Geometry-1" }
		LOD 100

                Stencil {
                    Ref 0          //0-255
                    Comp always    //default:always
                    Pass IncrSat   //default:keep
                    Fail keep      //default:keep
                    ZFail keep     //default:keep
                }

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// sample the texture
				fixed4 col = tex2D(_MainTex, i.uv);
				// apply fog
				UNITY_APPLY_FOG(i.fogCoord, col);
				return fixed4(0.2f,0.2f,0.2f,1.0f);
			}
			ENDCG
		}
	}
}

说明

在 TwoPassReflection.shader 中,第一个 pass 正常渲染模型,第二个 pass对顶点进行了一个简单的反射,并将 ZTest 设为 always,然后将一个 quad 放入本体和倒影之间,它的效果是这样的:

  • 图5:使用 TwoPassReflection.shader,无 mirror.shader image.png

倒影超出了想要的范围。解决这一问题,在 quad 上使用 mirror.shader 将 quad 覆盖的像素 stencil 值改为 1,并在 TwoPassReflection 第二个 pass 中约定只在 stencil 值为 1 的区域中渲染。

效果

  • 图6:quad 使用 mirror.shader image.png

这里有个 前提 是 mirror 必须在倒影之前渲染以先将反射区域的 stencil 值标记好。

阴影体 shadow volume 阴影渲染

说明

shadow volume 阴影体算法是将 ‘遮光体’ 遮挡光源后产生的阴影实例为一个几何体,对在该阴影几何体的渲染过程中找出应该渲染阴影效果的像素。

  • 图7:圆柱阴影体 image.png

检测手段有几种,本案例 shader 采用的是 Depth Fail,也叫 Carmack’s reverse 方法的思路。它的思想与步骤如下:

  1. 在一般物体渲染后,渲染阴影体,第一个 pass cull front,渲染内侧,在 stencil 测试阶段如果发现深度测试失败,说明该像素在 阴影体内部表面阴影体外部表面 与视角之间有发生遮挡,将该像素 stencil 值加 1
  2. 第二个 pass cull back,渲染外侧,如果有深度测试失败,则说明该像素在 阴影体外部表面 与视角之间有发生遮挡,将该像素 stencil 值减 1
  3. 经过两个 pass 的 stencil 操作,只有在阴影体内部的物体且它遮挡住阴影体内部表面的部分的 stencil 值为 1。对阴影体内 stencil 值为 1 的像素进行渲染。

本文中的 shader 只为展示 stencil buffer 在此技术中的角色,缺乏正确的阴影体网格或它的动态生成手段,粗暴的用 Unity 默认几何体中的圆柱体模拟一个阴影体,并且算法中也没有考虑被阴影覆盖的物体自身的阴影体的问题以及其他细节问题。

代码

Shader "Unlit/SV_DepthFailBeta"
{
    Properties
    {
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry+1"}  //在渲染所有阴影体内物体后再渲染阴影体
        LOD 100
        
        CGINCLUDE       //三个pass内着色器内容相同
        #include "UnityCG.cginc"
        struct appdata
        {
            float4 vertex : POSITION;
        };

        struct v2f
        {
            UNITY_FOG_COORDS(1)
            float4 vertex : SV_POSITION;
        };

        v2f vert (appdata v)
        {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            UNITY_TRANSFER_FOG(o,o.vertex);
            return o;
        }
        
        fixed4 frag (v2f i) : SV_Target
        {
            // apply fog
            UNITY_APPLY_FOG(i.fogCoord, col);
            return fixed4(0.3,0.3,0.3,1);           //影子颜色
        }
        ENDCG

        Pass
        {
            Cull Front          //阴影体内侧像素 Z 测试失败,stencil 值加 1
            Stencil {           
                Ref 0           //0-255
                Comp always     //default:always
                Pass keep       //default:keep
                Fail keep       //default:keep
                ZFail IncrWrap  //default:keep
            }

            ColorMask 0         //关闭 color buffer 写入
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            ENDCG
        }
        
        Pass
        {
            Cull Back           //阴影体外侧像素 Z 测试失败,stencil 值减 1
            Stencil {
                Ref 0           //0-255
                Comp always     //default:always
                Pass keep       //default:keep
                Fail keep       //default:keep
                ZFail DecrWrap  //default:keep
            }
            ColorMask 0         //关闭 color buffer 写入
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            ENDCG
        }

        Pass
        {
            Cull Back          //经过前两个 pass,stencil 值为 1 的值为在此阴影体内被阴影覆盖的像素
            Stencil {
                Ref 1          //0-255
                Comp equal     //default:always
                Pass keep      //default:keep
                Fail keep      //default:keep
                ZFail keep     //default:keep
            }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            ENDCG
        }
    }
}

效果

  • 图8:使用 SV_DepthFailBeta.shader image.png

参考