在3D碰撞检测中,为了加快碰撞检测的效率,减少不必要的碰撞检测,会使用基本几何体作为物体的包围体(Bounding Volume, BV)进行测试。基本包围体的碰撞检测相对来说便宜也容易的多,所以如果在基本包围体的碰撞检测中都没有通过的话,那么就没有必要进行更加复杂的碰撞检测了。
而对于不同性质,不同形状的模型,需要根据情况选择不同的包围体,一般来说,包围体分为如下的几种:
Sphere, AABB, OBB, 8-DOP, Convex Hull这几种常见的。
接下来将向大家讲述如何使用Sphere包围体。
想要使用包围体,我们就要知道,如何的表示一个包围球体。对于球体来说,表示它很简单,如下所示:
struct Sphere { VECTOR3 center ; float radious ; };
对于包围球之间的碰撞检测,十分的简单,只要判断两个包围球心之间的距离是否小于他们两个的半径之和就可以了。如果小于,那么这两个包围球发生了交叉,没有的话,就相互分离。以下是进行碰撞检测的代码:
int TestSphereSphere(Sphere a, Sphere b) { VECTOR3 d = a.center - b.center ; float dist2 = Dot(d, d); float radisum = a.radious + b.radious ; if(dist2 < radisum * radisum) return 1 ; else return 0 ; }
很简单不是嘛!由于进行开平方计算要消耗大量的CPU,所以,我们直接对球心之间距离的平方和他们半径之和的平方进行比较,结果与进行距离和半径之和的比较一致。
球形包围体的计算有很多的算法,在本篇文章中将讲述两种常见的计算方法。如果你使用DirectX,就会知道,DirectX内置了一个D3DXComputBoundingSphere的函数。这里将不会使用这个函数,而是使用我们自己创建的计算方法来进行包围球体的计算。
首先来介绍第一种包围球体的计算方法。
我们知道,在3D模型的表示中,一般都是用一系列的顶点来描述一个模型。所以,要求一个包围球体,我们就必须确定这个包围球体的球心,然后计算每一个顶点与球心的距离,选取最长的距离作为包围球体的半径。这个简单的算法就能够确定一个包围球体了。那么,有一个问题,如果的确定这个模型的球心了?
我们通过如下的简答方法来计算出这个模型的球心。我们将所有的顶点相加,然后除以顶点数,这样就能得到一个球心的位置了。
到这里,这个方法的理论就介绍完毕了,很容易不是吗???下面来看看这个方法的代码部分:
void Sphere::computeBoundingSphereAverage(VECTOR3* vertices, unsigned int vertex_num) { //Compute the center point VECTOR3 total ; total.x = 0 ; total.y = 0 ; total.z = 0 ; for(int i = 0 ; i < vertex_num ; i ++) { total.x += vertices[i].x ; total.y += vertices[i].y ; total.z += vertices[i].z ; }// end for total.x /= vertex_num ; total.y /= vertex_num ; total.z /= vertex_num ; center = total ; //Compute the radious float r = 0 ; for(int i = 0 ; i < vertex_num ; i ++) { VECTOR3 temp ; Vec3Sub(temp, total, vertices[i]); float length = 0 ; length = Vec3Length(length, temp); if(length > r) r = length ; }// end for radious = r ; }// end for computeBoundingSphereAverage
看看使用这个方法计算出来的包围球的效果如何:
Ritter,Jack提出了一种新的近似的计算包围球体的方法。它的思路是这样的:
首先我们分别找到这个模型在x,y,z正负六个方向上的最远距离的6个点。然后我们分别计算出这三对点之间的长度,也就是x轴向上两个点之间的长度,y轴向上两个点之间的长度,z轴向上两个点之间的长度。我们选取长度最长的那一个作为包围球的直径,以这个长度的两个点的中点作为包围球的球心。
通过上面的方法,我们能近似的求出这个包围球,但是并不能保证模型中的每一个顶点都在这个包围球里面,所以我们还需要对此进行修正。
我们遍历所有的顶点,判断顶点是否在球体里面,如果在里面,则忽略它,如果不在里面,我们按照下面的算法来对球体进行修正,以使的新的球体能够包含这个点。
请看下图:
我们假设当前的球心为O,半径为r。现在我们发现在这个球体之外有一个点P。所以,我们需要对这个包围球体进行修正,以便于将这个点包围在球体里面。为了最紧凑的包围住这个点,我们将点P与球心O连线,交圆与T点。这时,你就会发现,TP就是我们要求的包围球的新直径了,那么球心也就是他们之间的中点了。
求出T的计算比较庞大,所以我们计算新的半径使用下面的方法:
由于P点和O点都是已知的,所以求他们之间的距离比较容易。也就是说新的半径为: (r + OP) * 0.5
有了新的半径之后,我们需要做的就是平移球心点,平移的向量大小刚好就是SP的长度,方向是从O点指向P点,其中S点为PR的中点。
所以有了上面的算法,我们依次的遍历所有的点,我们就能够确定一个近似的包围球了。
这个理论也很简单,下面是实现的代码:
void Sphere::computeBoundingSphereRitter(VECTOR3* vertices, unsigned int vertex_num) { unsigned int maxX = 0 , maxY = 0, maxZ = 0 , minX = -1, minY = -1, minZ = -1 ; //Find the max and min along the x-axie, y-axie, z-axie for(int i = 0 ; i < vertex_num ; i ++) { if(vertices[i].x > maxX) maxX = i ; if(vertices[i].x < minX) minX = i ; if(vertices[i].y > maxY) maxY = i ; if(vertices[i].y < minY) minY = i ; if(vertices[i].z > maxZ) maxZ = i ; if(vertices[i].z < minZ) minZ = i ; }// end for float x = 0; VECTOR3 sub1 , sub2 ; sub1.x = vertices[maxX].x ; sub1.y = vertices[maxX].y ; sub1.z = vertices[maxX].z ; sub2.x = vertices[minX].x ; sub2.y = vertices[minX].y ; sub2.z = vertices[minX].z ; Vec3Sub(sub1, sub1, sub2); Vec3Dot(x, sub1, sub1); float y = 0 ; sub1.x = vertices[maxY].x ; sub1.y = vertices[maxY].y ; sub1.z = vertices[maxY].z ; sub2.x = vertices[minY].x ; sub2.y = vertices[minY].y ; sub2.z = vertices[minY].z ; Vec3Sub(sub1, sub1, sub2); Vec3Dot(y, sub1, sub1); float z = 0 ; sub1.x = vertices[maxZ].x ; sub1.y = vertices[maxZ].y ; sub1.z = vertices[maxZ].z ; sub2.x = vertices[minZ].x ; sub2.y = vertices[minZ].y ; sub2.z = vertices[minZ].z ; Vec3Sub(sub1, sub1, sub2); Vec3Dot(z, sub1, sub1); float dia = 0 ; int max = maxX , min = minX ; if( z > x && z > y) { max = maxZ ; min = minZ ; dia = z ; }else if(y > x && y > z) { max = maxY ; min = minY ; dia = y ; } //Compute the center point center.x = 0.5 * (vertices[max].x + vertices[min].x) ; center.y = 0.5 * (vertices[max].y + vertices[min].y) ; center.z = 0.5 * (vertices[max].z + vertices[min].z) ; //Compute the radious radious = 0.5 * sqrt(dia); //Fix it for(int i = 0 ; i < vertex_num ; i ++) { VECTOR3 d ; Vec3Sub(d, vertices[i], center); float dist2 = 0 ; Vec3Dot(dist2, d, d); if(dist2 > radious * radious) { float dist = sqrt(dist2); float newRadious = (dist + radious) * 0.5 ; float k = (newRadious - radious) / dist ; radious = newRadious ; VECTOR3 temp ; Vec3Mul(temp, d, k); Vec3Add(center, center, temp); }// end if }// end for vertex_num }// end for computeBoundingSphereRitter
上面两种方法,虽然第一种最简单,但是同样的他的效果不如第二种的好,如果你不能直观的看出来,那么请看下面两种对比图:
第一种算法:
第二种算法:
很明显的看出,第二种算法它的包围球体更加的紧凑点。
下面,我将这个包围球的类的所有代码列出来(仅仅有包围球的类,关于DirectX的操作部分,不属于这个类)
//--------------------------------------------------------------------------------- // declaration : Copyright (c), by XJ , 2014 . All right reserved . // brief : This file will define the bounding sphere in collision system // author : XJ // date : 2014 / 6 / 20 // file : Sphere.h // version : 1.0 //--------------------------------------------------------------------------------- #pragma once #include"XJMath.h" namespace XJCollision { class Sphere { public: Sphere(); Sphere(VECTOR3 c, float r); ~Sphere(); public: /** * This method will use the average method to compute the bounding sphere of the * input vertices array */ void computeBoundingSphereAverage(VECTOR3* vertices, unsigned int vertex_num); /** * This method will use the Ritter's method to compute the bounding sphere */ void computeBoundingSphereRitter(VECTOR3* vertices, unsigned int vertex_num); public: VECTOR3 center ; float radious ; }; };
#include"Sphere.h" #include<cmath> using namespace std ; using namespace XJCollision ; Sphere::Sphere() :center(), radious(0.0f) { } Sphere::Sphere(VECTOR3 c, float r) :center(c), radious(r) { } Sphere::~Sphere() { } void Sphere::computeBoundingSphereAverage(VECTOR3* vertices, unsigned int vertex_num) { //Compute the center point VECTOR3 total ; total.x = 0 ; total.y = 0 ; total.z = 0 ; for(int i = 0 ; i < vertex_num ; i ++) { total.x += vertices[i].x ; total.y += vertices[i].y ; total.z += vertices[i].z ; }// end for total.x /= vertex_num ; total.y /= vertex_num ; total.z /= vertex_num ; center = total ; //Compute the radious float r = 0 ; for(int i = 0 ; i < vertex_num ; i ++) { VECTOR3 temp ; Vec3Sub(temp, total, vertices[i]); float length = 0 ; length = Vec3Length(length, temp); if(length > r) r = length ; }// end for radious = r ; }// end for computeBoundingSphereAverage void Sphere::computeBoundingSphereRitter(VECTOR3* vertices, unsigned int vertex_num) { unsigned int maxX = 0 , maxY = 0, maxZ = 0 , minX = -1, minY = -1, minZ = -1 ; //Find the max and min along the x-axie, y-axie, z-axie for(int i = 0 ; i < vertex_num ; i ++) { if(vertices[i].x > maxX) maxX = i ; if(vertices[i].x < minX) minX = i ; if(vertices[i].y > maxY) maxY = i ; if(vertices[i].y < minY) minY = i ; if(vertices[i].z > maxZ) maxZ = i ; if(vertices[i].z < minZ) minZ = i ; }// end for float x = 0; VECTOR3 sub1 , sub2 ; sub1.x = vertices[maxX].x ; sub1.y = vertices[maxX].y ; sub1.z = vertices[maxX].z ; sub2.x = vertices[minX].x ; sub2.y = vertices[minX].y ; sub2.z = vertices[minX].z ; Vec3Sub(sub1, sub1, sub2); Vec3Dot(x, sub1, sub1); float y = 0 ; sub1.x = vertices[maxY].x ; sub1.y = vertices[maxY].y ; sub1.z = vertices[maxY].z ; sub2.x = vertices[minY].x ; sub2.y = vertices[minY].y ; sub2.z = vertices[minY].z ; Vec3Sub(sub1, sub1, sub2); Vec3Dot(y, sub1, sub1); float z = 0 ; sub1.x = vertices[maxZ].x ; sub1.y = vertices[maxZ].y ; sub1.z = vertices[maxZ].z ; sub2.x = vertices[minZ].x ; sub2.y = vertices[minZ].y ; sub2.z = vertices[minZ].z ; Vec3Sub(sub1, sub1, sub2); Vec3Dot(z, sub1, sub1); float dia = 0 ; int max = maxX , min = minX ; if( z > x && z > y) { max = maxZ ; min = minZ ; dia = z ; }else if(y > x && y > z) { max = maxY ; min = minY ; dia = y ; } //Compute the center point center.x = 0.5 * (vertices[max].x + vertices[min].x) ; center.y = 0.5 * (vertices[max].y + vertices[min].y) ; center.z = 0.5 * (vertices[max].z + vertices[min].z) ; //Compute the radious radious = 0.5 * sqrt(dia); //Fix it for(int i = 0 ; i < vertex_num ; i ++) { VECTOR3 d ; Vec3Sub(d, vertices[i], center); float dist2 = 0 ; Vec3Dot(dist2, d, d); if(dist2 > radious * radious) { float dist = sqrt(dist2); float newRadious = (dist + radious) * 0.5 ; float k = (newRadious - radious) / dist ; radious = newRadious ; VECTOR3 temp ; Vec3Mul(temp, d, k); Vec3Add(center, center, temp); }// end if }// end for vertex_num }// end for computeBoundingSphereRitter