这几天学了unity shader,不得不说unity shader小小的选择项竟然蕴含了这么多东西,最近看到shaderlab 的stencil buffer真是搞得一头雾水,网上也没有对stencil在untiy 的介绍,有也是很简略的,圣典也还没翻译,就官网那几个字,真不知道写那个的人是怎么想的,就写那几个字怎么让人明白那怎么回事。好了,吐槽这么多,下面来说一下自己对stencil的认识,官网链接如下http://docs.unity3d.com/Manual/SL-Stencil.html,重点介绍那几个例子。
语法介绍
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
-
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);
-
}
-
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 frag
-
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,所以还是被这个霸气侧漏的透了出来!
好了,先写到这里,我要去吃饭了,有发现错误的地方求大神指正。