翻译:李现民
最后修改:2012-07-03
原文:Depth sorting alpha blended objects
先说个题外话,本来我想回答在 Creators Club论坛上的一个常见问题,但(意外的是)我竟然没能从网上找到一个令人满意的答案。
问题本身很简单,但答案却有些复杂:
“为什么我的透明物体的绘制顺序是错误的,或者为什么它们的一部分不见了?”
当绘制一个3D场景的时候,将图形按深度排序非常重要,只有这样靠近摄像机的物体才能被绘制在(离摄像机)更远的物体的上面。我们不会希望远方的山脉被绘制在近在眼前的建筑物的上面!
当前得到广泛应用的深度排序技术有三种:
不幸的是,每种技术都有它的局限性。为了获得好的绘制结果,大多数游戏需要依赖于以上三种技术的组合。
1 深度缓冲
深度缓冲是一种简单而有效的办法,并且当你只绘制不透明物体时,其绘制结果非常完美,但该方法无法处理透明的物体!
这是因为深度缓冲算法仅仅跟踪记录了到目前为止所绘制的最近像素点,对不透明物体而言这足够了。举例说来,如果我们需要绘制两个三角形,A和B:
如果我们按照先B后A的顺序绘制,则深度缓冲会发现来自于A的新像素点比之前绘制的来自于B的像素点要近,因此直接覆盖绘制就可以了。如果我们按照相反的 顺序绘制(先A后B),则深度缓冲会发现来自于B的像素点比已经绘制的来自于于A的像素点要远,因此将会直接丢弃它们。无论哪种情况下我们都会得到正确的 结果:A在上面,而B在后面被隐藏。
但如果物体(几何体)是透明的怎么办?也就是物体B部分可以见时(透过物体A的半透明三角面片)。如果我们按照先B后A的顺序绘制,仍然会得到正确的结 果,但反之就会出错了。在第二种情况下,深度缓冲会首先从B得到一个像素点,然后发现已经绘制了某个来自于A的更近的像素点,但却不知道如何处理这种情 况。它仅有两种选择是:绘制B上的像素点(结果将是错误的,因为这会将更远处的B混合到更近的A之上,但alpha blending的顺序是不可交换的)或者直接将B整个丢弃。这很不好!
结论:深度缓冲对不透明物体是完美的,但对透明物体却没什么用。
2 画家算法
既然深度缓冲算法无法以错误的顺序正确绘制透明物体,那么一定存在一个简单的修正办法,对吧?只要我们总是保证以正确的顺序绘制就可以了!我们首先将场景 中的所有物体排序,这样我们就可以先绘制远处的物体,然后在其上绘制更近一些的物体,这样就可以保证前面示例中的B物体总是在A物体之前绘制。
不幸的是,这说起来容易做起来难。在很多情况下将对象排序是不够的。例如,A和B相互交叉的情况该怎么办?
这种情况很可能发生:比如说A是一个玻璃杯而B是一个放在里面的玻璃珠。现在根本无法以正确的方式对它们进行排序,因为A的一部分比B更近,但另一部分却更远。
我们甚至不需要使用两个单独的物体重现这个问题。组成玻璃杯的那些三角面片怎么处理呢?为了使结果看起来是正确的,我们需要在绘制玻璃杯的正面之前先绘制其背面。因此仅仅将物体进行排序还不够:我们真正需要的是排序每一个三角面片。
困难在于,将每个三角面片都排序的代价是极其昂贵的!而且即使我们可以承受这种代价,这也不足以保证在所有情况下都可以得到正确的绘制结果。比如两个透明的三角形相互交叉的情况如何处理?
没有办法对这些三角形排序,因为我们需要将B的上半部分绘制在A的前面,同时将其下半部分绘制在A的后面。唯一的解决办法是在检测这种情况发生时将这些三角形在它们相交的地方拆分,但这种做法的代价过于高昂了。
结论:画家算法需要你对排序的粒度作出权衡。如果仅仅对少量大型物体进行排序,则算法会非常快但精确度不高;反之,如果对大量小型物体进行排序(极限情况是对三角面片排序),则算法会很慢但会更加精确。
3 背面剔除
人们通常不认为背面剔除是一种排序技术,但事实上它是的确是一种重要的(排序)方法。它的局限性在于仅仅适用于凸面体。
考虑一个简单的凸面体,比如一个球体或一个立方体。无论你从哪个角度观察它,每一个屏幕像素都会被精确的覆盖两次:一次被物体的前面覆盖,另一次是被它的 背面覆盖。如果使用背面剔除丢弃物体背面的三角面片,那么就只剩下前面的了。哈哈!如果每一个屏幕像素只被覆盖一次,那你自动就会获得完美的alpha blending结果,而不需要任何排序。
当然,大多数游戏不会仅仅绘制球体或立方体:),所以背面剔除本身并不是一个完整的解决方案。
结论:背面剔除对凸面体是完美的,但对于其它的就无能为力了。
4 我该如何让游戏看起来更好一些?
最常用的方法是:
-
设置 DepthBufferEnable 与 DepthBufferWriteEnable 为 true
-
绘制所有的不透明物体(几何体)
-
保持 DepthBufferEnable=true,但修改 DepthBufferWriteEnable=false
-
将物体按它与摄像机之间的距离进行排序,然后以从后向前的顺序绘制
这种方法依赖于前述三种排序技术的组合:
-
不透明物体使用深度缓冲排序
-
透明物体与不透明物体仍然会被深度缓冲处理(所以你永远不会透过一个不透明物体看到一个透明物体)
-
画家算法按物体的相对关系对透明物体排序(如果两个透明物体相交的话会引起排序错误)
-
依赖背面剔除对单个透明物体上的所有三角面片进行排序(如果透明物体不是凸面体则会引起排序错误)
结果并非完美,但却非常有效并易于实现,而且对大多数游戏而言已经足够好了。
有很多方法可以用于改进排序的精确度:
避免alpha blending!你 的不透明物体越多,排序就越容易,也越精确。你真的需要在每个地方都使用alpha blending嘛?如果你的关卡设计需要在玻璃窗上再加一层,那么是否可以考虑修改设计以便实现起来更加容易呢?如果你正在使用alpha blending实现诸如树木之类的裁剪(cut-out)图形,是否可以考虑使用alpha test替代?就是简单地考虑接受/拒绝两种情况,这样被接受的像素点由于是不透明的,因而仍然可以使用深度缓冲排序。
放松,不要紧张。也许排序错误实际上并不那么糟糕呢?也许你可以试着调整显卡(使alpha通道更加柔和,更加半透明化一些)使排序错误看起来并不那么明显。在我们的3D粒子采样中就使用了这种方法,我们并没有尝试对单个烟雾中的粒子排序,而是挑选了一个粒子纹理使它看起来是OK的。如果你将烟雾纹理换成更加不透明的,那么排序错误就会变得比较明显了。
如果你的alpha混合模型不是凸面体,也许你可以试着将它们改的更加“凸”一些呢?即使不是完美的凸面体,只要它们越接近凸面体,排序错误就会越少。考虑将复杂的模型拆分成可独立排序的多个部件。比如一个人体模型无论如何都不是凸面体,但如果你把它拆分成躯干、头、手臂等,那么每一部分都可以近似认为是凸面体了。
如果你的纹理遮罩(texture masks)基本上是用于开/关裁剪(cut-outs)的,只是边缘部分有一些透明的像素用于反走样,你可以使用双pass绘制技术:
-
Pass 1:绘制不透明部分:关闭alpha blending,并且alpha test只接受100%不透明的区域,深度缓冲开启(补充:深度写入开启)
-
Pass 2:绘制边缘部分:开启alpha blending,并且alpha test只接受alpha < 1的像素,深度缓冲开启,深度写入关闭
以将物体绘制两次为代价,这种方法为纹理中间不透明部分提供了100%正确的深度缓冲排序,以及相对精确的半透明边缘部分排序。这是一种很好的方法,既对纹理裁剪的边缘部分做了一些反走样,同时也利用了深度缓冲的优点避免了对单个树木或草叶进行额外的排序。我们在广告牌采样中使用了这种技术:请参考Billboard.fx中的注释与effect passes部分。
使用z prepass。当你需要淡出一个正常状态下不透明的物体而又不想透过它自己的近端部分看到它的远端部分时,这是一种非常好的技术。假如从右边观察一个人 体。如果它是玻璃做的,那么你会期望透过它的右手臂看到躯干和左手臂。但是如果在整个淡出过程中它是一个实体人的话(不透明,也许是幽灵,或者正在传送, 又或者被杀死后正在重生),你会期望只看到透明的右手臂部分,以及它后面的背景,而不会同时看到躯干与左手臂。要达到这种效果需要:
-
设置 ColorWriteChannels=None,并启用深度缓冲
-
绘制物体到深度缓冲(但不影响颜色缓冲)
-
设置 ColorWriteChannels=All, DepthBufferFunction=Equal,并启用alpha blending
-
重绘物体,这时只有物体的最近端才会被混合到颜色缓冲中