Unity5.6如何使用StencilBuffer实现SpriteRenderer的Mask
什么是StencilBuffer
模板缓冲区(StencilBuffer)可以为屏幕上的每个像素点保存一个无符号整数值,在渲染的过程中,可以用这个值与一个预先设定的参考值相比较,根据比较的结果来决定是否更新相应的像素点的颜色值.这个比较的过程被称为模板测试.
主要过程为:将StencilBuffer的值与ReadMask与运算,然后与Ref值进行Comp比较,结果为true时进行Pass操作,否则进行Fail操作,操作值写入StencilBuffer前先与WriteMask与运算.
公式:(Ref & ReadMask) Comp (StencilBufferValue & ReadMask)
问题一:在未使用模板检测时,我们的屏幕到底默认的模板检测缓冲数值是多少?
我们用Unity来测试一下这个问题:
- 先准备两个shader
第一个Shader 叫RedShader 来自官方文档
Shader "Unlit/RedShader"
{
SubShader
{
//RenderType 为 Qpaque代表渲染的物体不透明
//Queue 为 Geometry 代表物体的渲染队列在不透明列队中
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass
{
Stencil{
Ref 2 //将写入模板测试缓存区中的值
Comp always //比较方式 always代表不会进行Fail操作
Pass replace //通过后所进行的操作,replace代表向模板缓存区中写入引用的值
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return half4(1,0,0,1);//最终输出到屏幕的颜色为红色,
}
ENDCG
}
}
}
- 有了第一个Shader,我们看看窗口效果,我们可以看到红色的一片,下一图则是显示StencilBuffer理论上应该显示值


3.接着我们添加第二个Shader GreenShader,修改的只是模板测试的值,其余均没有变化,最终输出颜色变为绿色,可以做比较
Shader "Unlit/GreenShader"
{
SubShader
{
//RenderType 为 Qpaque代表渲染的物体不透明
//Queue 为 Geometry 代表物体的渲染队列在不透明列队中
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass
{
Stencil{
Ref 2 //这里与RedShader值一样,用于与模板测试缓冲区的值做比较
Comp equal //equal 代表当前的Ref与模板测试缓冲区的值相等时通过模板测试
Pass keep //通过测试通过后,保持模板测试缓冲区中的值不变
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return half4(0,1,0,1);//最终输出到屏幕的颜色为绿色,
}
ENDCG
}
}
}
- 现在我们再来看看窗口的结果,发现绿色方块只有在红色范围内才显示
这是为什么呢,因为第二个shader所做事情就是与第一个shader写入模板测试缓冲区的值进行比较,如若两者相等,保留该像素的颜色,不相等剔除该像素颜色,我们再看看第二图,发现与第一个shader的第二图结果相同,这是因为采用的比较方式是(Comp Equal)。


5.通过上面的例子我们大致了解了StencilBuffer如果工作,再回到我们最初的问题,在未使用模板检测时,我们的屏幕到底默认的模板检测缓冲数值是多少?
我们修改一下第一个Shader(RedShader)的模板测试值,改为
Stencil{
Ref 0 //将写入模板测试缓存区中的值
Comp always //比较方式 always代表不会进行Fail操作
Pass replace //通过后所进行的操作,replace代表向模板缓存区中写入引用的值
}
其余不变
修改第二个Shader(BlueShader)的模板测试值,改为
Stencil{
Ref 0 //这里与RedShader值一样,用于与模板测试缓冲区的值做比较
Comp equal //equal 代表当前的Ref与模板测试缓冲区的值相等时通过模板测试
Pass keep //通过测试通过后,保持模板测试缓冲区中的值不变
}
两个shader都只改了Ref的值为0,但是我们可以看到窗口中的显示效果

似乎模板测试失效了,其实不然,而是因为默认屏幕的模板检测缓冲数值是0,因此当第一个Shader写入模板缓冲区的值为0时,第二个Shader与模板缓冲区中的值做比较时,位于红色区域范围外的值在第二个Shader进行模板缓冲测试时,其也能通过模板缓冲测试,因此它的像素值不会被舍弃。
问题二:对于Zfail DecrWrap这个属性该如何理解
- 我们先来添加第三个Shader 命名为(BlueShader)
它用于模板测试的参考值(Ref 为1),采用的比较方式为是否相等(Comp equal),最终输出颜色为蓝色
2.我们在第二个Shader (GreenShader)中的模板测试中添加一个属性(ZFail DecrWrap),这个属性的作用在于当模板缓冲测试通过,但深度缓冲测试没有通过,则进行减小模板缓冲区的值,如果原来的值为0,则会变为255,如果值大于0,则其值减一
Shader "Unlit/BlueShader"
{
SubShader
{
//RenderType 为 Qpaque代表渲染的物体不透明
//Queue 为 Geometry 代表物体的渲染队列在不透明列队中
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass
{
Stencil{
Ref 1 //这里小于GreenShader值,用于与模板测试缓冲区的值做比较
Comp equal //equal 代表当前的Ref与模板测试缓冲区的值相等时通过模板测试
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return half4(0,0,1,1);//最终输出到屏幕的颜色为绿色,
}
ENDCG
}
}
}
3.通过窗口观察效果,将RedShader,GreenShader,BlueShader分别给不同的球体,观看最终显示结果
发现只有在红球和绿球相交的部分,才出现了蓝球的颜色,这是因为蓝球检测的参考值(Ref 为1),比较方式为(Comp equal),所以相交部分外的模板检测结果都会被剔除
4.那为什么相交部分没有被蓝球全部占据?
那是因为在绿球的(GreenShader)中,模板检测中我们加入了(ZFail DecrWrap)属性,蓝色显示的部分正是由于深度检测失败,但是模板检测通过的结果,下一张图将说明当前各个部分的模板检测值


