一、开始前,先看一下举例来理解
1.引例
● 左图为颜色缓冲区中的一张图,在模板缓冲区中我们会给这张图的每一个片元分配一个0-255的数字(8位,默认为0)
● 中、右图可以看到,我们修改了一些0为1,通过自定义的一些准则,如输出模板缓冲区中1对应的片元的颜色;0的不输出,最后通过模板测试的结果就如右图所示
2.通过模板测试的应用,实现的效果举例
● ①传送门效果:可以看到左边传送门内的景象正是右侧的场景
②Minions讲解的一些效果,例如3D卡牌效果、侦探镜效果等
3 每个正方体面显示不同场景(每个面作为蒙版来显示场景)
https://www.patreon.com/posts/14832618
3.理解
● 对于上述的例子总结一下,这些效果基本可以归结为三层组成
○ 以②中的传送门为例子,三层分别对应:门外场景、门内场景、门
● 也就是说可以理解为:包括两层物体/场景、和一层遮罩
二、什么是模板测试
1.从渲染管线理解
● 下图为从片元着色器到FrameBuffer的流程(逐片元操作)
逐片元操作流程:
● 可以看到逐片元的流程依次为
○ 像素所有权测试→裁剪测试→透明度测试→模板测试→深度测试→透明度混合
○ PixelOwnershipTest:
■ 简单来说就是控制当前屏幕像素的使用权限
■ e.g.:游戏引擎仅渲染游戏窗口
○ ScissorTest(裁剪测试):
■ 在渲染窗口再定义要渲染哪一部分
■ 和裁剪空间一起理解,也就是只渲染能看到的部分
■ e.g.只渲染窗口的左下角部分
○ AlphaTest(透明度测试)
■ 提前设置一个透明度预值
■ 只能实现不透明效果和全透明效果
■ e.g. 设置透明度a为0.5,如果片元大于这个值就通过测试,如果小于0.5就剔除掉
○ StencilTest(模板测试)
○ DepthTest(深度测试)
○ Blending(透明度混合)
■ 可以实现半透明效果
○ 完成接下来的其他一系列操作后,我们会将合格的片元/像素输出到帧缓冲区(FrameBuffer)
● 逐片元操作是可以配置但不可编程的(对应图中为黄色背景),也就是说是由管线/硬件自身规定好的,我们只能对里边的内容进行配置。
这是渲染流水线的最后一步。逐片元操作(Per-Fragment Operation)是OpenGL中的说法,在DirectX中,这一阶段被称为输出合并阶段(Output-Merger)。Merger这个词可能更容易让读者明白这一步骤的目的:合并。而OpenGL中的名字可以让读者明白这个阶段的操作单位,即对每个片元进行一些操作。但要合并哪些数据,又要进行哪些操作呢
1.决定每个片元的可见性。这涉及了很多测试工作,比如深度测试、模板测试等。
2.如果一个片元通过了所有测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并或者说是混合。
A 测试:
裁剪测试(Scissor Test)
透明度测试(Alpha Test)
模板测试(Stencil Test)
深度测试(Depth Test)
B 合并:透明度混合和不透明度混合
step1. 没有经过测试的片段会被丢弃,不需要进行混合阶段;
step2. 经过测试的片段会进入混合阶段,可以将其转变为像素了(需要注意的是此时像素已经包含了深度信息,之前这部分信息被保存在深度缓冲里),需要把这个片元的颜色值和已经储存的颜色缓冲中的颜色进行合并,或者说混合。
step3. 一切完成之后,片元就会被送往颜色缓冲区。
2.从逻辑上理解
○ 理解:
■ referenceValue:当前模板缓冲片元的参考值
■ stencilBufferValue:模板缓冲区里的值
■ 中间的comparisonFunction,就是做一个比较
○ 结果:
■ 如果通过,这个片元就进入下一个阶段
■ 未通过/抛弃,停止并且不会进入下一个阶段,也就是说不会进入颜色缓冲区
○ 总结:就是通过一定条件来判断这个片元/片元属性执行保留还是抛弃的操作
3.从书面概念上理解
模板缓冲区-FrameBuffer
● 模板缓冲区可以为屏幕上的每一个像素点保存一个无符号整数值(通常为8位int 0-255)。
● 这个值的意义根据程序的具体应用而定。
模板测试
● 渲染过程中,可以用这个值与预先设定好的参考值作(ReferenceValue)比较,根据结果来决定是否更新相应的像素点的颜色值。
● 这个比较的过程就称为模板测试。
● 模板测试在透明度测试之后,深度测试之前。
● 如果模板测试通过,相应的像素点更新,否则不更新。
三、基本原理和使用方法
1.语法表示/结构解释
■ Ref:当前片元的参考值(0-255)
■ ReadMask:读掩码
■ WriteMask:写掩码
■ Comp:比较操作函数
■ Pass:测试通过,之后进行操作(StencilOperation,后边有详细讲解)
■ Fail:测试未通过,也会进行一个操作
■ ZFail:模板测试通过,深度测试未通过
2.ComparisonFunction
● 我们可以根据需求配置
3.StencilOperation 更新值
● 有不同的更新操作,根据自己的需求进行配置
3.StencilOperation 更新值
● 有不同的更新操作,根据自己的需求进行配置
四、Demo效果展示和讲解
1、案例一:3D卡牌效果
● 注:Unity中模板缓冲区默认都是0
● 在材质中将ReferenceValue改为0时的效果(模型直接摆放的效果)
蒙版的shader
代码理解
● 只有一个int类型的属性,作为蒙版的ID
● 渲染类型为不透明物体,队列为Geometry+1(默认的不透明物体后进行蒙版的渲染)
● ColorMask 颜色遮罩,0就是什么都不输出(是高度可配置的,可以改为RGBA(没有遮罩)、输出单通道R、G、 B)
● ZWrite off 关闭深度写入,防止显示的东西被深度剔除(后边深度测试细讲)
● Stencil/模板测试部分
○ Ref [_ID] 索引值就是前边属性声明的ID
○ Comp always //默认比较
○ Pass replace //默认是keep
○ Fail、ZFail,不写的话都是默认值,如代码所示
● 后续就是顶点、片元着色器
● 颜色给一个half4的就行,因为前边已经ColorMask0了(什么都不输出)
物体的shader
代码理解:
● 除了自身的属性之外,同样给了一个ID
● 渲染类型为不透明物体,队列为Geometry+2(前边蒙版后渲染)
● Stencil/模板测试部分:
○ Ref [_ID] 同上
○ Comp equal ,当给定的索引值和当前模板缓冲区的值相等时,才会渲染这个片元。
○ 后续不写就是默认值
● 后边就是自身的光照模型、颜色渲染等
③总结实现思路
● 前提认识:
○ Unity的模板缓冲区的默认值是0
● 默认物体(mask外边的物体)的索引值也设为0,Comp设为always
○ 也就是比较的结果是一直通过的,且保持保持模板缓冲区的值不变(不进行模板测试操作的物体渲染完之后,模板缓冲区的值还是0)
● 然后开始渲染蒙版(mask)
○ mask的设置Comp也是always,但是不同的是:ID给了1(属性部分定义),并且Pass的设置为replace,也就是说mask所在的模板缓冲区的值变成了1。
● 到这里,总结一下,mask外的物体 值是0,mask的值是1
● 最后是mask里边的物体。
○ mask的模板测试是这样的:ID是1,Comp是equal。翻译一下就是:模板缓冲区的值为1,比较的条件是相等
○ 此时,里边物体的模板缓冲区的值是1,外边物体的模板缓冲区是0,Comp的条件是相等。结果很明显不相等,这样的效果就是:除mask显示的部分,外边的场景不渲染。
○ 回顾前边,我们mask的缓冲区的值也为1,通过了测试,所以mask部分(卡牌部分)渲染了出来。
2.案例二:盒子不同面显示不同场景
实现思路
● 和卡牌效果类似,一个用蒙版遮罩的物体,盒子每个面使用一个蒙版遮罩
● 同样利用默认的值为0来做,只是面多了,蒙版和里边显示的物体也多了,ID依次为1、2、3、4
● 总结:一个蒙版对应一个物体,他们使用相同的ID,出来的效果就是:每个面显示的盒子内部物理不同
代码理解:
● 属性中使用了一个内置的枚举,这样就可以在外边自己选择可配置的属性了
五、模板测试的总结
● 最重要(用来比较的)两个值:
○ 当前模板缓冲区值(StencilBufferValue)、模板参考值(ReferenceValue)
● 模板测试的内容:
○ 主要就是对这两个值进行特定的比较操作,例如Never、Always、Equal等,具体参考上文的表格
● 模板测试后
○ 要对模板缓冲区的值进行更新操作,例如Keep,Replace等,具体参考上文表格
○ 更新操作:可以根据不同的结果对模板缓冲区做不同的更新操作,例如模板测试成功的操作Pass、模板测试失败的操作Fail、深度测试失败的操作ZFail、还有正对正面和背面更新操作Passback,Passfront,Failback等…
深度测试
一、什么是深度测试
● 帮助我们处理物体的遮挡关系
1.从渲染管线理解
深度测试同样位于逐片元操作过程中,在模板测试之后,透明度混合之前。
2.从逻辑上理解
● 理解:
○ 和模板测试差不多,都是通过一个比较来判断一系列操作
○ 图1:
■ 如果ZWrite On,且当前深度值和深度缓冲区的值作比较,如果通过就写入深度,不通过就忽略深度
○ 图2:
■ 当前深度值和深度缓冲区中的值做比较,如果通过就写入颜色缓冲区,不通过就不写入颜色缓冲区
3.从书面概念上理解
● 深度测试的概念
○ 就是针对当前屏幕上(更准确的说是FrameBuffer)对应的像素点,将对象自身的深度值与当前深度缓冲区的深度值做比较,如果通过了,这个对象在该像素点才会将颜色写入颜色缓冲区。
4.从发展上看
● 我们要渲染一个场景的话,通常会有多个物体。
● 首先要控制渲染顺序
○ 画家算法:
■ 这里是指油画的画法,也就是画一幅油画,是从远处开始画,然后近处的东西一点点叠加在上面(GAMES系列的课提到过多次)
■ 存在的问题:例如一列物体,最前面的物体最大,站在正前面看只能看到最前面的物体,这样一来后边的就不用画了,不然就是性能浪费(OverDraw)。
○ Z-Buffer算法:
■ 通过深度缓冲区来控制渲染顺序
● 控制Z-Buffer对深度的存储
○ 例如:什么时候更新深度缓冲区、什么时候使用深度缓冲区
○ 两个典型的功能:
■ Z Test
■ Z Write
○ 具体后边会讲
● 控制不同类型物体的渲染顺序
○ 透明物体
○ 不透明物体
○ 渲染队列(很有用的概念,后边会讲)
● 减少OverDraw
○ Early-Z,一种优化手段,后边会讲
■ Z-cull(优化手段)
■ Z-check(确认正确遮挡关系)
二、基本原理和使用方法
1.Z-Buffer(深度缓冲区)
● 和颜色缓冲区一样,在每个片段中存储了信息,并且通常和颜色缓冲有着一样的宽度和高度
● 颜色缓冲区:
○ 就是最终在显示屏硬件上显示颜色的GPU显存区域了,这个缓冲区储存了每帧更新后的最终颜色值,图形流水线经过一系列测试,包括片段丢弃、颜色混合等,最终生成的像素颜色值就储存在这里,然后提交给显示硬件显示。
● 深度缓冲是由窗口系统自动创建的,它会以16、24、32位float形式存储深度值。
○ //大部分系统中深度值是24位的
● Z-Buffer中存储的是当前的深度信息,对于每个像素存储一个深度值。
● 我们可以通过Z-Write 、Z-Test来调用Z-Buffer,来达到想要的渲染效果
2.Z Writer(深度写入)
● 深度写入包括两种状态
○ ZWrite On 、 ZWrite Off
○ 当我们开启深度写入,物体被渲染时针对物体在屏幕(FrameBuffer)上每个像素的深度都写入到深度缓冲区。
○ 关闭深度写入状态,物体的深度就不会写入深度缓冲区。
● 除了ZWrite的是否写入深度缓冲区,更重要的是:是否通过深度测试,也就是Z-Test
○ 如果Z-Test都没通过,也就不会写入深度了。
○ 也就是说,只有ZTest和ZWrite都可行的情况下才写入深度缓冲区
● 综上,ZWrite有On、Off两种情况;ZTest有通过、不通过两种情况,两者结合的四种情况如下:
3.Z-Test的比较操作
● 默认情况下:
○ Z Write:On
○ Z Test:LEqual
● 深度缓冲一开始为无穷大
4.Unity的渲染队列
● Unity内置的几种渲染队列:
○ 按照渲染顺序从先到后排序,队列数越小,越先渲染;反之同理。
● Unity中设置渲染队列:
○ 语法:Tags { “Queue” = “渲染队列名”}
○ 默认是Geometry
● Unity中不透明物体的渲染顺序:从前往后
○ 也就是说深度小的先渲染,其次再渲染深度大的
● Unity中透明物体的渲染顺序:从后往前(类似画家算法,会造成OverDraw)
● 可以在shader的Inspector面板中查看渲染队列相关属性
5.简述Early-Z技术
● Early-Z是位于三角形遍历之后、逐片元操作之前的。
● 传统的渲染管线中,ZTest是在Blending阶段,这时进行深度测试的话,所以对象的像素着色器都会计算一遍,没有性能提升,只是为了得到正确的效果,造成了大量的无用计算。(深度测试失败的片元是已经经过计算的片元,也就是说:到在一步测试不通过而被抛弃,前边的计算就是无用功了)
● 为了减少这些不必要的计算,现代GPU运用了Early-Z技术,在顶点和片元阶段之间(光栅化之后,片元着色器之前)进行一次深度测试(如下左图黑框部分)。
○ 如果这次深度测试失败,那就不用在片元着色器中作无关紧要的计算了,这样一来就会带来性能提升。
○ 最终的ZTest仍然要进行,以保证正确的遮挡关系。
○ 如右图前一次的Z-Cull是为了裁剪达到性能优化的目的,后一次的Z-check是为了保证正确的遮挡关系。
6.深度值
正确的理解深度值的概念
● 首先先了解一下模型在渲染管线中的几次空间变换
○ 模型一开始所在的模型空间:无深度。
○ 通过M矩阵变换到世界空间,此时模型坐标已经变换到了齐次坐标(x,y,z,w):深度存在z分量。
○ 通过V矩阵变换到观察空间(摄像机空间):深度存在z分量(线性)
○ 通过P矩阵变换到裁剪空间:深度缓冲中此空间的z/w中(已经变成了非线性的深度)
○ 最后通过一些投影映射变换到屏幕空间
7 为什么深度缓冲区中要存储一个非线性的深度?
原因1:给近处更多的精度
● 在深度缓冲区中的深度值是介于 0.0~1.0之间的,从观察者看到的内容与场景中所有对象的z值作比较。
● 这些z值可以投影平截头体(就是视锥)的近平面和远平面之间的任何值。
● 然而实践中几乎不使用线性深度缓冲区,正确的投影特性的非线性深度方程是和1/z成正比的。这样一来会有如下效果:在Z很近的时候有高精度,Z很远的时候低精度,这符合我们生活中的情况,具体如下图
○ 其实可以回想前几节课中伽马校正部分对比理解一下,也是根据实际情况(人眼特性),给暗部更多的精度,这里是近处给更多精度。
原因2:Z-Fight-深度冲突
○ 当两个平面或三角形紧密相互平行的时候,深度缓冲区不具有足够的精度来确定哪一个考前。
○ 结果就是这两个形状不断切换顺序,导致怪异问题,看起来像是两个形状在争夺靠前的位置。
○ 这就被称为深度冲突。
○ 深度冲突是深度缓冲区的普遍问题,当对象的距离越远一般越强(因为深度缓冲区在z值非常大的时候没有很高的精度)
○ 深度冲突无法避免,但是有技巧可以防止出现:
■ 物理上的做法,就是让物体不要靠得太近。
■ 尽可能的把近平面设置的远一点。
■ 放弃一些性能来得到更高精度的深度值。
三、Demo效果展示和讲解
案例一
图1详解:正常渲染顺序
● 梳理渲染过程:
○ 没渲染时,此时Unity的深度缓冲区默认值为无穷大
○ 渲染蓝色正方体
■
■ 相对于默认深度缓冲区的无穷大,肯定是小于等于,所以测试通过
○ 渲染绿色正方体
■ 此时蓝色物体位置的深度缓冲区的值已经不是无穷大了,其它位置还是
■ 注:深度缓冲区和颜色缓冲区都是相对于片元来讲的(片元可以理解为未完成的一个像素,还处于渲染管线中的像素)
■ 绿色正方体进行深度测试,深度测试同样是LessEqual,并且绿色的深度值比蓝色正方体的大。
■ 结果就是:两个正方体重叠部分是大于深度缓冲区的,也就是测试不通过,所以重叠部分没有写入绿色,还是蓝色的
■ 没有重叠部分,深度当然比无穷大小,所以写入, 渲染出来了绿色正方体未重叠的部分。
○ 红色同理。
图2详解:关闭前排正方体的深度写入
●
● 梳理渲染过程:
○ 设置:将蓝色正方体的深度写入ZWrite 关掉了;
○ 思路:第一个蓝色正方体的渲染时,测试通过,但是并没有写入深度。
○ 也就是说,渲染完蓝色正方体时,深度缓冲区的值还是无穷大。
○ 这就是蓝绿重叠部分,显示绿色的原因。
图3详解:
● 相较于图2,只是把绿色正方体的ZTest改为了always
● 无论是LessEqual还是always,测试都通过,所以效果和图2一样
图4详解:改变ZTest条件
● 将红色正方体的ZTest也改为了always,这样一来红色正方体的深度测试也是一直通过,并且写入。
● 因为是从前往后渲染的,所有依次为蓝、绿、红,深度缓冲区中的值也是后边渲染的
● 可以理解为后边遮住前边的效果。
图5详解:改变渲染队列
●
● 相对于图4,改变了绿色正方体的渲染队列为Geometry+1
● 此时的帧缓冲区面板如下
○ 尽管场景中绿色正方体在红色正方体前面,但是因为队列+1,它的渲染顺序变为了红色正方体后
● 也就是说,渲染队列优先级 > 透明物体的渲染顺序(从前到后)
图6详解:再次理解ZTest条件
●
● 相对于图1,将绿色正方体的ZTest改为了Greater,
● 也就是说蓝色正方体和绿色正方体重叠部分,大于模板缓冲区的部分通过测试,写入模板缓冲区
● 结果就是重叠部分为绿色,而未重叠部分的深度当然小于无穷大,所以没通过测试,自然也就不渲染。
● 红色部分正常。
shader截图
2.案例二:X-Ray效果
实现思路:
○ 分为三部分:前边的墙、被墙挡住的X-Ray效果部分、高出墙部分的物体
○ 回想一下前边6张图,哪张图是前边渲染完,后边渲染显示在先渲染完前边的? —>图6
○ 也就是说,X-Ray效果部分我们使用到了ZTest :Greater,深度写入关闭
○ 高出墙体部分是默认的渲染:LessEqual、ZWrite On
○ 代码理解
■ 写CGINCLUDE的好处:将顶点和片元着色器写在里边,在多passshade的时候,直接调用就可以了。(跟C++头文件类似)
■ X-Ray绘制部分
● 和之前实现思路相同,ZWrite Off,ZTest Greater
● Cull back 是剔除背面,为了优化
● Blend SrcAlpha One :由于有一个透明的效果,除了上边的,还需要一步Blend,来做透明度混合
● 渲染类型和渲染队列为Transparent
■ 正常绘制部分略
案例三、粒子系统中的深度测试
● 创建一个粒子系统ParticleSystem,可以看到默认的是透明的
● 为了加深理解,我们自己来复刻一下这个粒子系统的效果
○ 我们自己创建一个材质,给到粒子上
○ 此时粒子系统变成了这样
七、深度测试的总结
● 最重要的两个值:当前深度缓冲区的值(ZBufferValue) 和 深度参考值(ReferenceValue)。通过比较操作还实现理想的渲染效果
● Unity中的渲染顺序:
○ 先渲染不透明物体(从前到后),再渲染透明物体(从后往前)
● Unity中的默认条件:
○ ZWrite:On
○ Ztest:LessEqual
○ 渲染队列:Geometry(2000)
● 通过对ZWrite和ZTest的相互组合配置来控制半透明物体的渲染(关闭深度写入,快开启深度测试,透明度混合)
● 引入Early-Z之后深度测试相关的内容(Z-Cull、Z-Check)
● 深度缓冲区中存储的深度值为[0,1]的非线性值