游戏场景管理(二)视锥体剔除

作者:隐士低手

在学习场景管理之前,我们要先学习一下视锥体剔除(VFC),因为无论你使用什么空间划分算法,划分的空间都要进行视锥体剔除,被剔除的空间内部的所有物件都会被抛弃以此来加速渲染或碰撞。这也是场景管理的核心目的。

1. 包围体(BV)与视锥体

包围体就是把一个模型或空间包住的最小几何体,可以是球体,AABB包围盒,OBB包围盒。视锥体是一个被远近裁减面截断的锥体(6面体)。视锥体剔除就是判断包围体和视锥体的位置关系。不同的包围体与视锥体进行相交判断,复杂度是不一样的。这里的复杂度由低到高的顺序是球体 < AABB < OBB。另外如果把视锥体变成一个AABB包围体,那么相交判断也会变的非常简单。所以这里有一个优化点就是可以把复杂的包围体外面再套一个简单的包围体,比如AABB上面包裹一个球体,然后使用球体和视锥体进行测试,如果球体在视锥体内再做AABB测试。再比如可以把视锥体变成AABB然后进行测试,如果在AABB中再进行视锥体测试。这里只是提出一种优化的可能性,并不代表性能一定会提升。这取决于场景,如果一个场景中有很多节点,并且大部分节点都在视锥体之外那么这个优化应该会有一定的提升。我们应该注意到多一份包围体,就会多一份内存,天下没有免费的午餐,又一个空间换时间的优化算法。

2. 位置关系

包围体和视锥体有三种位置关系
① 包围体在视锥体外部
② 包围体在视锥体内部
③ 包围体部分在视锥体内部(相交)

如果包围体在视锥体外部,说明这个节点下的全部物件都不在可视范围内,可以全部丢弃。

如果包围体在视锥体内部说明全部物件都在视锥体内部,这些物件都会传递到GPU中进行渲染。这里有一个问题,如果包围体在视锥体内部也就说明所有包围体中的物件都在视锥体内部。物件进入到GPU中被拆解成三角面片,这些图元在GPU中做齐次空间裁剪,但这些三角面片其实是不需要做齐次空间裁剪的,因为它们的位置关系已经确定了,一定是在视锥体内部。GPU做了重复的工作。以前VFC和三角面片裁剪都在CPU中计算那么可以做标记优化,现在cpu和gpu按照流水线的方式工作,gpu并不知道这些三角面片不需要被裁剪,甚至gpu的设计就是喜欢蛮干并行处理,根本不管你逻辑是否已经被剔除。如果能够把VFC的工作交给gpu,一方面可以降低cpu的负担,另一方面gpu也可以避免一些重复性的工作,是不是会更好,这些疑问只有等到学习完Gpu driven rendering pipelines才能给出答案了。

包围体部分在视锥体内部,这说明包围体中的物件可能在视锥体中,可能在视锥体外,这时候我们需要做进一步的判断就是用每一个物件的包围体进行VFC。如果游戏的性能压力在cpu端,通常我们不会做进一步的测试。

3. 视锥体的六个面


视锥体的六个面由远近裁剪面,及上下左右四个面组成。

我们需要将这六个面用平面公式表示出来。

一个平面可以由平面上的三个点确定,也可以由平面法线和平面上的一个点确定。

我们只需要根据已知变量求解出上图中的ntl,nbl,ntr,nbr,ftl,fbl,ftr,fbr这八个点就可以确定6个平面。

平面方程的确定我简单说一下思路,具体细节网上有很多。

我们在unity中确定一个相机的参数是远近裁剪面+FOV。

FOV是水平方向的视角,比如60度就是广角摄像头。其实还应该有一个垂直方向的FOV才能确定一个锥体。如果水平方向和垂直方向的FOV角度相同,那么远近裁剪面就是一个正方形,视平面也是一个正方形。视平面是相机和近裁剪面之间的一个平面,它是用来接收投影变换的平面。这个平面的宽高比一定要和屏幕空间的宽高比一致,否则图片会发生拉伸。unity之所以没有让我们设置垂直平面的FOV是因为unity会自动根据屏幕的宽高比及水平FOV反推出垂直平面的FOV。简单的说确定一个相机需要知道它的远近裁剪面,水平FOV以及屏幕的宽高比。

上面这些参数只是确定了一个视锥体的形状,它在任何一个空间中还需要确定位置和朝向。比如我们求解的六个平面方程是在世界空间中,那么我们还需要知道相机在世界空间中的位置及朝向。

我们整理一下目前已知的变量
  •   相机的位置
  •   相机的朝向
  •   相机的FOV
  •   相机的宽高比
  •   近裁剪距离
  •   远裁剪面距离

我们根据这些信息可以推导出
  •   相机的位置
  •   相机的朝向(垂直与远近裁剪面中心的向量)
  •   近裁剪面的距离
  •   近裁剪面的高度
  •   近裁剪面的宽度
  •   远裁剪面的距离
  •   远裁剪面的宽度
  •   远裁剪面的高度


有了这些信息我们就可以推导出6个平面的方程了。

4. 如何判断一个点是否在视锥体内

这个很简单,只要这个点在6个平面中任意一个平面的外部那么这个点就在锥体外,反之这个点在所有平面的内部那么它就在视锥体内部。

如何判断一个点在平面内部还是外部,假定我们使用朝向锥体内部的法线确定的平面方程。那么平面的前面就是法线的方向也就是锥体内部。我们把点带入平面方程,如果大于零,点在平面的前面,也就是锥体的内部。

