主要讲解模板测试也就是RTR中所讲的Stencil buffer技术了。
模板测试,说道模板测试就要说道模板缓冲区。模板缓冲区与颜色缓冲区和深度缓冲区类似,会分配一个内存空间对fragment进行测试。
MinionsArt使用模板测试实现的一些效果
对MinionsArt的效果进行分析:MinionsArt的作品分为三层,一层原图,一层改变的图,还有一层进行mask的图层。
应用模板测试的经典独立游戏:笼中窥梦。
在介绍模板测试的内容之前,先复习一遍渲染管线。
我们从渲染管线出发,对渲染管线进行简单复习。
渲染管线的简单总结:应用数据阶段–>几何变换–>光栅化阶段–>逐片元操作
其中模板测试是位于光栅化的fragment shader之后的逐片元操作阶段中。
逐片元操作流程
首先逐片元操作的输入是经过光栅化后的fragment,他的输出是存储到framebuffer中准备渲染到屏幕上的像素。
输入fragment后,
第一步:Pixel Ownership Test。这一步是分配屏幕像素的使用权限。是设置屏幕上的哪一部分来显示计算结果,这就是为什么Unity中只在game/scene场中显示结果。
第二步:裁剪测试。是规定了你所使用的窗口后,你又要对window内的某一部分进行裁剪。比如对上边的这张图片,你可以控制只在这个窗口的坐上或者右下进行渲染。
第三步:透明度测试。透明度测试是设置了一个阈值,这个阈值控制了透明度的限制规则。比如说alpha的通道为0.5,那么alpha的值小于0.5的fragment就无法通过测试。基于这个简单的比较,他只能实现全透明或者全不透明效果,半透明效果需要在透明度混合中实现(以下是Unity入门简要中简单的透明度测试应用)
第四步:模板测试,从伪代码逻辑上来理解模板测试。
if(referenceValue & readMask comparisonFunction stencilBufferValue&readMask)
通过像素
else
舍去像素
//referenceValue是当前模板缓冲中的一个参考值,他会和readMask进行一个与操作。
//stencilBufferValue是模板缓冲区里的一个值,他也会和读掩码进行一个与操作。
//这两个值会根据comparisonFunction进行一个比较,通过了就会通过像素,否则就舍弃。
//按位与操作,对于二进制的每一位,只有两个都为1才返回1
从书面概念上来理解模板缓冲区:
模板测试所使用的的缓冲区与深度和颜色缓冲区类似,模板缓冲区为屏幕上的每个像素点保留了一个无符号整数值(通常为8位)。在渲染过程中,可以将这个值与一个预先设定的参考值进行比较,根据比较的结果来判定是否更新像素点中的颜色值。这个比较的过程称为模板测试。模板测试发生在透明度测试之后,深度测试之前。如果通过,则更新相应的像素点,否则则不更新。
第五步:深度测试
第六步:混合
最后是一些其它效果,经过以上操作后就可以存储到Framebuffer中了。
逐片元操作的整个流程是高度可配置,但是不可编程的流水线。他已经由硬件厂商规划好了流水线流程,我们能做的只有对其中的一些过程进行配置。
模板测试在UnityShader中的表示
stencil{
Ref referenceValue //0-255,用来和模板缓冲区中的存储值作比较
ReadMask readMask //分别是读掩码和写掩码操作
WriteMask writeMask
Comp comparisonFunction //设定如何比较
Pass stencilOperation //通过了模板缓冲区的操作
Fall stencilOperation //没通过的操作
ZFall stencilOperation //模板测试通过,深度测试没通过的操作。
}
以下为比较操作的一些配置
下边的为StencilOperation所提到的一些更新值
模仿案例一简单搭建的一个场景:
案例一的场景很简单,就是将场景内的所有物体的id都设置为1。把场景的Comp设置为equal,只有id与stencil buffer中的缓存值相等时才会被渲染。
而unity中的stencil buffer的默认值是0,id为1的在equal模式下与stencil buffer进行比较由于不等会被舍弃。而对于蒙版的材质来说,他会将他所在的像素在stencil buffer中的值用自己的id replace掉。此材质的id设置为1,所以他之后的stencil buffer在对应位置的值为1。由于更改后的stencil buffer能够与物体的id相匹配,所以可以达到案例一中的效果。
其中两部分关键stencil配置代码如下
Stencil{
Ref [_ID]
Comp equal
}
Stencil{
Ref[_ID]
Comp always
Pass replace
}
但是有一个问题:从蒙版观察过去的角度没有阴影。
我观察视频中的案例一,其蒙版观察的角度也没有阴影。0
模板测试能实现的一些效果:描边 多边形填充 反射区域控制 shadow volume阴影渲染。
查一下相关资料
深度测试是计算机用来判断深度测试的一种方法,没有深度测试图像会重叠在一起,没有先后效果。
从渲染管线出发,深度测试是在逐片元操作之后的。逐片元操作是位于fragment shader之后,渲染到屏幕之前的操作。
为了节省深度的开销,在片元着色器之前插入了一种名为early-z的技术。
深度测试的过程与模板测试类似,都是高度可配置的操作。深度缓冲的格式:
ZWriter
从书面概念上理解深度测试
所谓深度测试 ,就是针对当前对象在屏幕上framebuffer对应的像素点,将对象自身的深度值与当前该像素点缓存的深度值进行比较,如果通过了。本对象在该像素点才会将颜色写入颜色缓冲区,否则不会写入颜色缓冲区。
从发展上理解
通用的深度算法有两种,一是画家算法,二是ZTest算法。
画家算法:从后往前排序渲染,他先画最后一层物体,然后一层一层往前渲染。
他很重要的一个问题就是:有很多的无用消耗,后边的被前边的覆盖,那么后边的物体的开销就是无用的。这也是常说的overdraw。
ZBuffer算法:通过一个深度缓冲区来控制渲染顺序,有了深度缓冲区就需要对深度缓冲区进行控制。
这里有两个经典操作:ZTest和ZWrite。
再往后是控制不同类型物体的渲染顺序,透明物体,不透明物体等。这里引入了渲染队列的概念。
最后为了进一步减少OverDraw,对渲染细节上进行了很多优化,比如Early-Z,Z-cull,Z-check等技术。
深度缓冲就像颜色缓冲一样,在每个片段中存储了信息,并且和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以16,24或者32位Float的形式存储它的深度值。在大部分系统中,深度缓冲的精度都是24位的。
z-buffer中存储的是当前的深度信息,对于每个像素存储一个深度值。
通过ZWrite和ZTest来调用Z-Buffer,实现想要的渲染结果。
深度写入包括两种状态:ZWrite On和ZWrite Off
当我们开启深度写入的时候,物体被渲染时针对物体在屏幕上每个像素的深度都写入到深度缓冲区;反之,如果是ZWrite Off,那么物体的深度就不会写入深度缓冲区。但是,物体是否会写入深度,除了ZWrite这个状态之外,更重要的是需要通过深度测试通过,也就是ZTest通过,如果ZTest都没通过,那么也就不会写入深度了。
简而言之就是:ZWrite Off时,当一个物体在另一个物体前边时,他依旧不会写入深度缓冲。当他在另一个物体后边时,不会管他是否要考虑深度写入。
ZTest分为通过和不通过两种情况,ZWrite分为开启和关闭两种情况的话,一共是四种情况:
下边是一些更直观的效果:
Unity内几种内置的渲染队列,按照从后到前进行排序,队列数越小的,越先渲染 ,队列数越大的,越后渲染。
以下是Unity中几种内置的渲染队列:
Unity中设置渲染队列:Tags{“Queue” = “Transparent”}默认是Geometry
传统的渲染管线中,ZTest其实是在Blending阶段,也就是逐片元阶段进行深度测试的。所有对象的像素着色器都会计算一遍,并没有什么实际的性能提升,仅仅是为了得到正确的遮挡关系,会造成大量的无用计算,因为每个像素点上重叠了很多计算。
因此在现代GPU中运用了Early-Z技术,在Vertex阶段和Fragment阶段之间进行了一次深度测试,如果深度测试失败,就不必进行fragment阶段的计算了,因此在性能上会有很大的提升。但是最终的ZTest仍然需要进行,以保证最终的遮挡关系正确。
最后存储的是一个非线性的值,为什么是非线性,是因为将视椎体投影矩阵变换到[-1,1]这个过程是非线性的。
深度冲突是当两个物体的深度非常接近的时候,由于深度缓冲不知道哪一个物体更往前会导致两个物体不断切换。
如何防止深度冲突
技巧一:不要让两个物体靠的太近。
技巧二:尽可能将近平面设置的远一些,由于是非线性存储,越靠近近平面位置精度越高。
技巧三:放弃一些性能,在Zbuffer中采用更高精度的浮点类型。