您现在的位置是:首页 >技术杂谈 >3.1 深度和模板测试网站首页技术杂谈
3.1 深度和模板测试
目录
什么是深度测试?
深度值
要理解深度测试,先要明白深度值的概念。在渲染管线中,物体会从Vertex Shader经过MVP矩阵变换到齐次裁剪空间,然后经过齐次除法变换到标准化设备空间(NDC),最后经过视口变换到屏幕空间。在屏幕空间中物体表面上任意点的坐标的Z值即为深度值。
在深度缓冲区中深度值是非线性的,越靠近平面,Z值越小,精度越高。
具体可以参考这篇文章:OpenGL Projection Matrix (songho.ca)
精度过低会导致深度冲突,简要来说就是两个平面或三角形如此紧密相互平行深度缓冲区不具有足够的精度以至于无法得到哪一个靠前。
防止深度冲突一般来说有三种办法:
(1)物体之间不要离得太近。
(2)尽可能把近平面设置得远一些。
(3)放弃一些性能来得到更高的深度值的精度,从24位提高到32位。
深度缓冲区(Z-Buffer)
深度缓冲就像颜色缓冲(储存所有的片段颜色:视觉输出)一样,在每个片段中储存了信息,并且(通常)和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是24位的。
深度测试(Z-Test)
所谓深度测试,就是针对当前对象在屏幕上对应的像素点,将对象自身的深度值与当前像素点缓存的深度值进行比较,如果通过了,本对象在该像素点才会将颜色写入颜色缓冲区,否则不会写入颜色缓冲。
Z-Test比较操作:
ZTest 状态 | 描述 |
Greater | 深度大于当前缓存则通过 |
LEqual | 深度小于等于当前缓存则通过 |
Less | 深度小于当前缓存则通过 |
GEqual | 深度大于等于当前缓存则通过 |
Equal | 深度等于当前缓存则通过 |
NotEqual | 深度不等于当前缓存则通过 |
Always(Off) | 深度不论如何都通过 |
Never | 深度不论如何都不通过 |
深度写入(Z-Write)
深度写入是指将当前渲染对象的深度值写入深度缓存之中,替换掉原先的深度缓存值。深度是否写入与两个条件有关,只有深度测试通过并且深度写入开启(ZWrite On)才能够进行深度写入。
ZTest分为通过和不通过两种情况,ZWrite分为开启和关闭两种情况的话,一共就是四种情况:
-
深度测试通过,深度写入开启:写入深度缓冲区,写入颜色缓冲区;
-
深度测试通过,深度写入关闭:不写深度缓冲区,写入颜色缓冲区;
-
深度测试失败,深度写入开启:不写深度缓冲区,不写颜色缓冲区;
-
深度测试失败,深度写入关闭:不写深度缓冲区,不写颜色缓冲区;
Early-Z技术
传统的渲染管线中,ZTest其实是在Blending阶段,这时候进行深度测试,所有对象的像素着色器都会计算一遍,没有什么性能提升,仅仅是为了得出正确的遮挡结果,会造成大量的无用计算,因为每个像素点上肯定重叠了很多计算。
现代GPU中运用了Early-Z的技术,在Vertex阶段和Fragment阶段之间(光栅化之后,fragment之前)进行一次深度测试,如果深度测试失败,就不必进行fragment阶段的计算了,因此在性能上会有很大的提升。但是最终的ZTest仍然需要进行,以保证最终的遮挡关系结果正确。前面的一次主要是Z-Cull为了裁剪以达到优化的目的,后一次主要是Z-Check,为了检查。
深度测试在Unity中的实现及应用(填坑,慢慢补完)
(1)语法表示
ZTest Equal
参数 | 值 | 功能 |
---|---|---|
operation | Less | 绘制位于现有几何体前面的几何体。不绘制位于现有几何体相同距离或后面的几何体。 |
LEqual | 绘制位于现有几何体前面或相同距离的几何体。不绘制位于现有几何体后面的几何体。 这是默认值。 | |
Equal | 绘制位于现有几何体相同距离的几何体。不绘制位于现有几何体前面的或后面的几何体。 | |
GEqual | 绘制位于现有几何体后面或相同距离的几何体。不绘制位于现有几何体前面的几何体。 | |
Greater | 绘制位于现有几何体后面的几何体。不绘制位于现有几何体相同距离或前面的几何体。 | |
NotEqual | 绘制不位于现有几何体相同距离的几何体。不绘制位于现有几何体相同距离的几何体。 | |
Always | 不进行深度测试。绘制所有几何体,无论距离如何。 |
(2)深度测试应用
1.基于深度的着色(湖水渲染)
参考文章:Unity Toon Water Shader Tutorial (roystan.net)
2.阴影贴图(ShadowMap)
ShadowMap是实时渲染中阴影的一种实现方式,其基本步骤是:
(1)创建一个相机专门从光源的视角渲染整个场景,获得深度纹理
(2)实际相机渲染物体,将物体从世界空间转换到光源空间,与深度纹理对比数据获得阴影信息
(3)根据阴影信息渲染场景
3.透明物体、粒子渲染
4.透视X-Ray效果
5.切边效果
什么是模板测试?
模板测试位于深度测试(Depth Test)之前,透明度测试(Alpha Test)之后。它和深度测试类似,需要一个模板缓冲来存储模板值。通常这个模板值(Stencil Value)是8位的,即每个像素/片段一共能有256种不同的模板值。模板缓冲的初始值会被清除为0。
模板缓冲操作允许我们在渲染片段时将模板缓冲设定为一个特定的值(如上图中将部分的值设置为1),这个值的具体意义与程序的具体应用有关。在渲染的过程中,可以用某个值与这个预先设定的参考值作比较,根据比较的结果来决定是否更新相应的像素点的颜色值,这个比较的过程被称为模板测试。
在上面的过程中, 需要注意几个关键的信息点:
-
模板值:模板缓存中已经存在的值
-
参考值:在渲染该物体前为其设定的值
-
比较函数:决定如何比较模板值和参考值
-
操作函数:根据不同的比较结果对模板值及像素颜色的操作
模板测试在Unity中的实现及应用
(1)语法表示
Stencil
{
Ref 1
Comp Always
Pass Keep
Fail Keep
ZFail Keep
WriteMask 255
ReadMask 255
}
Ref 用来指定参考值1231
Comp 用来指定比较函数,比较函数如下表所示:
Greater | 相当于“>”操作,即仅当左边>右边,模板测试通过,渲染像素 |
GEqual | 相当于“>=”操作,即仅当左边>=右边,模板测试通过,渲染像素 |
Less | 相当于“<”操作,即仅当左边<右边,模板测试通过,渲染像素 |
LEqual | 相当于“<=”操作,即仅当左边<=右边,模板测试通过,渲染像素 |
Equal | 相当于“=”操作,即仅当左边=右边,模板测试通过,渲染像素 |
NotEqual | 相当于“!=”操作,即仅当左边!=右边,模板测试通过,渲染像素 |
Always | 不管公式两边为何值,模板测试总是通过,渲染像素 |
Never | 不敢公式两边为何值,模板测试总是失败 ,像素被抛弃 |
Pass、Fail 和 ZFail 分别表示通过模板测试、不通过模板测试以及通过模板测试却没通过深度测试这三种情况,用来指定对模板值的更新操作。更新操作如下表所示:
Keep | 保留当前缓冲中的内容,即stencilBufferValue不变 |
Zero | 将0写入缓冲,即stencilBufferValue值变为0 |
Replace | 将参考值写入缓冲,即将referenceValue赋值给stencilBufferValue |
IncrSat | stencilBufferValue加1,如果stencilBufferValue超过255了,那么保留为255,即不大于255 |
DecrSat | stencilBufferValue减1,如果stencilBufferValue超过为0,那么保留为0,即不小于0 |
Invert | 将当前模板缓冲值(stencilBufferValue)按位取反 |
IncrWrap | 当前缓冲的值加1,如果缓冲值超过255了,那么变成0,(然后继续自增) |
DecrWrap | 当前缓冲的值减1,如果缓冲值已经为0,那么变成255,(然后继续自减) |
ReadMask 和 WriteMask 两个掩码是对模板值和参考值的额外处理,值为0-255之间的整数值。处理方法参考模板测试方程:
(ref & readMask)comparisonFunction (stencilBufferValue & readMask)
(2)模板测试应用
1.描边
代码部分:
Shader "Unlit/StencilOutline"
{
Properties
{
_MainTex ("基本贴图", 2D) = "white" {}
_OutlineWidth ("描边宽度", Range(0, 1)) = 0.2
_OutlineCol ("描边颜色", Color) = (0, 0, 0, 1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
Stencil
{
Ref 1
Comp Greater
Pass Replace
}
// 基础Pass
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;
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);
return col;
}
ENDCG
}
// 描边Pass
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float4 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
float _OutlineWidth;
fixed4 _OutlineCol;
v2f vert (appdata v)
{
v2f o;
v.vertex += v.normal * _OutlineWidth * 0.1f;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return _OutlineCol;
}
ENDCG
}
}
}
解释说明: 第一个Pass用于模型的基本着色并设定模型所在区域的模板缓冲值,第二个Pass通过顶点在法线方向的扩张来进行描边。
2.多边形填充
代码部分:
Shader "Stencil/PolygonsFill"
{
CGINCLUDE
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
ENDCG
SubShader
{
Tags { "RenderType"="Opaque" }
// 设置颜色1
Pass
{
Stencil
{
Ref 0
Comp Always
Pass IncrSat
ZFail IncrSat
}
CGPROGRAM
fixed4 frag (v2f i) : SV_Target
{
return fixed4(0, 0, 0, 1);
}
ENDCG
}
// 设置颜色2
Pass
{
Stencil
{
Ref 2
Comp Equal
Pass Keep
ZFail keep
}
CGPROGRAM
fixed4 frag (v2f i) : SV_Target
{
return fixed4(0.2, 0.2, 0.2, 1);
}
ENDCG
}
// 设置颜色3
Pass
{
Stencil
{
Ref 3
Comp Equal
Pass Keep
ZFail keep
}
CGPROGRAM
fixed4 frag (v2f i) : SV_Target
{
return fixed4(0.6, 0.6, 0.6, 1);
}
ENDCG
}
// 设置颜色3
Pass
{
Stencil
{
Ref 4
Comp Equal
Pass Keep
ZFail keep
}
CGPROGRAM
fixed4 frag (v2f i) : SV_Target
{
return fixed4(1, 1, 1, 1);
}
ENDCG
}
}
}
解释说明:第一个Pass用于修改模板缓冲的值,并填充在没有重叠情况下的颜色。之后几个Pass分别对应重叠两个、三个以及四个情况下的颜色。
3.反射区域控制
代码部分:
----需要反射的物体Shader----
Shader "Stencil/Reflection"
{
Properties
{
_MainTex ("基本贴图", 2D) = "white" {}
_PlanePos ("反射平面位置", vector) = (0, 0, 0, 1)
_PlaneNormal ("反射平面法线", vector) = (0, 1, 0, 1)
_MirrorRange ("镜面范围", Range(0, 5)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
CGINCLUDE
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 reflectParams : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
ENDCG
// 基础Pass
Pass
{
CGPROGRAM
v2f vert (appdata v)
{
v2f o = (v2f)0;
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);
return col;
}
ENDCG
}
// 反射Pass
Pass
{
Cull Front // 镜像颠倒,所以需要剔除正面,而不是背面
ZTest Always // 防止被地面挡住
Blend SrcAlpha OneMinusSrcAlpha //渲染的叠加方式
Stencil
{
Ref 1
Comp Equal
}
CGPROGRAM
fixed4 _PlanePos;
fixed4 _PlaneNormal;
fixed _MirrorRange;
v2f vert (appdata v)
{
v2f o = (v2f)0;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
float4 posWS = mul(unity_ObjectToWorld, v.vertex);
float3 nnDirWS = -_PlaneNormal.xyz;
float3 objectToPlane = posWS - _PlanePos;
float dist = dot(objectToPlane, _PlaneNormal);
float alpha = lerp(1.0, 0.0, dist / _MirrorRange);
o.reflectParams.x = dist; // 保存顶点到平面的垂直距离
o.reflectParams.y = alpha; // 保存镜子反射物体的Alpha值
posWS.xyz = posWS.xyz + nnDirWS * dist * 2;
o.vertex = UnityWorldToClipPos(posWS.xyz);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
if (i.reflectParams.x < 0 || i.reflectParams.y <= 0) discard; // 物体在镜子背面或者Alpha<0就剔除
fixed4 col = tex2D(_MainTex, i.uv);
fixed4 finalRGB = fixed4(col.rgb, i.reflectParams.y);
return finalRGB;
}
ENDCG
}
}
}
----作为镜面的物体Shader----
Shader "Stencil/Mirror"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
Stencil
{
Ref 0
Comp Always
Pass IncrSat
}
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;
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);
return col;
}
ENDCG
}
}
}
解释说明:反射部分用了两个Pass,相当于绘制了两次物体,一次正常绘制,另一个绘制镜面反射出来的部分。
根据上图所示的向量求出相对于镜面反向的P点。
4.阴影锥(Shadow Volume)
代码部分:
Shader "Stencil/ShadowVolume"
{
Properties
{
_ShadowCol ("阴影颜色", Color) = (0.3, 0.3, 0.3, 0.3)
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry+1" }
CGINCLUDE
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
fixed4 _ShadowCol;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return _ShadowCol; // 阴影颜色
}
ENDCG
Pass
{
ColorMask 0
Cull Front
Stencil
{
Ref 0
Comp Always
ZFail IncrWrap
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
Pass
{
ColorMask 0
Cull Back
Stencil
{
Ref 0
Comp Always
ZFail DecrWrap
}
}
Pass
{
Cull Back
Stencil
{
Ref 1
Comp Equal
Pass Keep
ZFail Keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
解释说明:
什么是Shadow Volume?
如上图所示,左边的绿色部分和右边的灰色部分即为阴影体。阴影体实际上就是Mesh,我们需要根据光源的位置和阴影的产生者(Shadow Caster)的形状去实时生成这个Mesh。
需要有三个Pass渲染阴影体。第一个Pass只渲染背面,如果深度测试失败,说明该片元在阴影体内部或是视角被遮挡了,模板值加1;第二个Pass只渲染正面,如果深度测试失败,说明该片元被其他片元遮挡了,模板值减1;第三个Pass判断模板值,模板值为1的片元则说明该片元只在阴影体内部,属于接收阴影的部分,需要上阴影色。