书籍:《实时碰撞检测算法技术》
在碰撞检测中,为减少计算消耗,在进行相交测试前,可以先进行粗略的包围体(BV)测试。对于某些应用程序,包围体测试足以提供碰撞检测依据。
一般情况下,包围体计算须采用预处理而非实时计算。当包围体所包含的对象移动时,一些包围体需要实现空间重对齐。因此,若包围体的计算代价高昂,重对齐包围体就是一个更可取的方法。
A)低消耗的相交测试。
B)实现紧密拟合。
C)计算耗费较少。
D)易于旋转和变换。
E)内存占用较少。
球体、轴对齐包围盒(AABB)、有向包围盒(OBB)、8-DOP以及凸壳
以下仅对球体,AABB和OBB进行简单介绍。
轴对齐包围盒(AABB)是应用最广泛的包围体之一。最大特点是能实现快速的相交测试,即仅执行相应的坐标值之间的比较。
轴对齐包围盒(AABB)有3种常规表达方式
1.采用各坐标轴上的最小值和最大值
//region R = (x,y,z) | min.x <= x <= max.x, min.y <= y <= max.y, min.z <= z <= max.z
struct AABB {
Point min;
Point max;
};
AABB间的相交测试
bool Collision(AABB a, AABB b) {
//Exit with no intersection if separated along an axis
if(a.max[0] < b.min[0] || a.min[0] > b.max[0]) return false;
if(a.max[1] < b.min[1] || a.min[1] > b.max[1]) return false;
if(a.max[2] < b.min[2] || a.min[2] > b.max[2]) return false;
//Overlapping on all axes means AABs are intersecting
return true;
}
2.采用一个最小顶点值和直径范围dx,dy,dz
//region R = (x,y,z) | min.x <= x <= min.x + dx, min.y <= y <= min.y + dy, min.z <= z <= min.z + z
struct AABB {
Point min;
float d[3];
};
AABB间的相交测试
bool Collision(AABB a, AABB b) {
float t;
if((t = a.min[0] - b.min[0]) > b.d[0] || -t > a.d[0]) return false;
if((t = a.min[1] - b.min[1]) > b.d[1] || -t > a.d[0]) return false;
if((t = a.min[2] - b.min[2]) > b.d[2] || -t > a.d[0]) return false;
return true;
}
3.给定AABB的中心点C,以及各轴向半径rx,ry,rz
//region R = (x, y, z) | |c.x - x| <= rx, |c.y - y| <= ry, |c.z - z| <= rz
struct AABB {
Point c;
float r[3];
};
AABB间的相交测试
bool Collision(AABB a, AABB b) {
if(Abs(a.c[0] - b.c[0]) > (a.r[0] + b.r[0])) return false;
if(Abs(a.c[1] - b.c[1]) > (a.r[1] + b.r[1])) return false;
if(Abs(a.c[2] - b.c[2]) > (a.r[2] + b.r[2])) return false;
return true;
}
对于需要执行大量相交测试的碰撞检测系统,可根据状态的相似性对测试进行排序。例如,如果相应操作基本出现在xz平面,则y坐标测试应最后执行,从而使状态的变化降低到最小程度。
若物体只是以平移方式运动,与“最大-最小值”方式相比,另外两种更加方便——只需要更新6个参数中的3个(即Point中的x,y,z)。“中心-半径”的另一个比较有用的性质是:可作为一个包围球加以检测。
对于AABB的计算与更新,在此仅列出以下4种基本方法,不进行详细说明:
A)使用固定尺寸且较为松散的AABB包围物体对象。
B)基于原点确定动态且紧凑的重构方式。
C)利用爬山法确定动态且紧凑的重构方式。
D)基于旋转后的AABB,确定近似且动态的重构方式。
与包围盒相比,球体是另一类较常用的包围体。如同包围盒,球体也具备快速相交测试这一特征。同时,球体基本不受旋转变换的影响。
//region R = (x, y, z) | (x – c.x) ^ 2 + (y – c.y) ^ 2 + (z – c.z) ^ 2 <= r ^2
struct Sphere {
Point c; //Sphere center
float r; //Sphere radius
};
球体间的相交测试:
计算两球体间的几何距离,并与半径和比较。可以采用距离的平方进行相关计算,从而避免计算成本较高的平方根运算。
bool Collision(Sphere a, Sphere a) {
Vector d = a.c – b.c;
float dist = Dot(d, d);
float radiusSum = a,r + b.r;
return dist <= radiusSum * radiusSum;
}
这里你可能需要了解Dot的几何意义,Dot(Vector a, Vector b)即a与b的点积,若b为单位向量,则是a在b方向上的投影。
关于包围球的计算在此不进行详细说明。
方向包围盒是一个长方体,类似于AABB,但是具有方向性。存在多种OBB表达方式:8个顶点的点集、6个面的面集、三个平行面集合、一个顶点和3个彼此正交的边向量,以及中心点、一个旋转矩阵和3个1/2边长。通常最后一种表达方式最为常用,其测试算法基于分离轴理论。
结构显示如下:
//region R = x | x = c + r * u[0] + s * r[1] + t * u[2], |r| <= e[0], |s| <= e[1], |t| <= e[2]
struct OBB {
Point c; //OBB center point
Vector u[3]; //Local x-, y-, z-axes
Vector e; //Positive halfwidth extents of OBB along each axis
};
OBB无疑是包围体中内存消耗较大的一类。为节省内存开销,可以只存储旋转矩阵中的两个轴,第三轴仅在测试时利用向量叉积进行计算。相对来讲,这能够节省CPU的操作开销,同时还能节省3个浮点数分量。
OBB间的相交测试;
OBB相交测试可以采用“分离轴测试”加以实现。对于某一轴L,若两盒投影半径之和小于中心点间投影距离,则OBB处于分离状态。如下图:
对于OBB,2D最多测试四个分离轴(A的两个坐标轴,B的两个坐标轴),3D最多测试15个分离轴(A的三个、B的三个,以及垂直于每个轴的9个轴)。若在所有轴上均不重叠,则不相交,否则在任一轴上重叠则判定为相交且退出测试。
可将B转换至A的坐标系统中从而减少操作数量。设t为B至A的位移向量,R为将B转换(投影)至A坐标系中的旋转矩阵,则在L轴上测试如下:
需注意的是,为使测试更加高效,各轴测试顺序应按表中内容执行。
bool Collision(OBB &a, OBB &b) {
float ra, rb;
Matrix33 R, AbsR;
//Compute rotation matrix expressing b in a’s coordinate frame
for(int i = 0; j < 3; j++) {
for(int j = 0; j < 3; j++)
R[i][j] = Dot(a.u[i], b.u[j]);
}
//Compute translation vector t
Vector t = b.c - a.c;
//Bring translation into a’s coordinate frame
t = Vector(Dot(t, a.u[0]), Dot(t, a.u[1]), Dot(t, a.u[2]));
//Compute common subexpressions. Add in an epsilon term to
//counteract arithmetic errors when tow edges are parallel and
//their cross product is (near) null
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++)
AbsR[i][j] = Abs(R[i][j]) + EPSILON;
}
//Test axes L = A0, L = A1, L = A2
for(int i = 0; i < 3; i++) {
ra = a.e[i];
rb = b.e[0] * AbsR[i][0] + b.e[1] * AbsR[i][1] + b.e[2] * AbsR[i][2];
if(Abs(t[i]) > ra + rb) return false;
}
//Test axes L = B0, L = B1, L = B2
for(int i = 0; i < 3; i++) {
ra = a.e[0] * AbsR[i][0] + a.e[1] * AbsR[i][1] + a.e[2] * AbsR[i][2];
rb = b.e[i];
if(Abs(t[0] * R[0][i] + t[1] * R[1][i] + t[2] * R[2][i]) > ra + rb) return false;
}
//Test axis L = A0 x B0
ra = a.e[1] * AbsR[2][0] + a.e[2] * AbsR[1][0];
rb = b.e[1] * AbsR[0][2] + b.e[2] * AbsR[0][1];
if(Abs(t[2] * R[1][0] - t[1] * R[2][0]) > ra + rb) return false;
//Test aixs L = A0 x B1
ra = a.e[1] * AbsR[2][1] + a.e[2] * AbsR[1][1];
rb = b.e[0] * AbsR[0][2] + b.e[2] * AbsR[0][0];
if(Abs(t[2] * R[1][1] - t[1] * R[2][1]) > ra + rb) return false;
//Test aixs L = A0 x B2
ra = a.e[1] * AbsR[2][2] + a.e[2] * AbsR[1][2];
rb = b.e[0] * AbsR[0][1] + b.e[1] * AbsR[0][0];
if(Abs(t[2] * R[1][2] - t[1] * R[2][2]) > ra + rb) return false;
//Test aixs L = A1 x B0
ra = a.e[0] * AbsR[2][0] + a.e[2] * AbsR[0][0];
rb = b.e[1] * AbsR[1][2] + b.e[2] * AbsR[1][1];
if(Abs(t[0] * R[2][0] - t[2] * R[0][0]) > ra + rb) return false;
//Test aixs L = A1 x B1
ra = a.e[0] * AbsR[2][1] + a.e[2] * AbsR[0][1];
rb = b.e[0] * AbsR[1][2] + b.e[2] * AbsR[1][0];
if(Abs(t[0] * R[2][1] - t[2] * R[0][1]) > ra + rb) return false;
//Test aixs L = A1 x B2
ra = a.e[0] * AbsR[2][2] + a.e[2] * AbsR[0][2];
rb = b.e[0] * AbsR[1][1] + b.e[1] * AbsR[1][0];
if(Abs(t[0] * R[2][2] - t[2] * R[0][2]) > ra + rb) return false;
//Test aixs L = A2 x B0
ra = a.e[0] * AbsR[1][0] + a.e[1] * AbsR[0][0];
rb = b.e[1] * AbsR[2][2] + b.e[2] * AbsR[2][1];
if(Abs(t[2] * R[0][0] - t[0] * R[1][0]) > ra + rb) return false;
//Test aixs L = A2 x B1
ra = a.e[0] * AbsR[1][1] + a.e[1] * AbsR[0][1];
rb = b.e[0] * AbsR[2][2] + b.e[2] * AbsR[2][0];
if(Abs(t[1] * R[0][1] - t[0] * R[1][1]) > ra + rb) return false;
//Test aixs L = A2 x B2
ra = a.e[0] * AbsR[1][2] + a.e[1] * AbsR[0][2];
rb = b.e[0] * AbsR[2][1] + b.e[1] * AbsR[2][0];
if(Abs(t[1] * R[0][2] - t[0] * R[1][2]) > ra + rb) return false;
//Since no separating axis is found, the OBBs must be intersecting
return true;
}
关于 OBB 的计算在此不进行详细说明。