上图显示了红色和绿色区域的模板检测值为2,蓝色区域的模板检测值为1,其余地方的模板检测值均为0
问题三:回归主题我们该如何使用StencilBuffer去实现SpriteRenderer的Mask
1.首先准备第一个Shader,命名为SpriteRendererMask,这个shader就是我们的Maskshader,可以实现遮罩的功能,在Hierarchy中新建一个GameObject空物体,将其命名为Mask,为其添加一个SpriteRenderer组件,在Assets中创建SpriteRendererMask对于的材质球,将其赋予刚刚创建的SpriteRenderer组件,现在你会发现你看不到任何显示,因此你需要一张贴图,这里我准备了一张贴图,将贴图导入unity,设置它的Texture Type 为 Sprite,将它赋予SpriteRenderer,并设置材质的_ColorMask为15,你就能看到遮罩的效果。

Shader "WBL/Mask/SpriteRendererMask"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//_MaskTex("Mask",2D)="white"{}
_Cutoff("Cutoff",Range(0,1))=0.5
_ColorMask("_ColorMask",Range(0,15))=0
}
SubShader
{
//RenderType 为 TransparentCutout代表渲染的物体可以裁剪
//Queue 为 AlphaTest 代表物体的渲染队列在透明度测试列队中
Tags {
"RenderType"="TransparentCutout" "Queue"="AlphaTest"
}
ColorMask [_ColorMask] //颜色遮罩值为0,表示不把颜色输出到屏幕上。
Stencil{
Ref 10 //用于写入到模板缓冲区中的参考值
Comp Greater //比较方式为大于 表示只要参考值比模板缓冲区中的值大就写入模板缓冲区
Pass replace //当模板测试通过,将参考值写入模板缓冲区
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _Cutoff;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
clip(col.a-_Cutoff);
return col;
}
ENDCG
}
}
}
2.有了遮罩,接下来我们就要在遮罩下创建正常显示的图片,看遮罩效果是否能显示,因此我们需要另一个Shader,将其命名为SpriteRendererNormal,通过这个Shader,我们可以显示一张贴图,在遮罩范围内,在Mask节点的下面创建一个Normal空节点,为其添加SpriteRenderer组件,然后创建一个用于该组件的材质,为其增加一张贴图,你会看到如下的效果,这是你会发现,好像结果不对,背景颜色没有去掉,将Mask节点的材质属性_ColorMask设置为0.
(记得要将Normal节点的Transform的Position.z=-0.01,其余都设置为0)


3.最终会看到如下效果,从效果可以看出里面的图片被外面的圆圈所限制

Shader "WBL/Mask/SpriteRendererNormal"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Cutoff("Cutoff",Range(0,1))=0.5
}
SubShader
{
//RenderType 为 Transparent代表渲染的物体不透明
//Queue 为 Transparent 代表物体的渲染队列在不透明列队中
Tags { "RenderType"="Transparent" "Queue"="Transparent"}
Blend SrcAlpha OneMinusSrcAlpha
Zwrite On
//ZTest Greater
Stencil{
Ref 10
Comp equal
Pass keep
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _Cutoff;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
问题四:这种遮罩当两个图片有所重叠时会出现不正常的效果,该如何解决
1.当两个图片重叠时,就会出现下列这种现象

2.其实这个只需要改变SpriteRenderer的orderLayer值就能正常显示,对于你想要让它显示在后面的图片,则改变orderLayer,让其Mask节点的orderLayer和Normal节点的orderLayer都大于另一张图片的orderLayer比如,可以设置当前问号这张图片的orderLayer都为2,而设置前面那张锁的图片的orderlayer为1,则会看到如下效果,两者的顺序恢复正常

本文介绍了Unity5.6中如何利用StencilBuffer实现SpriteRenderer的遮罩功能。首先解释了StencilBuffer的工作原理,通过几个Shader实例探讨了模板缓冲区的默认值和Zfail DecrWrap属性。接着详细阐述了创建遮罩和正常显示图片的步骤,以及解决图片重叠时遮罩显示异常的方法,涉及orderLayer的调整。
1万+

被折叠的 条评论
为什么被折叠?



