Unity Shader: 理解Stencil buffer并将它用于一些实战案例(描边,多边形填充,反射区域限定,阴影体shadow volume阴影渲染)

本文示例项目Github连接:https://github.com/liu-if-else/UnityStencilBufferUses

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

文章目录

  • 1,Stencil buffer在OpenGL/Unity 渲染管线中的角色
    • 为什么叫stencil 模板
    • stencil 与 depth
    • stencil测试在管线中的位置与它的写入与读取
  • 2,使用Stencil buffer进行描边
    • 代码
    • 说明
    • 效果
  • 3,使用stencil buffer进行多边形填充
    • 代码
    • 说明
    • 效果
  • 4,用stencil buffer进行反射区域限定
    • 代码
    • 说明
    • 效果
  • 5,阴影体shadow volume阴影渲染
    • 说明
    • 代码
    • 效果

1,Stencil buffer在OpenGL/Unity 渲染管线中的角色

为什么叫stencil 模板

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

如果将屏幕上所有像素想象成一串连续的0,那么stencil buffer的作用就是将某些0变为1,2,3…255,每个pass之前可以决定只渲染某个特定stencil值的像素并抛弃其他像素,就像一块模板一样扣住了所有其他非该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。

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

在OpenGL渲染管线中,片段着色器之后,Blending混融之前有三个测试操作环节: scissor test(unity好像没提供),Stencil Test,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
            }

2,使用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
        }

stencil的默认值是0,而buffer的值在当前帧结束前是不清除的,所以它可以跨越不同的shader与pass。Stencil结构写在Subshader中,那么下面的所有pass中的stencil test都按此运行。理想环境下,第一个pass渲染前屏幕上所有像素的stencil值都是0,在该pass fragment shader片段作色器结束后,所有进行了渲染的像素stencil值都变为了1。

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

第二个pass中,将顶点进行进行放大。
进行同样的stencil测试,上一个pass渲染的区域stencil值已经变为1,那么现在只会在放大后的区域,stencil值仍然为0的区域,进行与渲染。

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

给予描边颜色。

效果

Unity Shader: 理解Stencil buffer并将它用于一些实战案例(描边,多边形填充,反射区域限定,阴影体shadow volume阴影渲染)_第1张图片
[图1:使用StencilPerPassOutline.shader]
Unity Shader: 理解Stencil buffer并将它用于一些实战案例(描边,多边形填充,反射区域限定,阴影体shadow volume阴影渲染)_第2张图片
[图2:使用StencilOutline.shader]

3,使用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的区域进行区别渲染。

效果

Unity Shader: 理解Stencil buffer并将它用于一些实战案例(描边,多边形填充,反射区域限定,阴影体shadow volume阴影渲染)_第3张图片
[图3:使用PolygonsBeta.shader]
Unity Shader: 理解Stencil buffer并将它用于一些实战案例(描边,多边形填充,反射区域限定,阴影体shadow volume阴影渲染)_第4张图片
[图4:使用polygons.shader]

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

4,用stencil buffer进行反射区域限定

此用法主要是辅助一个简单的反射shader,可以比较简单的模拟出一个镜面效果。

代码

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放入本体和倒影之间,它的效果是这样的:
Unity Shader: 理解Stencil buffer并将它用于一些实战案例(描边,多边形填充,反射区域限定,阴影体shadow volume阴影渲染)_第5张图片
[图5:使用TwoPassReflection.shader(无stencil测试)]

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

效果

Unity Shader: 理解Stencil buffer并将它用于一些实战案例(描边,多边形填充,反射区域限定,阴影体shadow volume阴影渲染)_第6张图片
[图5:quad使用mirror.shader]

5,阴影体shadow volume阴影渲染

说明

shadow volume是将‘遮挡体’遮挡光源后产生的阴影实例为一个几何体,对在该阴影几何体的渲染过程中检测应该渲染阴影的像素。
Unity Shader: 理解Stencil buffer并将它用于一些实战案例(描边,多边形填充,反射区域限定,阴影体shadow volume阴影渲染)_第7张图片
[图6:圆柱阴影体]

检测手段有几种,本案例的shader是根据Depth Fail,也叫Carmack’s reverse方法的思路。步骤:
1,在一般物体渲染后,渲染阴影体,第一个pass cull front,渲染内侧,在stencil测试阶段如果发现深度测试失败,说明该像素在阴影体内部或阴影体外部与视角之间,将该像素stencil值加1。
2,第二个pass cull back,渲染外侧,如果有深度测试失败,则说明该像素在阴影体外部与视角之间,将该像素stencil值减1。经过两个pass的stencil操作,应该正确被阴影覆盖的像素的stencil值为1。
3,对阴影体内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
        }
    }
}

效果

Unity Shader: 理解Stencil buffer并将它用于一些实战案例(描边,多边形填充,反射区域限定,阴影体shadow volume阴影渲染)_第8张图片
[图7:使用SV_DepthFailBeta.shader]


参考:
Shadow Volume–Depth Fail, wikipedia:https://en.wikipedia.org/wiki/Shadow_volume#Depth_fail

Creating Reflections and Shadows Using Stencil, BuffersMark J. Kilgard:https://www2.cs.duke.edu/courses/spring15/cps124/classwork/14_buffers/stencil.pdf

Stencil Shadow Volume, OGL: http://ogldev.atspace.co.uk/www/tutorial40/tutorial40.html

ShaderLab: Stencil, Unity: https://docs.unity3d.com/Manual/SL-Stencil.html

Simon F’s answer to topic of ‘Uses for Stencil Buffer’ :https://computergraphics.stackexchange.com/questions/5046/uses-for-stencil-buffer

本文示例项目Github连接:https://github.com/liu-if-else/UnityStencilBufferUses

你可能感兴趣的:(Unity3D&Shader,Shader与Unity3D)