Unity Shader的StencilBuffer详解
有些开发者在接触shaderlab 的stencil buffer可能很懵逼,不知道stencil buffer是什么,网上也没有多少对stencil在untiy 的资料,为此下面就通过几个例子给大家介绍下StencilBuffer。
一般Comp,Pass,Fail,ZFail只用于正面的渲染,除非有Cull front,这样的语句出现。如果要渲染两面,可以用CompFront,PassFront等和CompBack,PassBack等。意思和上面的一样
比较方式:
Greater | 大于 |
GEqual | 大于等于 |
Less | 小于 |
LEqual | 小于等于 |
Equal | 等于 |
NotEqual | 不等于 |
Always | 永远通过 |
Never | 永远通不过 |
这个是在接在Comp之后的,结果可以影响Fail 的执行。
stencilOperation(stencil操作)
Keep | 保持 |
Zero | 归零 |
Replace | 拿比较的参考值替代原来buffer的值 |
IncrSat | 值增加1,但是不溢出,如果是255,就不再加 |
DecrSat | 值减少1,不溢出,到0就不再减 |
Invert | 翻转所有的位,所以1会变成254 |
IncrWrap | 值增加1,会溢出,所以255会变成0 |
DecrWrap | 值减少1,会溢出,所以0会变成255 |
至于官网的延迟光照那段大致意思就是Deffer render里面stencil Function不好用就是了。
下面上代码了,在我看来,官网的这两个例子非常难,也没有什么解释所以很不好理解。
Shader "Red" { SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry"}//这里渲染的类型为不透明物体,次序是Geometry。至于Geometry是多少我就不清楚的,求大神科普 Pass { Stencil { Ref 2 //参考值为2,stencilBuffer值默认为0 Comp always //stencil比较方式是永远通过 Pass replace //pass的处理是替换,就是拿2替换buffer 的值 ZFail decrWrap<span style="white-space:pre"> </span>//ZFail的处理是溢出型减1 } <span style="white-space:pre"> </span>//下面这段就不多说了,主要是stencil和Zbuffer都通过的话就执行。把点渲染成红色。 CGPROGRAM #pragma vertex vert #pragma fragment frag struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); return o; } half4 frag(v2f i) : SV_Target { return half4(1,0,0,1); } ENDCG } } }
结果就像这样,至于为什么要用平面来切,待会解释。好,现在在平面以上的点,stencilbuffer值全为2,因为都被replace了。在平面下面的点,通过了stencil测试但是没有通过深度测试,stencil值减一全为255。
下面是第二段。
Shader "Green" { SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry+1"}<span style="white-space:pre"> </span>//渲染次序为Geometry+1,在红球之后 Pass { Stencil { Ref 2<span style="white-space:pre"> </span>//参考值为2 Comp equal<span style="white-space:pre"> </span>//stencil比较方式是相同,这回不是都通过了 Pass keep <span style="white-space:pre"> </span>//stencil和Zbuffer都测试通过时,选择保持 Fail decrWrap <span style="white-space:pre"> </span>//stencil没通过,选择溢出型减1,所以被平面挡住的那层stencil值就变成254 ZFail keep<span style="white-space:pre"> </span>//<span style="font-family: Arial, Helvetica, sans-serif;">stencil通过,深度测试没通过时,选择保持</span> } CGPROGRAM #pragma vertex vert #pragma fragment frag struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); return o; } half4 frag(v2f i) : SV_Target { return half4(0,1,0,1); } ENDCG } } }
那个网格线就是附着了第二个shader的球体。神奇的地方来了,这个球体本身是没有颜色的,因为stencil为0的话不可能等于2的。在红球和这个球交汇处变成了绿色,注意,这个绿色不是红球的,而是“绿球的”。有一个隐藏的部分是底下被遮住部分,如果“绿球”有挡住红球部分,则stencil会变为254。这个对于下面这个shader非常重要。
Shader "Blue" { SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry+2"}<span style="white-space:pre"> </span>//渲染次序为Geometry+2,在前面两个shader之后 Pass { Stencil { Ref 254<span style="white-space:pre"> </span>//参考值为254 Comp equal<span style="white-space:pre"> </span>//比较方式是是否相等 } CGPROGRAM #pragma vertex vert #pragma fragment frag struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); return o; } half4 frag(v2f i) : SV_Target { return half4(0,0,1,1); } ENDCG } } }
好,底下那个部分很神奇吧!先解释一下,底下那个蓝色部分,是红球通过“绿球部分”再转到“蓝球”部分的,红球深度测试失败,stencil减1,再通过“绿球”stencil测试失败,stencil再减1,到这个蓝色上就符合了同样是这个蓝色块是在“蓝球”表面的。但是有个问题是红球的内表面也映到了了蓝球的上面,这个我不清楚,请高手解答
下面介绍另一组官网的shader
Shader "HolePrepare" { SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry+1"}<span style="white-space:pre"> </span>//渲染次序为<span style="font-family: Arial, Helvetica, sans-serif;">Geometry+1</span> ColorMask 0<span style="white-space:pre"> </span>//开始玩花样了,这个球不渲染任何的颜色 ZWrite off<span style="white-space:pre"> </span>//关闭深度写,代表这个球是透明的 Stencil { Ref 1<span style="white-space:pre"> </span>//参考值是1 Comp always<span style="white-space:pre"> </span>//比较方式是永远通过 Pass replace<span style="white-space:pre"> </span>//Pass选择stencil操作是替代 }
<span style="font-family: Arial, Helvetica, sans-serif;"><span style="white-space:pre"> </span>//下面是两个通道。第一个通道渲染背面(我觉得说内侧更合适),第二个通道渲染正面,但是这个比较难理解的是在深度测试成功时你看到了背面,失败时看到了正////面,这就相当于,你看一个不透明的杯子,你直接看到了内表面,你用手遮住这个杯子再看,你就看到了外表面。</span>
<span style="white-space: pre;"> </span>CGINCLUDE
Shader "HolePrepare" { SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry+1"} //ColorMask 0<span style="white-space:pre"> </span>注释掉ColorMask,让他可以显示颜色 ZWrite off Stencil { Ref 1 Comp always Pass replace } CGINCLUDE struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); return o; } half4 frag(v2f i) : SV_Target { return half4(1,1,0,1); } half4 frag2(v2f i) : SV_Target {<span style="white-space:pre"> </span>//添加一段片段函数以供调用 return half4(1,0,0,1); } ENDCG Pass { Cull Front ZTest Less CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG } Pass { Cull Back ZTest Greater CGPROGRAM #pragma vertex vert #pragma fragment frag2<span style="white-space:pre"> </span>//渲染正面时,调用frag2函数 ENDCG } } }
Shader "HolePrepare" { SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry+1"} //ColorMask 0<span style="white-space:pre"> </span>注释掉ColorMask,让他可以显示颜色 ZWrite off Stencil { Ref 1 Comp always Pass replace } CGINCLUDE struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); return o; } half4 frag(v2f i) : SV_Target { return half4(1,1,0,1); } half4 frag2(v2f i) : SV_Target {<span style="white-space:pre"> </span>//添加一段片段函数以供调用 return half4(1,0,0,1); } ENDCG Pass { Cull Front ZTest Less CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG } Pass { Cull Back ZTest Greater CGPROGRAM #pragma vertex vert #pragma fragment frag2<span style="white-space:pre"> </span>//渲染正面时,调用frag2函数 ENDCG } } }
如上图,主要的改动的是三个方面,效果如下
嗯,所以可以直接被看见部分(内侧)渲染黄色,被挡住部分(外侧)渲染红色,然后中间的部分,挡住了内侧,暴露了外侧,所以不渲染,为什么要对这个shader解释这么多呢,原因就在下面这个shader。
Shader "Hole" { Properties { _Color ("Main Color", Color) = (1,1,1,0) } SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry+2"}<span style="white-space:pre"> </span>//渲染次序为Geometry+2 ColorMask RGB<span style="white-space:pre"> </span>//显示出颜色,这个默认的,写不写都一样 Cull Front<span style="white-space:pre"> </span>//只渲染背面,嗯,这个shader开始牛起来了 ZTest Always<span style="white-space:pre"> </span>//深度测试永远通过,霸气侧漏!这个意味着不管你怎么挡,这个球始终可以在你眼皮子底下出现 Stencil { Ref 1<span style="white-space:pre"> </span>//参考值为1 Comp notequal <span style="white-space:pre"> </span>//比较方式为不相等<span style="white-space:pre"> </span> } CGPROGRAM #pragma surface surf Lambert float4 _Color; struct Input { float4 color : COLOR; }; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _Color.rgb; o.Normal = half3(0,0,-1); o.Alpha = 1; } ENDCG } }
这个球的样子是这样
没人挡得住它的显示了
这是他被平面切割的情况,根本看不出来啊!好,这个球说,“谁能挡我!!”
然后他就被挡了
好,解释一下,这个透明球先渲染,所以他上面的内表面和下面的外表秒stencil值为1,或许有人会问上面内表面可以赋值是可以理解的,下面外表面的怎么回事?我也想了好久,其实答案就在每个通道的ztest,内表面的为ztest Less,说明这个内表面的只要在物体前面就可以渲染出来,即深度测试成功,stencil赋值为1,而下面的外表面为ztest Greater,说明这个外表面需要在被挡住的时候显示出来,很贱有没有,这个时候深度测试成功。中间那部分因为没有深度测试成功,所以shencil值还是为0,所以还是被这个霸气侧漏的透了出来!