我经常说,我们以前玩过的很多游戏虽然看上去是 2D 的,但其实是 3D 游戏——比如热血物语、双截龙系列,它们具有一套三维的立体空间逻辑,从而不再使得游戏的舞台限制在只有左右移动和上下跳跃这两种轴向——拥有三个维度极为自由开放的移动范围相信也给很多玩家留下了深刻的印象,在上次的文章(https://indienova.com/indie-game-development/make-a-fc-nes-collision/)里,我们已经介绍了这种游戏的物理部分简单实现,本次将会聊到这类游戏在渲染上的重要技巧。
显然,在用 2D 精灵模拟的 3D 空间里,任何一个物体都不简单:如图所示的一个立方体,具有一个 oblique
视角(或者,我们叫 cabinet
视角更准确)的造型。这样的视角下,如果有另外一个精灵和它相互遮挡,应该怎么处理它们的渲染顺序关系呢?
注:为了简化描述,本文章的 3D 空间假设所有物件的 z
坐标都相同,即“没有和地面的高度差”。
任意两个物体之间的排序
按照我们一般的简单思路(比如在经典的 topdown 视角下,如 RPGMAKER 系列),谁在世界上具有较大的 y
值,谁就靠近屏幕,谁就应该更靠后地渲染,对吧?不过在这样的视角下,就没有那么简单了,来看一个例子:
如图,根据十字形标记,我们可以轻松地发现 shari 的 y
坐标小于立方体的坐标,所以她理应被挡在立方体的后面(先于立方体渲染),对吧?可是当她来到另外一侧的时候,情况就变化了:
如图,shari 的 y
坐标仍然小于立方体的 y
坐标,但这个位置上,我们是希望 shari 遮挡立方体而不是被立方体遮挡的,所以我们需要考虑到更多的情况。
显然,这个世界里的任何一个物体都应该携带它的"底座"信息。如图,红色的平行四边形就是上面立方体的"底座"信息,而绿色是它的高度信息(如果你需要一些更细致的排序,比如算上 z
轴,本文暂不讨论)有了这个信息,我们就可以确定任意两个对象之间的绘制顺序。
我们把两个要排序的物体分别命名为 A
和 B
,A
底座的上边缘称为 aTop
,下边缘称为 aBottom
;B
底座的上边缘称为 bTop
,下边缘称为 bBottom
。
aBottom
的 y
坐标小于 bTop
的 y
坐标:
这时候 A
显然是在 B
的后方,也就是 A
先于 B
渲染。
aTop
的 y
坐标大于 bBottom
的 y
坐标:
这时候 A
显然是在 B
的前方,也就是 B
先于 A
渲染。
其它的情况:
由于我们这里的视角下,侧面是一个 45
度的斜线,所以我们可以很轻松地把两个物体下边缘的 y
轴距离加到 aBottom 的右端点去进行判断,像是这样:
distanceY = bBottom.y - aBottom.y
(我们用 left
和 right
表示一个线段的左端点和右端点)
aBottom.left.x > bBottom.right.x + distanceY
如果这个判断是成立的,那就是上图所述的情况:A
在 B
的前方,也就是 B
先于 A
渲染。
否则就是下图所述的情况:A
在 B
的后方,也就是 A
先于 B
渲染。
对场景内所有物体的排序
好,至此我们已经可以为场景内任意的两个对象进行排序了,那么,我们也可以为场景中所有的对象组织排序。很多人的第一反应是,直接用冒泡/选择/归并/快排这样的算法来对场景中需要排序的物体进行排序不就好了吗?其实这是不合适的,因为场景中的物体遮挡存在拓扑关系,如果直接使用线性表的排序方法,可能在 swap 一对物体以确保他们遮挡关系的同时又会直接破坏了另一对物体的遮挡关系,因此,我们的第一步是对场景中需要排序的物体进行连线。
带有弧线的那一端是射线的末端,末端的物体比首端的物体更先渲染(位置更靠后)。
这里说明一下,我们只对需要排序的一对物体进行连线(出于性能考虑),所以你的显示对象可能需要有相应的 bounding_box
或是什么的。
当场景中的物体足够多,遮挡关系足够复杂时,不难发现,我们的物体遮挡关系实际上构建出了一个有向图。而有向图可以通过拓扑排序来获取一个不唯一的线性序列,照着这个线性序列进行渲染就可以获得正确的场景排序了。
(图中蓝绿色的矩形是判断精灵相交/重叠用的 bounding_box
,DRAWIDX
则是拓扑排序后得到的渲染顺序值)
这样,就得到了整个场景的正确呈现顺序。
最后一点问题
相信我们都看到过一些"视觉错觉"的趣味图片,对吧?譬如三个无限循环的台阶,相互遮挡的三个棱柱……之类的,很有意思,不过在游戏开发当中可一点也不有趣。很显然,当游戏场景中出现了几个互相存在遮挡和被遮挡关系的对象时,我们构造出的有向图就成环了。而成环的有向图是不能进行拓扑排序的,这就导致我们没有办法去按正确顺序呈现画面。解决这个问题的最好方法就是把容易引起该问题的那个物体分割为两个物体,使得其中的一个部分"专门遮挡"而另一个部分"专门被遮挡",这样就可以消除我们构造的有向图中的环。
本次的讨论就到这里,祝大家开发顺利,新年快乐!
有关oblique投影的2D渲染顺序问题,之前就遇到过。没想到可以用拓扑排序解决,学习了
最后排序的地方还是没太懂,可以说的更具体点吗?
@阿客:文章中写得已经足够直白。首先你要给每一对"需要排序的对象"之间按照它们的顺序安排一条有向的线,这样,所有场景对象就会形成一个有向图;如果你对上述描述有困惑,可以尝试学习邻接矩阵或邻接表这样的东西来了解有向图的一些程序实现方式,有了这个有向图之后,只需要对它执行拓扑排序就可以获得最终的渲染序列了(如果你不了解拓扑排序的话,也可以去查找相关的实现代码,我们一般是用记录每个节点的入度,再用节点逐个削除法来实现的)
@Lanza:谢谢了
@Lanza:不知道为什么,你回复里面说的一下就明白了