本文简单总结了Emil Persson的文章Depth in Depth的一些内容,基本上是关于光栅化管线中的Z compare部分的一些工作原理以及优化方法。由于文章很早了,所以最新的硬件可能会有一些更好的特性,但是这些理解这些基本内容还是有一定好处的。
光线跟踪中,三角形的剔除是通过找到与光线的最近交点搞定的,从某种意义上说,空间划分结构(例如KD树)起了很大的作用。而光栅化采用了一种完全不同的工作原理,即深度缓冲。这种机理的好处在于工作效率较高,而且从正确性来说基本可以满足要求(当然,如果near plane和far plane的距离足够远,可能出现一些z fighting的现象,而光线跟踪中没有这种限制)。如果near plane和far plane设置的靠谱的话,基本上来说和光线跟踪中的三角形剔除没啥大区别。而且很多光线跟踪的primary ray求交都可以用raster来优化(当然,前提是interop的效率足够高,否则意义不是特别大)。那么本文将简单总结下AMD硬件中的z compare的一些特性,了解这些特性对于实时渲染还是很有意义的。
首先,考虑一个简单的问题,在光栅化过程中,有几次Z compare(这个问题我曾经在面试中遇到过,但是当时不了解,所以回答悲剧了)。答案是三次(对于现在最新的硬件或者Nv的卡可能不太一样),一次是Hi-Z,一次是Early-Z,都是在PS之前的,之后会有一次Late-Z。
Hi-Z
Hi-Z(Hierarchical Z),这是一种粗粒度的Z compare,它不是针对pixel的,而是基于tile的。所以这种比较所需要的内存要远远小于正常的深度缓冲,假设一个tile是16*16的,那么它只需要有普通的深度缓冲区的1/256即可,在HD-2000以前的硬件中,都是存在on-chip memory的,所以非常有限。如果深度缓冲区很多,可能导致部分的Hi-Z失效。而在这之后的硬件中,Hi-Z memory都是存在video memory里面的,所以基本上而言是不受内存限制的(当然也有理论上限,不过已经足够了)。
Hi-Z的工作方式与z compare没有本质区别,只不过是粒度大一些,对于每个tile存储一个z值而已。这里的z值存储是有一些规律的,如果我们的z compare是less equal或者less的话,那么这里的z值就存储当前tile中的最大z值。由于很多光栅化系统都是基于tile的,所以在光栅化三角形的时候,可以直接基于tile进行一次粗粒度的检测,如果当前的需要光栅化的tile中的最小z值都比hi-z memory中的值大的话,证明整个tile都是被前面的内容覆盖的,所以可以直接剔除掉,不用做多余工作。反之依然。
当然,hi-z不是任何时候都可以开启的,如果stencil功能开启的话,而且z fail或者stencil fail不是keep,那么hi-z会被driver直接禁止掉。因为hi-z禁止一个tile中的内容,而这里面有些是要更新stencil模板的,所以这样做会有错误发生,只能禁止hi-z防止这种错误。所以在我们不需要模板缓冲的时候,我们一定要尽可能的关闭这项功能。或者是更改这种功能使其不与Hi-Z冲突。例如,我们在做shadow volumn的时候,经常会用stencil fail来工作,因为这种方式可以适应视点在阴影内部的情况,所以貌似相对较优。但是实际上我们无意识的disable了Hi-z的功能,也是一种损失。所以如果大部分情况我们确信视点不会在阴影中的时候,我们可以用stencil pass来做shadow volumn,这样不会禁止Hi-Z。当我们检测到视点在内部的时候,可以做其他处理来得到正确的阴影效果。
注意这里由于Hi-Z不更新深度缓冲,所以类似alpha test和pixel discard之类的功能不会对于hi-Z造成影响。
Early-Z
Hi-Z结束之后的比较就是Early-Z了,这次是一种基于pixel的z compare,不过是在PS之前的。Early-Z就是比较需要光栅化的pixel深度与当前的depth值,如果前者大的话,就直接被剔除掉了,不会pass到管线的后续流程当中,从而节省了一定的带宽。对于HD2000前的硬件而言,early-Z是直接更新深度缓冲的,从而导致了它会与一些行为不兼容,例如alpha test。因为alpha test在early-Z之后,如果z compare可以通过,但是alpha test没有通过,early-Z可能会导致一些错误的深度更新。所以alpha-test和early-Z是不共存的。两者之间的矛盾在于深度缓冲的更新,在HD2000之后,early-Z不会与深度更新绑定,所以可以与alpha test一起工作。
Late-Z
Late-Z没啥好说的,就是在ps之后进行一次保守的z值比较,从而保证看到的东西是正确的。当然如果early-Z开启的话,那么late-z就没有太大意义了(在论坛里面,有人说两者是互斥的。这种互斥不是因为功能冲突,而是因为功能重复)。
Z-compression
Z-compression是一种压缩手段,它可以把深度缓冲区按照tile进行压缩。实际每个tile都有一个深度状态,状态分为三种cleared,compressed和uncompressed。
clear很简单,就是刚刚clear过的深度,还没有三角形覆盖该区域。所以如果需要读取cleared状态的深度,可以直接返回深度更新的值,而不用每个pixel的深度都读取了。那么换个角度思考,Z Clear的过程也变得很廉价,只需要更新状态就可以了,即把所有的tile的状态都更新到cleared,而不需要实际写像素深度。
compressed是一种压缩状态,假设一个tile是16*16的。正常我们在读取这个tile的深度的时候,我们需要读取256个float值,但是有些情况这些值是很有规律的。假设一个三角形完全覆盖某一个tile的所有pixel,那么我们可以根据这个tile一个角的深度值和x,y轴上的深度变化值计算出每个像素的深度值。这样我们只需要读取3个float就可以了,剩下的是一些计算工作了。而这些深度计算工作是非常适合GPU进行的,高度并行,行为高度一致。
还有一种情况,最糟糕。就是三角形的边缘跨过某一个tile,从而导致该tile是非压缩状态的。对于这种tile的深度读取,我们需要读取256个float,代价相对于前面大很多了。
下面简单根据上面的特性介绍一些优化方法吧(没有什么顺序):
1.渲染过程中,要从前往后渲染。这其实很好理解,这种渲染可以使得Hyber-Z的工作达到最高效率,可能使得大量的pixel在Hi-Z和Early-Z就被reject了,节省了PS很大的工作量。这里面有一种叫做Pre-Z的优化方法,如果上述方法效果不好(可能因为PS工作代价太大),那么我们先用disable color write,然后渲染一些我们认为重要的东西,只更新depth值,从而得到我们需要的深度缓冲。然后第二个pass就可以根据所得到的深度缓冲reject大量的pixel了,使得渲染更高效一些。
2.尽量不要用alpha test,texkill等功能,因为他们可能会关掉early-Z从而导致PS工作代价沉重。
3.不要更新z compare的方向,因为这种更新会导致Hi-Z直接失效(Hi-z的工作方式与z compare的方向直接有关的)。
4.最后渲染天空盒。因为天空盒也无非是模型,所以渲染的过程一定不要根据天空盒的大小更新远近裁剪平面。可以在vs或者projection matrix里面做点小手脚,例如vs中:
Out.position = mul( mvp , vertex ).xyww;
这样就导致了在perspective divide之后的z等于1,当然这里面我们要注意不要让它被远裁剪平面裁剪掉,这里可以把z compare改成less equal即可。
5.尽可能的关闭depth write如果不需要的话,因为这样可以让early-z工作的机会更多一些。
6.要把远近裁剪平面贴的近一些,否则会导致一些z fighting的现象。而且也会使得Hi-Z的工作效率更低一些。
本文总结了depth in depth里面的一些基本内容,想深入了解的朋友,还是推荐看原版论文比较合适。
AMD显卡中的Hyber-Z的工作原理
最新推荐文章于 2024-01-10 08:45:25 发布