5. 如何判断AABB包围盒是否在视锥体内部

我一开始也会很天真的以为如果AABB的8个点都在视锥体外部那么这个包围体就在视锥体外部了,事实是这样吗?


事实是打脸的,图中的黄色方块,所有点都在外部,但是它和视锥体却是相交的。

那如果所有点都在同一个平面的外部就一定在视锥体外部了吧。这个没错,但是反之错了。


事实再一次打脸,图中橙色的方块,虽然所有点没有在同一个面的外部,但是它却在视锥体的外部。

我们做的是快速判断,为了追求速度不能做更复杂的判断了,二选一选一个吧,选哪一个呢?

当然是选下面的方案了,上面的方案是错误的因为有些物件明明在视锥体中却不能显示,这是一种错误。而下面的方案虽然会额外传递一些无用的物件到gpu中但它至少是正确的,要先保证正确性才能考虑性能优化。

6. 齐次空间裁剪

在投影变换后透视除法之前存在一个裁剪空间,这个空间就是齐次裁剪空间,在这个空间中的视锥体是一个以相机为原点,边长为1的立方体。如果点在世界空间中的坐标为(x,y,z),将这个点通过矩阵乘法变换到齐次裁剪空间中(x',y',z'),那么判断(x',y',z')这点是否在视锥体内会变得非常简单。


满足上面条件的(x,y,z)就在视锥体内。

这个方法实现简单,但是效率不高,因为执行一次坐标变换需要16次乘法+12次加法。

但是如果使用平面方程只需要3次乘法+2次加法。

虽然可以使用sse指令集进行优化,但是平面方程计算也可以使用sse指令优化比如方程为:

Ax + By + Cz + D = 0可以看成是向量(A,B,C,D)和向量(x,y,z,1)的点积,如果用sse4指令集一条指令就可以计算出结果。

因此把点转换到齐次空间中进行判断实现简单,但是并不高效。

为什么这里要提到齐次裁剪空间呢,因为我们在写shader的时候会用到这个变换矩阵,如果代码中可以拿到这个矩阵,我们就可以通过这个矩阵快速的反推出视锥体6个裁剪面的方程,具体实现就不介绍了,这里只是说可以优化求解平面方程的速度。

7. 每次都需要判断8个点么?


有更快的算法,首先我们要找出长方体的n点和p点,p点就是距离平面最近的顶点,n点就是最远对角线的顶点(距离p最远的顶点)

如果p在平面外侧那么可以判断这个长方体在平面外侧。

如果p点在平面内侧,n点在平面外侧,说明它们相交。

如果p点和n点都在内侧,则说明长方体在平面内侧。

之前我们需要判断8个点现在只需要判断2个点,多么好的优化。

如何求p点和n点如果是AABB包围盒的话,求法很简单

p = (xmin,ymin,zmin) if (normal.x >= 0) p.x = xmax; if (normal.y >=0)) p.y = ymax; if (normal.z >= 0) p.z = zmax:

n = (xmax,ymax,zmax) if (normal.x >= 0) n.x = xmin; if (normal.y >=0)) n.y = ymin; if (normal.z >= 0) n.z = zmin:

这里的normal是平面的法线向量。

8. 还能再快么

这个算法在Game Programming Gems 5 中提到名字叫做Radar Approach - Testing Points 。它使用的算法和之前的算法完全不一样,大体思路是这样的。


首先我们为相机构建一个坐标系,这个坐标系的原点就是相机的位置z轴就是相机的朝向。假定这个坐标系的基向量为x,y,z。

图中红色的点p是被测试的点,蓝色的点是p在z轴的投影。

v = p - cc

p.z = v • z

v向量是p点的向量表示,z是单位基向量,所以p点的z坐标的值就是v向量和z向量的点积。

如果近裁剪面 < p.z < 远裁剪面,那么这个点在z轴上就在视锥体中了。

同理我们可以求出p.x和p.y,然后拿这两个值和一个平面的长和宽做对比,这个平面就是通过p点且和视线垂直的平面,假想成和远近裁剪面平行的平面。


a为垂直方向的fov,那么h = p.z * 2 * tan(a/2)

-h/2 < pc.y < h/2

(这里的图是h,如果是unity3d的话可以变成w = p.z * 2 * tan(a/2)那么-w/2 < p.x < w/2)

根据宽高比我们可以求出w = h * ratio;则-w/2 < p.x < w/2

这个方法和上面np点的算法到底谁快,我没有测试过。等到后面实现的时候可以做一下性能测试,但是测试的前提是我的demo能加载一个复杂的场景。。。

9. 还能再快么

对于基础测试目前我掌握的信息就这些了,如果有大牛还请赐教更快的算法。如果把这些放到gpu计算是个不错的想法。

10. 平移旋转一致性测试

这是逻辑上的优化,不是算法层面上的优化,我们假定相机移动的很慢,如果一个AABB被一个平面拒绝,那么下一帧这个平面被拒绝的可能性就会很大,所以我们下次进行测试的时候可以先从这个平面进行。

11. 八分测试

这个算法就是把视锥体切成八部分,然后巴拉巴拉,我总觉得这个算法有把简单问题复杂化的意思而且它还有限制所以就没有仔细研究,限制如下图:


使用八分测试来检测包围盒,需要包围盒满足它的中心到它的顶点距离必须小于视锥体中心到视锥体平面的最小距离。也就是图中的d2要小于d1,我总觉得这个算法有把简单问题复杂化的意思。所以就没有仔细研究。

版权声明:本文为CSDN博主(隐士低手)原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/liran2019/article/details/106851796

最新文章