图形 3.1 深度与模板测试 传送门效果示例
深度与模板测试 传送门效果示例
模板测试
模板测试能做些什么?
下图是OpenGL官网上的三张图。它能很形象的大概介绍模板测试的基本原理和使用方法。左图放在了颜色缓冲区,对应的中间的图则是模板缓冲区。模板缓冲区会针对左图上的每一个片元分配0-255的数字,默认为0。那么系统会根据设定的规则来进行输出,比如为0就输出黑色,1就输出左图,那么进行了模板测试之后就能得到第三张图。
下图是游戏《传送门》的一张截图,左边的蓝色光圈隧道里面出现的是另一个房间的景象。而实际上则是通过模板测试应用处理了墙的一部分。
下图的作品是由MinionsArt作者所制作,他在油管和Patreon上有很多shader案例和讲解。下图的作品都是通过深度与模板测试效果的应用来制作出的作品,通过观察可以发现,这种效果一般都会至少由遮罩内,遮罩外,遮罩本身的三层来组成,通过遮罩的切换,我们能观察到遮罩内外的不同效果。
下图是游戏《笼中窥梦》的一些游戏截图,游戏设计了一个方盒子作为主要场景,而场景的每个面显示的场景都不同,通过这些面的视觉误差效果来推进完成游戏解密。而这也是通过模板测试来实现的,通过对正方体的每个面做蒙版控制来显示内容。
模板测试是什么?
1.从渲染管线出发:
片元着色器处理完到帧缓冲区中间,需要进行逐片元操作的测试。
第一步获取对当前屏幕像素的使用权限;
第二步是裁剪测试,控制渲染的区域,裁剪掉其他部分;
第三步为透明度测试,通过设置一个透明度阈值,舍弃透明度在该阈值以下的像素;
而第四部就是本节所讲的模板测试。这部分内容不可由编程控制,它是由渲染管线和硬件自身规定好,但是是高度可配置的一个过程。
2.从逻辑上理解
referenceValue是当前片元模板缓冲区的参考值,它和遮罩Mask做一个与运算。
模板缓冲区里储存的值stencilBufferValue也和遮罩Mask做与运算。
然后我们将这两个值进行互相比较,通过了则输出这个像素,否则便舍弃它。
即通过一定条件来判断是对该片元或片元属性执行抛弃操作还是保留操作。
3.从书面概念上理解
而说到模板测试就要先说到模板缓冲区,模板缓冲区与颜色缓冲区和深度缓冲区类似,模板缓冲区可以为屏幕上的每个像素点保存一个无符号整数值(通常的话是个8位整数)。这个值的具体意义视程序的具体应用而定。在渲染的过程中,可以用这个值与一个预先设定的参考值相比较,根据比较的结果来决定是否更新相应的像素点的颜色值。这个比较的过程被称为模板测试。模板测试发生在透明度测试之后,深度测试之前。如果模板测试通过,则对应的像素点更新,否则不更新。
语法表示
stencil{
Ref referenceValue
ReadMask readMask
WriteMask writeMask
Comp comparisonFunction
Pass stencilOperation
Fail stencilOperation
ZFail stencilOperation
}
Ref 给当前片元设定的参考值(0-255),用来和模板缓冲区做对比。
ReadMask WriteMask 读写遮罩
Comp 比较操作
Pass 模板测试通过后模板缓冲区的执行操作
Fail 没有通过模板缓冲区所执行的操作
ZFail 通过了模板测试但没有通过深度测试所执行的操作
ComparisonFunction | |
Greater | 相当于">"操作,即仅当左边>右边,模板测试通过,渲染像素 |
GEqual | 相当于">="操作,即仅当左边>=右边,模板测试通过,渲染像素 |
Less | 相当于"<"操作,即仅当左边<右边,模板测试通过,渲染像素 |
LEqual | 相当于"<="操作,即仅当左边<=右边,模板测试通过,渲染像素 |
Equal | 相当于"="操作,即仅当左边=右边,模板测试通过,渲染像素 |
NotEqual | 相当于"!="操作,即仅当左边!=右边,模板测试通过,渲染像素 |
Always | 不管公式两边为何值,模板测试总是通过,渲染像素 |
Never | 不管公式两边为何值,模板测试总是失败,像素被抛弃 |
stencilOperation | |
Keep | 保留当前缓冲中的内容 |
Zero | 将0写入缓冲,即stencilBufferValue值变为0 |
Replace | 将参考值写入缓冲,即将referenceValue赋值给stencilBufferValue |
IncrSat | stencilBufferValue加1,如果stencilBufferValue超过了255,那么保留为255 |
DecrSat | stencilBufferValue减1,如果stencilBufferValue低于0,那么保留为0 |
Invert | 将当前模板缓冲值(stencilBufferValue)按位取反 |
IncrWarp | 当前缓冲的值加1,如果缓冲值超过255了,那么变成0,(然后继续自增) |
DecrWarp | 当前缓冲的值减1,如果缓冲值已经为0,那么变成255,(然后继续自减) |
模板测试总结
- 使用模板缓冲区最重要的两个值:当前模板缓冲值(stencilBufferValue)和模板参考值(referenceValue)
- 模板测试主要就是对这两个值使用特定的比较操作:Never,Always,Less,LEqual,Greater,Equal等等
- 模板测试之后要对模板缓冲区的值(stencilBufferValue)进行更新操作,更新操作包括:Keep,Zero,Replace,IncrSat,DecrSat,Invert等等
- 模板测试之后可以根据结果对模板缓冲区做不同的更新操作,比如模板测试成功操作Pass,模板测试失败操作Fail,深度测试失败操作ZFail,还有正对正面和背面精确更新操作PassBack,PassFront,FailBack等等
模板测试扩展
描边、多边形填充、反射区域控制、shadow volume阴影渲染、等等
深度测试
深度测试是什么?
从渲染管线出发:
下图大体能帮助理解深度测试:
深度测试在逐片元操作的部分,而前面的部分则是三个阶段:应用阶段、几何阶段、光栅化阶段。当然这些在前面的章节都有了解。而深度测试则在模板测试后,透明度混合之前的这个一个位置,它也属于逐片元操作的一部分,也是我们的一个高度可配置的一个测试过程。
以下是比较更改不同的设置而渲染的画面:
1图:全部开启深度测试
2图:绿色方块没有开启深度测试
3图:绿色方块比较函数改为Always
4图:绿色方块、红色方块比较函数均改为Always
5图:绿色方块、红色方块比较函数均改为Always,绿色方框绘制队列+1
6图:绿色方块比较函数改为Greater
从逻辑上理解:
可以看到和模板测试的格式差不多,都是通过一个比较函数来判断我们是否写入深度或者忽略深度写入颜色缓冲或者不写入,不过里面的判断和模板测试有点不同,它多了一个ZWrite的判断,如果深入写入都没有开启的话,也就不会进行比较函数的判断。
从书面概念上理解:
深度测试——所谓深度测试,就是针对当前对象在屏幕上(更准确的说是frame buffer)对应的像素点,将对象自身的深度值与当前该像素点缓存的深度值进行比较,如果通过了,本对象在该像素点才会将颜色写入颜色缓冲区,否则不会写入颜色缓冲。
从发展上理解:
深度缓冲区(Z-Buffer)
深度缓冲就像颜色缓冲(储存所有的片段颜色:视觉输出)一样,在每个片段中储存了信息,并且(通常)和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是24位的。
z-buffer中储存的是当前的深度信息,对于每个像素储存一个深度值。通过Z Write和Z Test来调用Z-Buffer,实现想要的渲染结果
Z Write
深度写入包括两种状态:ZWrite On与ZWrite Off
当我们开启深度写入的时候,物体被渲染时针对物体在屏幕(更准确地说是frame buffer)上每个像素的深度都写入到深度缓冲区;反之,如果是ZWrite Off,那么物体的深度就不会写入深度缓冲区。
ZTest分为通过和不通过两种情况,ZWrite分为开启和关闭两种情况的话,一共就是四种情况:
1.深度测试通过,深度写入开启;写入深度缓冲区,写入颜色缓冲区;
2.深度测试通过,深度写入关闭;不写深度缓冲区,写入颜色缓冲区;
3.深度测试失败,深度写入开启;不写深度缓冲区,不写颜色缓冲区;
4.深度测试失败,深度写入关闭;不写深度缓冲区,不写颜色缓冲区;
ZTest比较操作
ZTest状态 | 描述 |
Greater | 深度大于当前缓存则通过 |
LEqual | 深度小于等于当前缓存则通过 |
Less | 深度小于当前缓存则通过 |
GEqual | 深度大于等于当前缓存则通过 |
Equal | 深度不等于当前缓存则通过 |
NotEqual | 深度大于当前缓存则通过 |
Always(Off) | 深度无论如何都通过 |
Never | 深度无论如何都不通过 |
默认是ZWrite On和ZTest Lequal,深度缓存一开始为无穷大
渲染队列
Unity中有内置几种的渲染队列,按照渲染顺序,从先到后进行排序,队列数越小的,越先渲染,队列数越大的,越后渲染。
Backgroud(1000) | 最早被渲染的物体的队列。 |
Geometry(2000) | 不透明物体的渲染队列。大多数物体都应该使用该队列进行渲染,也是Unity Shader中的默认渲染队列。 |
AlphaTest(2450) | 有透明通道,需要进行Alpha Test的物体的队列,比在Geometry中更有效。 |
Transparent(3000) | 半透明物体的渲染队列。一般是不写深度的物体,AlphaBlend等的在该队列。 |
Overlay(4000) | 最后被渲染的物体的队列,一般是覆盖效果,比如镜头光晕,屏幕贴片之类的。 |
Unity中设置渲染队列:Tags { “Queue” = “Transparent” },默认是Geometry
- 不透明物体的渲染顺序:从前往后
- 透明物体的渲染顺序:从后往前
可以在shader的Inspector面板查看相关属性
简述Early-Z技术
传统的渲染管线中,ZTest其实是在Blending阶段,这时候进行深度测试,所有对象的像素着色器都会计算一遍,没有什么性能提升,仅仅是为了得出正确的遮挡结果,会造成大量的无用计算,因为每个像素点上肯定重叠了很多计算。因此现代GPU中运用了Early-Z的技术,在Vertex阶段和Fragment阶段之间(光栅化之后,fragment之前)进行一次深度测试,如果深度测试失败,就不必进行fragment阶段的计算了,因此性能上会有很大的提升。但是最终的ZTest仍然需要运行,以保证最终的遮挡关系结果正确。前面的一次主要是Z-Cull为了裁剪以达到优化的目的,后一次主要是Z-Check,为了检查,如下图:
深度值
模型空间中没有储存深度
通过M矩阵的变换后,在世界空间中存放在Z分量中
经过V矩阵变换到观察空间中,也储存在Z分量中(线性深度)
然后经过P矩阵变换到裁剪空间下,准备投影,为深度缓冲中储存的z/w(透视投影非线性)
最后通过投影映射的手段显示在屏幕空间上。
深度测试总结
- 使用深度缓冲区最重要的两个值:当前深度缓冲值(ZBufferValue)和深度参考值(referenceValue),并通过比较操作获取理想渲染效果
- Unity中的渲染顺序:先渲染不透明物体,顺序是从前到后;再渲染透明物体,顺序是从后到前
- 通过ZWrite和ZTest组合使用控制半透明物体的渲染
- 引入early-z技术后的深度测试相关的渲染流程
- 深度缓冲区中储存的深度值为0-1之间的浮点值,且为非线性
深度测试扩展
深入了解深度值在不同阶段的变化过程,通过深度测试、深度写入、深度图等方法控制深度缓冲区。
- 基于深度的着色(湖水渲染)
- 隐形贴图(ShadowMap)
- 透明物体、粒子渲染
- 透视X-Ray效果
- 切边效果等
参考链接
【技术美术百人计划】图形 3.1 深度与模板测试 传送门效果示例