一个shader多个pass有不同渲染队列时,选最小的那个队列渲染物体,然后按照pass顺序依次进行
模板测试
能做什么?
- 传送门,透过传送门看到后面的场景(印象中传送门应该是使用两个门打开场景通道吧)
- Minions讲解的一些效果
- 展示柜,不同面看到不同场景(镜中窥梦)


是什么?
- 左图为颜色缓冲区中的一张图,在模板缓冲区中我们会给这张图的每一个片元分配一个0-255的数字(8位,默认为0)
- 中、右图可以看到,我们修改模板缓冲区部分0->1,通过自定义的一些准则,比如只显示模板值为1对应的片元的颜色,最后通过模板测试的结果就如右图所示
从渲染管线理解
- 片元着色器结束后,渲染管线执行逐片元操作,对片元进行一系列测试
- 逐片元流程
- PixelOwnershipTest(像素所有权测试):控制当前屏幕像素的使用权限,显示器上只有游戏窗口部分使用(全屏、窗口)
- ScissorTest(裁剪测试):在渲染空间中选定只渲染某一区域,比如可以只渲染窗口的右上角(Unity相机好像有个选项,不知道是不是,有时候用来小地图)
- AlphaTest(透明度测试):设置透明度阈值,像素透明度与该阈值进行测试,不通过将会被舍弃
- StencilTest(模板测试)
- DepthTest(深度测试)
- Blending(透明度混合):实现半透明效果必须
- 完成接下来的其他一系列操作后,我们会将通过测试的片元/像素输出到帧缓冲区(FrameBuffer)
从逻辑上理解模板测试
1 | if(referenceValue & readMask comparisonFuction stencilBufferValue & readMask |
- referenceValue:当前模板缓冲片元的参考值
- stencilBufferValue:模板缓冲区里的值
- comparisonFunction,比较操作
- 通过一定条件来判断这个片元/片元属性执行保留还是抛弃的操作
从书面概念上理解
-
模板缓冲区-FrameBuffer
- 模板缓冲区为屏幕上的每一个像素点保存一个无符号整数值(通常为8位int 0-255)
- 这个值的意义根据程序的具体应用而定
-
模板测试
- 渲染过程中,可以用这个值与预先设定好的参考值作(ReferenceValue)比较,根据结果来决定是否更新相应的像素点的颜色值
- 这个比较的过程就称为模板测试
- 模板测试在透明度测试之后,深度测试之前
- 如果模板测试通过,相应的像素点(颜色缓冲区)更新,否则不更新
怎么用?
Unity Shader中的语法结构
1 | stencil { |
ComparisonFunction
StencilOperation
一些效果展示&讲解
3D卡牌效果
- Minions Art: https://www.patreon.com/posts/14832618
- 未使用模板测试时,场景中的摆放
- 遮罩Shader
1 | Shader "FX/StencilMask" { |
- 物体Shader
1 | Shader "Toon/Lit StencilMask" { |
- 思路理解
- Unity模板缓冲区默认值为0
- 注意渲染顺序,一般是先让mask将模版缓冲更新,设置部分区域才能通过测试
- 默认物体(mask前边,不参与测试的物体)的索引值设为0,Comp设为Always
- mask的的索引值设为1,Comp设为always,但测试通过后将缓冲区替换成索引值,即mask所在的模板缓冲区的值变成了设定的索引值
- 此时模板覆盖的区域缓冲区值被修改为1,其他保持默认0
- 参与模版测试,模版后面的物体的索引值设为1,Comp设为Equal,故只有在模板覆盖的区域才能通过测试
- 于是便实现了卡牌效果,注意背景层需要给很大,才能兼顾不同视角,避免穿帮
盒子展示柜
- 遮罩shader
1 | Shader "Custom/Stencil Window" { |
- 物体Shader
1 | Shader "Custom/Diffuse Stencil" { |
- 思路理解
- 盒子里放满了所有要展示的物品/场景
- 盒子每个面都是一个模板,写入能通过测试的模版值
- 盒子里的物品只有对应模板值才能通过测试
总结
-
前期准备:
- 当前模板缓冲区值(StencilBufferValue)、模板参考值(ReferenceValue)
-
模板测试过程:
- 主要就是对这两个值进行特定的比较操作,例如Never、Always、Equal等,具体参考上文的表格
-
模板测试进行后:
- 要对模板缓冲区的值进行更新操作,例如Keep,Replace等,具体参考上文表格
- 更新操作:可以根据不同的结果对模板缓冲区做不同的更新操作,例如模板测试成功的操作Pass、模板测试失败的操作Fail、深度测试失败的操作ZFail、还有正对正面和背面更新操作Passback,Passfront,Failback等…
-
模板测试不常用,建议避免使用,有一定开销(缓冲区开辟,可好像比深度缓冲小,虽然深度是必须的),只在高端机使用
扩展&参考资料
- 官方文档:https://docs.unity3d.com/Manual/SL-Stencil.html
- 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/
深度测试
是什么?
从渲染管线理解
深度测试同样位于逐片元操作过程中,在模板测试之后,透明度混合之前。
从逻辑上理解深度测试
1 | if(ZWrite On && (currentDepthValue ComparisonFunction DepthBufferValue)){ |
1 | if(currentDepthValue ComparsionFunction DepthBufferValue){ |
和模板测试类似,通过每一帧全局的缓冲值与当前值比较,来进行一系列操作
从书面概念上理解
深度测试 :针对当前屏幕上(更准确的说是FrameBuffer)对应的像素点,将对象自身的深度值与当前深度缓冲区的深度值做比较,如果通过了,这个对象在该像素点才会将颜色写入颜色缓冲区,否则不会写入颜色缓冲区。
从发展上理解深度测试
- 控制渲染顺序
- 画家算法:
- 这里是指油画的画法,也就是画一幅油画,是从远处开始画,然后近处的东西一点点叠加在上面
- 存在的问题:例如一列物体,最前面的物体最大,站在正前面看只能看到最前面的物体,这样一来后边的就不用画了,不然就是性能浪费(OverDraw)。
- Z-Buffer算法:
- 通过深度缓冲区来控制渲染顺序
- 画家算法:
- 控制Z-Buffer对深度的存储
- 什么时候更新深度缓冲区、什么时候使用深度缓冲区
- Z Test、Z Write
- 控制不同类型物体的渲染顺序
- 透明物体
- 不透明物体
- 渲染队列
- 减少OverDraw
- Early-Z
- Z-cull(优化手段)
- Z-check(确认正确遮挡关系)
- Early-Z
怎么用?
Z-Buffer(深度缓冲区)
- 类似于颜色缓冲区,但存储的是距离摄像机最近的深度,通常和颜色缓冲有着一样的宽度和高度
- 颜色缓冲区:
- 最终在显示屏硬件上显示颜色的GPU显存区域,这个缓冲区储存了每帧更新后的最终颜色值,图形流水线经过一系列测试,包括片段丢弃、颜色混合等,最终生成的像素颜色值就储存在这里,然后提交给显示硬件显示
- 深度缓冲是由窗口系统自动创建的,它会以16、24、32位float形式存储深度值(大部分系统中深度值是24位的)
- Z-Buffer中存储的是当前的深度信息,对于每个像素存储一个深度值
- 可以通过Z-Write 、Z-Test来调用Z-Buffer,从而实现效果
Z Write(深度写入)
-
深度写入包括两种状态:ZWrite On 与 ZWrite Off
-
开启深度写入,物体被渲染时针对物体在屏幕(更准确地说是frame buffer)上每个像素的深度都写入到深度缓冲区
-
关闭深度写入,物体的深度就不会写入深度缓冲区
-
物体是否会写入深度,除了ZWrite这个状态之外,更重要的是需要深度测试通过,也就是ZTest通过,如果ZTest都没通过,那么也就不会写入深度了
-
ZTest分为通过和不通过两种情况,ZWrite分为开启和关闭两种情况的四种情况:
- 深度测试通过,深度写入开启:写入深度缓冲区,写入颜色缓冲区
- 深度测试通过,深度写入关闭:不写深度缓冲区,写入颜色缓冲区
- 深度测试失败,深度写入开启:不写深度缓冲区,不写颜色缓冲区
- 深度测试失败,深度写入关闭:不写深度缓冲区,不屑颜色缓冲区
Z Test比较操作
默认是ZWrite On 和 ZTest LEqual,深度缓冲区的值一开始是无限大的
Unity的渲染队列
- Unity内置的几种渲染队列,队列数越小,越先渲染
- Unity中设置渲染队列
- 默认是Geometry,通过语法Tags { “Queue” = “渲染队列名”}修改
- shader属性面板也能修改
- Unity中不透明物体的渲染顺序:从前往后(深度小的先渲染)
- Unity中透明物体的渲染顺序:从后往前(类似画家算法,会造成OverDraw,但由于需要透明度混合,没法避免)
简述Early-Z技术
- Early-Z是位于三角形遍历之后、逐片元操作之前的
- 传统的渲染管线中,ZTest是在Blending阶段,所有对象的像素着色器都已经计算过一遍了,这时进行深度测试的话,只是为了得到正确的效果,造成了大量的无用计算(丢弃的片元本来可以不用计算的)。
- 为了减少这些不必要的计算,现代GPU运用了Early-Z技术,在顶点和片元阶段之间(光栅化之后,片元着色器之前)进行一次深度测试(如下左图黑框部分)。
- 如果这次深度测试失败,那就不用在片元着色器中作无关紧要的计算了,这样一来就会带来性能提升
- 最终的ZTest仍然要进行,以保证正确的遮挡关系
- 如右图前一次的Z-Cull是为了裁剪达到性能优化的目的,后一次的Z-check是为了保证正确的遮挡关系
- 印象中Tiled Base渲染不能使用,待查证
深度值
- 模型在渲染管线中的几次空间变换
- 模型一开始所在的模型空间:没有深度
- 通过M矩阵变换到世界空间,此时模型坐标已经变换到了齐次坐标(x,y,z,w):深度存在z分量
- 通过V矩阵变换到观察空间(摄像机空间):深度存在z分量(线性)
- 通过P矩阵变换到裁剪空间:深度缓冲中此空间的z/w中(已经变成了非线性的深度)
- 最后通过一些投影映射变换到屏幕空间
- 为什么深度缓冲区中要存储一个非线性的深度?
-
参考链接: https://learnopengl-cn.readthedocs.io/zh/latest/04 Advanced OpenGL/01 Depth testing/
-
原因1:和伽马校正类似,人们不太关心远处的深度变化,需要给近处的物体更精细的深度
- 在深度缓冲区中的深度值是介于 0.0~1.0之间的,从观察者看到的内容与场景中所有对象的z值作比较
- 这些z值可以投影平截头体(就是视锥)的近平面和远平面之间的任何值
- 正确的投影特性的非线性深度方程是和1/z成正比的,所以在Z很近的时候有高精度,Z很远的时候低精度
-
原因2:Z-Fight 深度冲突
- 当两个平面或三角形紧密相互平行的时候,深度缓冲区不具有足够的精度来确定哪一个在前面
- 结果就是这两个形状不断切换顺序,导致怪异问题,看起来像是两个形状在争夺靠前的位置
- 深度冲突是深度缓冲区的普遍问题,当对象的距离越远一般越强(因为深度缓冲区在z值非常大的时候没有很高的精度)
- 深度冲突无法避免,但是有技巧可以防止出现:
- 物理上的做法,就是让物体不要靠得太近
- 尽可能的把近平面设置的远一点
- 放弃一些性能来得到更高精度的深度值
-
一些效果展示&讲解
三个正方体遮挡关系
如果使用URP,需要关闭SRP合批
挺好理解的,这里就不过多解释,忘记时翻一下原文视频
1 | Shader "Custom/ZTest" |
X-Ray效果
1 | Shader "Custom/XRay" |
- X-Ray效果部分我们使用到了ZTest :Greater,深度写入关闭(不关闭会影响其他物体渲染)
- 高出墙体部分是默认的渲染:LessEqual、ZWrite On
- Cull back 是剔除背面,为了优化
- Blend SrcAlpha One :需要混合墙壁和xray效果
粒子系统中的深度测试
-
默认例子效果是不透明的
-
关键修改
1
2
3
4
5
6
7Tags{
"RenderType" = "Opaque"
"Queue" = "Transparent"
}
ZWrite Off
ZTest LEqual
Blend One One
深度测试的总结
- 最重要的两个值:当前深度缓冲区的值(ZBufferValue)和 深度参考值(ReferenceValue),进行比较测试
- Unity中的渲染顺序:
- 先渲染不透明物体(从前到后),再渲染透明物体(从后往前)
- Unity中的默认条件:
- ZWrite:On
- Ztest:LessEqual
- 渲染队列:Geometry(2000)
- 通过对ZWrite和ZTest的相互组合配置来控制半透明物体的渲染(关闭深度写入,快开启深度测试,透明度混合)
- 引入Early-Z之后深度测试相关的内容(Z-Cull、Z-Check)
-深度缓冲区中存储的深度值为[0,1]的非线性值
扩展&参考资料
- https://blog.csdn.net/puppet_master/article/details/53900568
- https://learnopengl-cn.readthedocs.io/zh/latest/04 Advanced OpenGL/01 Depth testing/
- https://docs.unity3d.com/cn/2018.4/Manual/SL-CullAndDepth.html
- https://blog.csdn.net/yangxuan0261/article/details/79725466
- https://roystan.net/articles/toon-water.html
- 《shader入门精要》
- 《Unity ShaderLab 开发实战详解》
大佬笔记
- https://www.yuque.com/zohar-jpfbj/tiacmz/nw0g0g?
- https://www.yuque.com/sugelameiyoudi-jadcc/okgm7e/nqoaio#NdG1l
- https://blog.csdn.net/wrl780143706/article/details/119730998
- https://blog.csdn.net/qq_43210334/article/details/118179532
- https://blog.csdn.net/whitebreeze/article/details/118097525
- https://www.yuque.com/wuyan-popj0/qo443s/un9pbe#QhGlg
- https://xzyw7.github.io/post/J5gfuU4WE/
- https://www.yuque.com/zhuying-7uqy4/kipb65/thba9q#WcSdV
作业
根据课程内容,用深度测试和模板测试做些有意思的效果
模板测试–解谜游戏探索镜效果
- 在PBR Shader增加模板测试
- 保险箱模板值为0,操作为Equal
- 箱子里的物体模板值为1,操作为Equal(别问我为什么是鲨鱼
- 探索镜面范围写入模板值1
- 注意渲染顺序,镜子先写入模板值
- Timeline简单动画制作(节奏有点怪,运动是线性等缺点。。
深度测试–在Unity中手写一个卡通水
参考文章:苏格拉没有底:https://zhuanlan.zhihu.com/p/425605759
1 | // 核心代码 |
- 很喜欢这个风格,虽然知道很多人会做这个
- 水面不加入深度写入,所以通过深度图获取被水体遮挡的深度值(原因在于深度图是渲染顺序小于2500才会被记录)
- 水面高度:计算水体屏幕深度和深度图的差值
- urp计算屏幕坐标,只能手动计算xy/w。而屏幕深度就是w分量
- 通过计算当前深度和最高深度的比例来lerp水面分层的颜色
- 泡沫部分引入噪声图,通过时间增量采样来获取动画效果
- 岸边需要更多泡沫,利用深度浅泡沫越多的特点
- 目前还需要漂浮物与水面接触时更多的泡沫,所以需要引入深度法线图
- 通过点乘两个法向量,值越大,泡沐越多
- URP获取深度与Builtin不同,https://codeantenna.com/a/ZivlGXdX96