Unity Shader的StencilBuffer详解

发表于2017-12-08
评论0 3k浏览

有些开发者在接触shaderlab 的stencil buffer可能很懵逼,不知道stencil buffer是什么,网上也没有多少对stencil在untiy 的资料,为此下面就通过几个例子给大家介绍下StencilBuffer。


语法介绍

stencil buffer,也是一个buffer(废话),长度是8位,主要用于筛选pixel用,stencil buffer其实是zBuffer其中的一部分,stencil的测试与深度测试也是紧密连接,因为还要用到深度测试的结果。
语法

Stencil {

Ref 2
Comp equal
Pass keep 
Fail decrWrap 
ZFail keep

下面一条一条来:
Ref referenceValue,这个是设定参考值,stencilbuffer里面的值会与他比较
ReadMask readMask,这个是在比较参考值和buffer值的时候用的,用于读取buffer值里面的值。
WriteMask writeMask,这个是写入buffer值用的。
Comp comparisonFunction,这个比较重要,这个是比较方式。大致有Greater,GEqual,Equal等八种比较方式,具体待会列图。
Pass stencilOperation,这个是当stencil测试和深度测试都通过的时候,进行的stencilOperation操作方法。注意是都通过的时候!
Fail stencilOperation,这个是在stencil测试通过的时候执行的stencilOperation方法。这里只要stencil测试通过就可以了
ZFail stencilOperation,这个是在stencil测试通过,但是深度测试没有通过的时候执行的stencilOperation方法。

一般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  
        }  
    }   
}  


不过因为Colormask不渲染任何颜色,所以上面这个shader看不出来什么东西。下面添加一段对上面代码的改写。
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,所以还是被这个霸气侧漏的透了出来!

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引