前言
B站专栏居然可以插入Gif代码和公式,有时间再慢慢修改之前的专栏吧,也顺便当作复习了。
深度测试做特效一定绕不开,模板测试能不用则不用,要么只在高端机上用。
布置任务:根据今天课程内容,使用深度测试与模板测试做一些有意思的效果

核心代码如下
Shader "Unlit/FieldShield"
{
Properties
{
_WarpTex ("WarpTex", 2D) = "white" {}
_Color ("ShieldColor", Color) = (1.0,1.0,1.0,1.0)
_WarpInt("DistortionInt", Range(0,50))=1.0
_WarpTimeSpeed("DisTimeSpeed",Range(0,10))=1.0
_InterInt("InterInt",Range(0,5))=1.0
_RimInt("RimInt",Range(0,5))=1.0
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Transparent"}
Pass
{
ZWrite Off
Cull Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
half3 normal: NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
half3 nDirWs : TEXCOORD1;
float4 worldPos: TEXCOORD2;
float4 scrPos: TEXCOORD3;
};
sampler2D _WarpTex;
float4 _WarpTex_ST;
fixed4 _Color;
float _WarpInt;
float _WarpTimeSpeed;
float _InterInt;
float _RimInt;
sampler2D _CameraDepthTexture;
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _WarpTex);
o.scrPos = ComputeScreenPos(o.pos);
COMPUTE_EYEDEPTH(o.scrPos.z);
o.nDirWs = UnityObjectToWorldNormal(v.normal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
half3 vDirWs = normalize(UnityWorldSpaceViewDir(i.worldPos));
half nDotV = abs(dot(vDirWs,i.nDirWs));
float depthTex = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.scrPos)));
float depthPos = i.scrPos.z;
float distance = depthTex - depthPos;
half interGlow = (1-distance)*_InterInt;
half rimGlow = (1-nDotV)*_RimInt;
half glow = max(interGlow, rimGlow);
float offset =frac(_Time.x*_WarpTimeSpeed);
fixed3 warpColor = tex2D(_WarpTex,i.uv-offset);
fixed3 finalColor = _Color*glow*warpColor;
return fixed4(finalColor,glow);
}
ENDCG
}
}
}
模板测试
什么是模板测试
首先我们需要知道屏幕上的像素会有模板缓存。模板测试则是在渲染物体时,通过判断物体该点的参考模板缓存和像素模板缓存之间的关系做进一步的处理。

上面的话有点抽象,我们进一步慢慢解析。
- 渲染管线出发
pixel Ownership Test 屏幕部分使用权限
Scissor Test 自定义渲染部分
Alpha Test 透明度测试,只能实现不透明和全透明效果
Stencil Test则是深度测试之前,透明度测试之后的测试。

- 从逻辑上理解
设定判断条件,如果通过判断,那么对片元执行一些操作,是保留是舍弃都可以自定义。

模板测试的语法
referenceValue是用来与模板缓冲区中的值进行比较的。
ReadMask 读取的时候将该值 maskValue 与 referenceValue 和 stencilBufferValue 分别进行按位与(&)操作
WriteMask 写入的时候将该值与 referenceValue 和 stencilBufferValue 分别进行按位与(&)操作
Comp是比较操作
Pass表示通过了之后的操作
Fail表示失败了之后的操作
ZFail表示深度测试失败了之后的操作

- ComparisionFuncion

具体对应的值可在Unity的接口枚举中查看,当所填值不在该范围内时采用默认值8即Always渲染时总是保留。
- StencilOperation

具体对应的值可在Unity的接口枚举中查看,当所填值不在范围内时采用默认值zero
模板测试项目详解
- 案例1

这里有3个要点:
1. 前面的圆环Shader的作用是改变圆环覆盖区域的像素的Stencil Buffer值,代码如下,Always表示总是通过测试,Replace表示将模板缓冲区中的值替换为_StencilRef。
Stencil
{
Ref [_StencilRef] //StencilRef的值自定义输入
Comp Always
Pass Replace
} 2. 圆环物体要关闭深度写入
3. 圆环后的物体需要进行如下的设置,Unity模板缓冲区中默认的模板缓冲区为0(即不配置的情况下Comp Always Pass keep),蒙版Shader的作用是把该部分替换为1,然后物体shader则是计算是否相等,如果相等,则显示,不相等就忽略。
Stencil
{
Ref [_StencilRef] //和上面的物体StencilBuffer一样
Comp [_StencilComp] //材质面板选择Equal
Pass [_StencilOp] //材质面板选择Keep
}
完整代码如下:
圆环的Shader
Shader "Unlit/TA3.1Stencil"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_StencilRef("StencilRef",float)=1.0
[Enum(UnityEngine.Rendering.CompareFunction)]_StencilComp("StencilComp",int)=0
[Enum(UnityEngine.Rendering.StencilOp)]_StencilOp("StencilOp",int)=0
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry-1" }
ZWrite Off
Stencil
{
Ref [_StencilRef]
Comp Always
Pass Replace
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
fixed3 color: COLOR;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
fixed3 color: TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (a2v v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.color = v.color;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 col = tex2D(_MainTex, i.uv);
float opacity = 1 - i.color.r;
return fixed4(col*opacity,opacity);
}
ENDCG
}
}
} 物体Shader
Shader "Unlit/TA3.1Stencil"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_StencilRef("StencilRef",float)=1.0
[Enum(UnityEngine.Rendering.CompareFunction)]_StencilComp("StencilComp",int)=0
[Enum(UnityEngine.Rendering.StencilOp)]_StencilOp("StencilOp",int)=0
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
Stencil
{
Ref [_StencilRef]
Comp [_StencilComp]
Pass [_StencilOp]
}
Pass
{
//Blend One OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
//fixed3 color: COLOR;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
//fixed3 color: TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (a2v v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
//o.color = v.color;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
- 案例2
项目2的实现原理和项目1大同小异,就是不同的圆环和不同的物体有不同的StencilBuffer值,一个圆环和一个物体的StencilBuffer值相等。

总结

内容扩展
模板测试还能做什么请看这篇https://blog.csdn.net/liu_if_else/article/details/86316361
参考资料
https://blog.csdn.net/u011047171/article/details/46928463
https://blog.csdn.net/liu_if_else/article/details/86316361
https://gameinstitute.qq.com/community/detail/127404
https://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/02%20Stencil%20testing/
https://www.patreon.com/posts/14832618
https://www.udemy.com/course/unity-shaders/
深度测试
深度测试是什么
深度测试是模板测试之后,透明度混合之前的操作,所谓深度测试,就是针对当前对象在屏幕上(更准确的说是frame buffer)对应的像素点,将对象自身的深度值与当前该像素点缓存的深度值进行比较,如果通过了,本对象在该像素点才会将颜色写入颜色缓冲区,否则否则不会写入颜色缓冲。
如果要通过深度测试,那么深度测试必须开启,而且自定义的条件必须满足才算深度测试通过。

深度缓冲区
Z-Buffer通常以16,24位存储Unity渲染区域上每一个像素的深度值,默认为无限大,我们可以调用ZWrite和ZTest实现深度测试的效果。
ZWrite和ZTest
ZWrite On/Off就是指目标物体的深度是否写入深度缓冲区。
ZTest则是目标物体的深度和深度缓冲区的深度值如何比较,它决定了颜色是否写入颜色缓冲区。
1.深度测试通过,深度写入开启:写入深度缓冲区,写入颜色缓冲区;
2.深度测试通过,深度写入关闭:不写深度缓冲区,写入颜色缓冲区;
3.深度测试失败,不写深度缓冲区,不写颜色缓冲区;
ZTestOp和前面模板的CompOp是一模一样的。
渲染队列
渲染队列顺序越小(默认2000),越先渲染,如果渲染队列顺序相同,那么则按照深度渲染。

不透明物体的渲染队列按照深度值:从小往大
透明物体的渲染队列按照深度值:从大到小
Early-Z
Early-Z技术的出现是因为深度测试的范围非常靠后了,基本上片元着色器中的上所有流程走完了才进行深度测试,如果没通过,那么前面的步骤全浪费了。所以,我们提前进行深度测试。

因此 现代GPU中运用了Early-Z的技术,在Vertex阶段和Fragment阶段之间(光栅化之后,fragment之前)进行一次深度测试,如果深度测试失败,就不必进行fragment阶段的计算了,因此在性能上会有很大的提升。但是最终的ZTest仍然需要进行,以保证最终的遮挡关系结果正确。前面的一次主要是Z-Cull为了裁剪以达到优化的目的,后一次主要是Z-Check,为了检查。
非线性深度值
深度值是线性的吗?不好回答。
只能说模型空间、世界空间和视角空间的深度值是线性的。但是一旦转入裁剪空间和屏幕空间,那么深度值就不是线性了。为什么?
https://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/01%20Depth%20testing/
在深度缓冲区中包含深度值介于和之间,从观察者看到其内容与场景中的所有对象的 z 值进行了比较。我们可以通过下面的线性变换将深度值转化到(0,1)范围,但是一般不这么做,为什么?

和Gamma矫正类似,距离我们进的物体需要更高精度的深度值来保存,如果存储精度不够,那么会出现深度冲突的现象,所以用非线性的方式来存储深度值。


深度冲突
深度冲突就是指物体靠的很近,在深度值精度不够的情况下,无法分辨谁在前,谁在后。
防止深度冲突可以将近平面设置的远一点,或者别让物体靠的太近,也可以消耗性能来提升深度缓冲区的精度。
案例1详解
- red: ZWrite On ZTest LessEqual gray: ZWrite On ZTest LessEqual white: ZWrite On ZTest LessEqual

- red: ZWrite Off ZTest LessEqual gray: ZWrite On ZTest LessEqual white: ZWrite On ZTest LessEqual
- red: ZWrite On ZTest LessEqual gray: ZWrite On ZTest Always white: ZWrite On ZTest LessEqual

- red: ZWrite On ZTest LessEqual gray: ZWrite On ZTest Always white: ZWrite On ZTest Always

- red: ZWrite On ZTest LessEqual gray: ZWrite On ZTest Always Queue=Geometry+1 white: ZWrite On ZTest Always

- red: ZWrite On ZTest LessEqual gray: ZWrite On ZTest GreatEqual Queue=Geometry+1 white: ZWrite On ZTest Always

核心代码如下
Properties
{
_MainTex ("Texture", 2D) = "green" {}
[Enum(Off,0,On,1)]_ZWriteMode("ZWriteMode",int)=1
[Enum(UnityEngine.Rendering.CompareFunction)]_ZTestOp("ZTestOp",int)=1.0
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry+1" }
LOD 100
ZWrite [_ZWriteMode]
ZTest [_ZTestOp]
Pass
}
案例2详解
红外线效果

制作思路如下:
完成这个Pass需要两个Shader,第一个Shader的作用是渲染红色轮廓,需要将ZTest设置为GEqual,关闭深度写入。第二个Shader则正常渲染。
案例3详解

核心思路和上面类似,不过是多了一个混合的效果,如果没有混合,那么物体的则是红黑相间的。核心代码如下:
Pass
{
Blend One OneMinusSrcAlpha
ZTest GEqual
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
half3 normal: NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
half3 vDirOs : TEXCOORD0;
half3 nDirOs : TEXCOORD1;
};
fixed4 _EdgeColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.nDirOs = v.normal;
o.vDirOs = normalize(ObjSpaceViewDir(v.vertex));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
half nDotV = max(0,dot(i.nDirOs,i.vDirOs));
half opacity = pow((1-nDotV),3);
fixed3 finalColor = _EdgeColor*opacity;
return fixed4(finalColor,opacity);
}
ENDCG
}
粒子系统案例
粒子系统需要透明的效果,所以默认的情况下关闭深度写入并开启混合模式就可以了,Blend One One。
这里有点没搞懂,v.uv.z是用来干嘛?
总结

深度测试扩展

参考资料
https://blog.csdn.net/puppet_master/article/details/53900568
https://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/01%20Depth%20testing/
https://docs.unity3d.com/cn/2018.4/Manual/SL-CullAndDepth.html
https://blog.csdn.net/yangxuan0261/article/details/79725466
《Unity ShaderLab 开发实战详解》
《Unity Shader 入门精要》
https://roystan.net/articles/toon-water.html