第四章:包围体
- 引言-包围体
- (1)包围体测试和几何体测试
- (2)包围体测试的代价和作用
- (3)相交测试的优化
- (4)包围体相关章节和主旨
- 一、BV 期望特征
- 1.1 有效的包围体
- 1.2 包围体的紧密度和测试耗费
- 1.3 包围体的生成与类型选取
- 1.4 包围体讨论方式
- 二、轴对齐包围盒
- 2.1 轴对齐包围盒的定义
- (1)AABB的定义
- (2)AABB的三种定义方式
- (3)AABB定义方式的比较
- 2.2 AABB间的相交测试
- (1)"最小值-最大值"的相交测试
- (2)"最小值-直径"的相交测试
- (3)"中心-半径"的相交测试
- (4)优化建议
- 2.3 AABB的计算与更新
- (1)统一至局部坐标或世界坐标
- (2)局部坐标与世界坐标的计算精度
- (3)局部坐标与世界坐标的更新机制
- (4)非对齐包围体的更新与重构
- 2.4 基于包围球的AABB
- (1)固定尺寸的AABB
- (2)固定尺寸AABB的优点
- 2.5 基于原点的AABB重构
- (1)各方向的最大顶点距离
- (2)算法优化
- 2.6 爬山法构造AABB
- (1)爬山算法
- (2)时间复杂度与健壮度
- 2.7 旋转AABB后的重计算
- (1)重对齐AABB
- (2)重对齐的计算原理
- (3)算法代码
- 2.8 轴对齐包围盒总结
- (1)AABB是什么、如何定义
- (2)AABB的计算
- (3)AABB的更新
- (4)AABB计算方法之间的比较
- 三、球体
- 3.1 包围球的相交测试
- 3.2 计算包围球
- (1)简单构造算法
- (2)增量算法
- 3.3 最大离散方向上的包围球
引言-包围体
(1)包围体测试和几何体测试
- 直接对两个物体的几何体执行碰撞检测的代价非常高昂,特别是物体包含成百上千个多边形时。
- 为了减少计算消耗,在几何体相交测试前通常先执行物体的包围体相交测试。
- 包围体(BV:Bounding volume)是一个简单的体空间,可以包围一个或多个具有复杂形状的物体。
- 与具有复杂形状的物体相比,包围体测试的速度更快。因此利用包围体可以执行快速的剔除测试,因为只有当包围体产生碰撞时,才有可能和必要进一步计算具有复杂形状的几何体测试。
(2)包围体测试的代价和作用
- 当物体对象之间真正产生碰撞时,包围体测试是多余的。因为包围体测试后还要进行几何体测试,从而精确的判断是否真正碰撞。然而在大多数情况下只有少数物体才会彼此靠近并且产生碰撞,因此使用包围体通常可以得到有效的性能改善,所以为包围体测试付出一些代价是值得的。
(3)相交测试的优化
- 某些应用程序只使用包围体测试就足以完成碰撞检测了。
- 削减物体包含的多边形数量可以提升碰撞检测的效率,这样就可以减少对包围体内部冗余的多边形进行测试。(第6章主旨内容)
- 物体对象A、B之间多边形测试的时间复杂度一般是O(n²)。因此如果待检测多边形的数量能减少一半,则相应的工作量将减少75%。
(4)包围体相关章节和主旨
- 第6章中,依据包围体的层次结构,可以将参与测试的物体对象和多边形数量减少至最低值。
- 本章中主要讨论同一类型包围体之间的相交测试。
- 不同类型的包围体之间也常常需要执行相交测试,第5章将讨论异类型包围体(BV)之间的相交测试。
一、BV 期望特征
1.1 有效的包围体
- 不是所有几何对象都可以成为有效的包围体,包围体的期望特征包括:
1.低代价的相交测试。
2.实现紧密拟合 (紧密包围几何体)。
3.计算耗费较少。
4.易于旋转和变换。
5.内存占用较少。 - 包围体的核心理念为:优先使用代价低廉的测试类型,并且尽量使测试提早退出 (即根据某些条件,判断一些情况下一定相交或者不相交)。
1.2 包围体的紧密度和测试耗费
- 如果包围体紧密度过低,当包围体测试结果为相交时,几何体测试结果可能并不相交,因此包围体测试再高效也没有意义。
- 如果包围体紧密度过高,使用成百上千个三角形对物体进行拟合,那么相交测试的效率非常低,失去了使用包围体的意义。
- 为了使得包围体尽可能高效,包围体形状应该尽量与物体对象之间实现较紧密的拟合,并且在紧密拟合与测试耗费之间形成某种平衡。
1.3 包围体的生成与类型选取
- 相交测试不应局限于同类型的包围体之间,并且相交测试一个包含以下计算查询:点包含、光线-体相交测试、面-多边形相交测试。
- 一般情况下包围体的计算采用预处理方式而非实时计算。但是当物体移动时,其包围体可能需要实现空间对齐。因此如果包围体的计算代价高昂,则重对齐包围体就是一个更好的方法。
- 几何体和包围体数据都需要占用内存。
- 包围体的期望特征值之间是存在互斥性的(就像价格和品质不可兼得一样),因此没有任何一种包围体是最好的,每种包围体适用于不同的情况。对于特定的应用场景,可以针对不同的包围体执行相关测试,确定最终较好的包围体类型。
- 下图为5种常用类型的包围体之间的比较:
1.4 包围体讨论方式
- 接下来将介绍各种类型的包围体。先介绍某类包围体是什么、如何定义,再讨论如何进行这类包围体之间的相交测试,最后讨论针对一个点集如何构造和更新这类包围体。
二、轴对齐包围盒
2.1 轴对齐包围盒的定义
(1)AABB的定义
- 轴对齐包围盒(AABB)是应用最为广泛的包围体之一。在3D空间中,AABB是一个6面盒状长方体(在2D空间中是包含四条边的矩形),且其面法线皆平行于给定的坐标轴。
- AABB的最大特点是能够实现快速的相交测试,即仅执行相应坐标值之间的比较。
(2)AABB的三种定义方式
- AABB存在3种常规表达方式:
1.坐标值上最小值和最大值
struct AABB
{
Point min; //长方体坐标最小值
Point max; //长方体坐标最大值
};
2.最小顶点值和直径范围:
struct AABB
{
Point min; //长方体坐标最小值
float d[3]; //直径范围:dx、dy、dz, max=min+d。
};
3.中心点和轴向半径
struct AABB
{
Point c; //长方体中心点
float r[3]; //各轴向半径:rx、ry、rz,min=c-r,max=c+r。
};
(3)AABB定义方式的比较
- 若考虑存储需要,则"中心-半径"方式最为经济,不同于中心位置坐标需要使用三个float变量,半径只需要几个位。
- 虽然存在较小的差异,但"最小值-半径"方式基本与"中心-半径"方式相同。最坏情况是"最小值-最大值"方式,因为其存储的6个值都需要以相同的精确度存储,因此为了降低整体存储空间的大小,需要使用整数而非浮点数表示AABB。
- 如果物体只是以平移的方式运动,则与"最小值-最大值",另外两种表达方式将更加方便,因为它们需要更新6个参数中的3个。
- "中心-半径"方式的一个比较有用的性质是:可以把AABB直接作为一个包围球加以检测。
2.2 AABB间的相交测试
- AABB之间的相交测试非常直观并且无需考虑其表达方式。若两个AABB在3个轴上都相交,则AABB相交,而AABB沿每个轴的有效范围可以看作是对应轴上的数值区间。
(1)"最小值-最大值"的相交测试
int TestAABB_AABB(AABB a, AABB b)
{
// 当两个AABB在某一轴向上不相交时,两AABB不相交
if (a.max[0]<b.min[0] || a.min[0]>b.max[0])return 0;
if (a.max[1]<b.min[1] || a.min[1]>b.max[1])return 0;
if (a.max[2]<b.min[2] || a.min[2]>b.max[2])return 0;
// 当两个AABB在所有维度轴都相交时,两AABB才相交
return 1;
}
(2)"最小值-直径"的相交测试
int TestAABB_AABB(AABB a, AABB b)
{
// 当两个AABB在某一轴向上不相交时,两AABB不相交
float t; // 看不懂表达式就看这里↓
if ((t = a.min[0] - b.min[0]) > b.d[0] || -t > a.d[0])return 0; // a.min[0]-b.min[0] > b.d[0]
if ((t = a.min[1] - b.min[1]) > b.d[1] || -t > a.d[1])return 0; // 移项:a.min[0] > b.min[0]+b.d[0]
if ((t = a.min[2] - b.min[2]) > b.d[2] || -t > a.d[2])return 0; // 等价: a.min[0] > b.max[0]
// -t > a.d[0] 等价:b.min[0] > a.max[0]
// 当两个AABB在所有维度轴都相交时,两AABB才相交
return 1;
}
相比较而言"最小值-直径"方式不那么吸引人,因为即使以简洁的方式编写代码,操作符的数量仍多余"最大值-最小值"的相交测试。
(3)"中心-半径"的相交测试
int TestAABB_AABB(AABB a, AABB b)
{
// 当两个AABB在某一轴向上不相交时,两AABB不相交
if (Abs(a.c[0] - b.c[0]) > (a.r[0] + b.r[0]))return 0; // Abs(a.c[0] - b.c[0])表示两AABB中心点间的距离
if (Abs(a.c[1] - b.c[1]) > (a.r[1] + b.r[1]))return 0; // (a.r[0] + b.r[0])表示半径之和
if (Abs(a.c[2] - b.c[2]) > (a.r[2] + b.r[2]))return 0; // 当且仅当半径之和大于等于距离时,此维度相交
// 当两个AABB在所有维度轴都相交时,两AABB才相交
return 1;
}
(4)优化建议
在现代计算机体系结构中,函数Abs()一般转换为一条指令,否则可以通过判断浮点值二进制格式的符号位高效地实现该函数。
当AABB声明为整数而非浮点数时,上述测试可以替换为另一个版本。使用整数时,区间[A,B]和[C,D]可以按照下列表达式加以计算:
当C>B时无符号整数下溢左边的计算结果将变得不可预测,从而使该表达式产生错误。而上溢则可替代绝对值函数调用,是的"中心-半径"方式的测试如下所示:
int TestAABB_AABB(AABB a, AABB b)
{
// 当两个AABB在某一轴向上不相交时,两AABB不相交
int r;
r = a.r[0] + b.r[0]; if ((unsigned int)(a.c[0] - b.c[0] + r) > r + r)return 0;
r = a.r[1] + b.r[1]; if ((unsigned int)(a.c[1] - b.c[1] + r) > r + r)return 0;
r = a.r[2] + b.r[2]; if ((unsigned int)(a.c[2] - b.c[2] + r) > r + r)return 0;
// 当两个AABB在所有维度轴都相交时,两AABB才相交
return 1;
}
- PS:其实本人对上述讨论不是很理解,如果强制转换负数是无法实现绝对值效果的,那么表达式错误怎么还能使用呢?上述代码似乎使用了一些小技巧,但限于个人水平无法完全理解,如果您理解上文讨论和代码的含义,请在评论区不吝赐教~
- 利用整数也可以实现其他技巧,但其应用多依赖于平台的体系结构,如SIMD指令用几条代码即可实现AABB测试。
- 最后,对于需要大量执行相交测试的碰撞检测系统,可以根据状态的相似性对测试进行排序。例如:如果相应操作基本出现在xz平面,则最后执行y坐标测试,从而使状态的变化降低到最小程度。
2.3 AABB的计算与更新
(1)统一至局部坐标或世界坐标
- 包围体常定义于局部模型空间,为了执行两个包围体之间的相交测试,需要将它们转换到统一的坐标系统。有两种方案:
1.将两个包围体全部转换为世界坐标空间系统
2.将一个包围体转换为另一个包围体的局部坐标系。 - 局部空间转换的优势在于:只需执行一次世界坐标空间的转换,就能够产生更紧凑的包围体。下图中阐述了这一概念,三幅图进行了不同的转换产生了不同的碰撞检测结果。
(2)局部坐标与世界坐标的计算精度
- 如果将包围体转换至另一个包围体的局部坐标空间,则精度是需要考虑的一个问题。如果采用世界坐标空间执行测试,可能会将物体移至距离原点较远处。因此,包围体靠近原点的坐标转换时附加的平移操作将会使原值出现精度损失(丢位)。
- 然而当测试位于局部空间时,对象总是处于原点位置,因而能够保持计算精度。需注意的是,通过调整平移操作,可以使得被转换物体的原点居中,则世界坐标转换也能够维护相应的精度。
(3)局部坐标与世界坐标的更新机制
- 当采用世界坐标系转换时,对于某一给定空间内的全部包围体,只需要进行一次转换即可将所有包围体转换到同一空间。
- 当采用转换至某一物体的局部空间时,由于全部变化只影响到当前新物体或者是新的目标坐标系统,所以缓存已更新的包围体没有任何用处。
- 因此当采用世界坐标系转换时,可以将每个物体更新的包围体进行临时缓存。采用局部空间时进行缓存没有意义。
(4)非对齐包围体的更新与重构
- 某些包围体(如球体和凸体)可以直接转换至任意一种坐标系统中,因为它们不受限于特点的方向,并称之为非对齐或自由定向的包围体。
- 相反,对齐的包围体(如AABB)则依赖于其既定的方向。如果对齐的包围体在运动过程中存在旋转,且使得包围体处于非对齐状态,则需要重新对齐包围体或重新构建包围体。
- AABB的更新和重构的4种方法为:
1.使用固定尺寸且较为松散的AABB包围物体对象。
2.基于原点确定动态且紧凑的的重构方式。
3.利用爬山法确定动态且紧凑的重构方式。
4.基于旋转后的AABB,确定近似且动态的重构方式。 - 上述原则的内容将在后续内容中介绍。
2.4 基于包围球的AABB
(1)固定尺寸的AABB
- 第一种重新构造AABB的方法是:通过在任意方向上全包围物体,构造出一个固定尺寸的AABB。
- 当我们使用"中心-半径"方式定义AABB时,我们以中心点为圆心、以三个轴中的最大半径值为半径,就可以得到一个类似下图的包围球。
- 上图中物体A的包围球的外接立方体即为基于包围球的AABB,这个AABB如果依旧以"中心-半径"的方式定义,则其中心不变,而所有半径值都变为原来三个半径中的最大值。
- 我们可以选取任意点为中心点,但仅当选取物体的中心为AABB的中心点时,当前球体的半径才最小。
(2)固定尺寸AABB的优点
- 该方法的优点是在更新AABB的过程中仅需考虑平移变化,而可以忽略旋转变化。因为无论如何旋转,基于包围球的AABB都会完全包围住物体。因此我们只需要构造一次物体的AABB,即可固定物体AABB的尺寸。
- 当然我们可以直接使用包围球A作为物体的包围体,包围球也仅需考虑平移而不考虑旋转,并且我们可以看到包围球的拟合更加紧密。
2.5 基于原点的AABB重构
(1)各方向的最大顶点距离
- 本节所描述的更新策略是在AABB与各坐标值重对齐时,动态的调整原AABB的尺寸。
- 对于一个紧密拟合的包围体,此策略将检测其底层几何数据,同时沿着坐标轴的6个方向搜索端点从而构造AABB的包围数据。
- 遍历物体的全部顶点,并记录各个方向向量上的最大顶点距离,这是一种简单直接的方法。
- 最大顶点距离可以通过顶点向量在方向向量上的投影得到,由于需要进行比较,这里不必规范化方向向量。
- 算法如下:
void ExtremePointsAlongDirection(Vector dir, Point pt[], int n, int* imin, int* imax)
{
float minproj = FLT_MAX, maxproj = -FLT_MAX;
for (int i = 0; i < n; i++)
{
// 计算每个点在向量dir上的投影值
float proj = Point::Dot(pt[i], dir);
// 记录向量dir上的最远点索引
if (proj < minproj)
{
minproj = proj;
*imin = i;
}
if (proj > maxproj)
{
maxproj = proj;
*imax = i;
}
}
}
(2)算法优化
- 尽管该算法复杂度为O(n),但当n值较大时依旧需要付出高昂的代价。可通过忽略冗余顶点来加速该算法,这一方案基于下列事实:只有凸体上的顶点才会影响包围体形状,如下图所示。
- 在预处理过程中,相对于其他冗余顶点,凸体上的全部k个顶点将被提前存储,只检查这k个最初的顶点,即可构造紧凑的AABB。
- 对于一般性凹体,该方法具有一定优势。但针对凸体,其全部顶点已位于凸体之上,该方案没有意义。
- 通过额外的特点预处理搜索结构,确定端点的时间消耗可降到O(logn)。例如Dobkin-Kirkpatrick层次结构(第九章内容)便用于此目的。
- 但该结构将占用额外的内存空间并且存在遍历时间消耗,这在大多数情况下将是导致性能下降的直接原因。但如果紧凑包围体在计算中确实非常重要,可以考虑它而不考虑AABB。
2.6 爬山法构造AABB
(1)爬山算法
- 这种方法可以加速AABB重对齐过程,它的原理是:快速计算物体顶点的邻接顶点。该方法使用简单的爬山法即可确定新AABB的各端点。
- 这种方法不记录各轴向上的最小最大值,而是维护6个顶点指针,指针指向各轴向上物体的端点。
(2)时间复杂度与健壮度
- 爬山法将一个参考顶点和它的邻接顶点进行比较,并查看参考点是否仍为之前同一方向上的端点,如果不是,则端点替换为新的邻接顶点,重复该过程直至获得该方向上的最终端点。为了避免在局部最小值处阻塞,爬山法要求物体对象为凸体。因此爬山法是基于非凸对象预计算后的凸体算法。综上所述,这种紧凑型AABB的计算方法是一种常量时间操作。
- 当爬山算法确实减少了相应的计算量时才考虑转换顶点。然后相关实现可以进一步改善该算法:在某一给定轴上搜索端点时,可以仅使用x、y、z分量中的一个。例如:当在+x轴上获取端点时,只需要计算被转换顶点的x分量,使得转换产生的消耗减少了2/3。
- 为了实现健壮的爬山算法,还需要考虑其他一些问题。考察下列情况:沿着任意轴上的一个端点以及围绕该轴的若干共面顶点。此时如果物体对象围绕其余两轴中的任一轴旋转180°,则该端点将位于同一轴上的反方向上,由于该端点存在某些共面顶点,则爬山法无法搜索更佳的邻接顶点,且止于当前搜索方向上的(最少)端点处。健壮的实现必须考虑这种特殊情况:在预处理时可以先剔除共面顶点,具体内容将在12章讨论。关于搜索端点的问题将在9.5.4小节中讨论。
2.7 旋转AABB后的重计算
(1)重对齐AABB
- 考虑前述4个AABB重构原则中的最后一项。
- 最简单的方法是利用一个新的AABB包围旋转后的AABB,但只是形成了一个逼近的AABB而非紧凑的AABB。当结果AABB大于初始AABB时,需要注意的是,应在初始AABB的局部空间内计算旋转,否则针对上一个旋转后的AABB,重复旋转计算将使得AABB无线增长。
(2)重对齐的计算原理
- 考察一个轴对齐的包围盒A,且旋转矩阵M作用于A上,结果将是具有某一方向的旋转包围盒A。旋转矩阵的3个列(或行,取决于矩阵的有限规则)给出了A中的1世界坐标轴(若向量为列向量且右乘矩阵,则矩阵M的列为坐标轴;若向量为行向量且左乘矩阵,则矩阵M的行为坐标轴)。
- 假设矩阵为行向量且左乘矩阵,则矩阵M的行为坐标轴。设点坐标为p,则坐标变化计算式为:Mp。
设Mp=q,则q.x=M(0,0)*p.x+M(0,1)*p.y+M(0,2)*p.z。 - 设A为局部坐标系下的初始AABB,设B为A重对齐后的AABB。分析q.x的计算式,其中M是已知即确定的,p.x位于[A.min.x,A.max.x]范围内,因此M(0,0) * p.x位于M(0,0) * [A.min.x,A.max.x]范围内。式子中第二三项的范围也同理,由此我们可以确定q.x的范围,即可得到B的x范围,q.y和q.z同理。
- B的最大顶点的x坐标计算表达式如下:
(3)算法代码
void UpdateAABB(AABB a, float m[3][3], float t[3], AABB& b)
{
for (int i = 0; i < 3; i++)
{
b.min[i] = b.max[i] = t[i]; // t为位移变化值
for (int j = 0; j < 3; j++)
{
float e = m[i][j] * a.min[j];
float f = m[i][j] * a.max[j];
if (e < f)
{
b.min[i] += e;
b.max[i] += f;
}else
{
b.min[i] += f;
b.max[i] += e;
}
}
}
}
- 如果采用"中心-半径"的AABB,则旋转后重对齐包围盒算法代码为:
void UpdateAABB(AABB a, float m[3][3], float t[3], AABB& b)
{
for (int i = 0; i < 3; i++)
{
b.c[i] = t[i];
b.r[i] = 0.0f;
for (int j = 0; j < 3; j++)
{
b.c[i] += m[i][j] * a.c[j];
b.r[i] += Abs(m[i][j]) * a.r[j];
}
}
}
- 需要注意的是,计算旋转后的AABB等价于计算一个有向包围盒(有向包围盒及其相交测试将在后续章节讨论)。有向包围盒和AABB的区别在于有向包围盒需要存储方向矩阵,并且有向包围盒需要执行矩阵乘法计算,得到有向包围盒的旋转矩阵和转换矩阵M的组合矩阵。使用有向包围盒的优点是:针对初始的有向包围盒,重构的轴对齐包围盒紧凑度更高。同时对于有向盒体而言,这种轴对齐的测试方式也更加简介、高效。
2.8 轴对齐包围盒总结
(1)AABB是什么、如何定义
- 在本节我们学习了AABB包围盒,它就是个长方体、可用三种方式定义。相交测试就是测试长方体在每个维度的边是否相交。包围体的构造方法:遍历所有顶点一遍,记录每个维度上的最大最小值。
(2)AABB的计算
- 针对一个点集,我们求出各个轴上的最值,即minx、maxx、miny、maxy、minz、maxz,使用它们我们构造出两个点min(minx,miny,minz)和max(maxx,maxy,maxz),这即是AABB的"最大值-最小值"表示方式。
- 求各个维度的最大距离maxx-minx、maxy-miny、maxz-minz,即可以得到d[0]、d[1]、d[2],使用min为最小值点,这即是AABB的"最小值-直径"表达方式。
- 计算max和min的中点c,即c=(max+min)/2。再计算各个维度最大距离的一半:(maxx-minx)/2、(maxy-miny)/2、(maxz-minz)/2,即可以获得r[0]、r[1]、r[2]。这即是AABB的"中心-半径"表达方式。
(3)AABB的更新
- AABB旋转后需要重对齐,我们有四种方法:
第一种:使用基于包围球的AABB,这个AABB非常宽松无论物体绕着中心如何旋转都无法超出包围盒。因此我们只需要计算一次即可固定物体的AABB,以后无需进行重对齐等更新。
第二种:重新构造一个AABB,即每次重对齐时遍历所有旋转后的顶点,记录每个维度上的最大最小值。这个方法时间复杂度为O(n),以后需要重对齐,每次重对齐时都重新构造AABB。
第三种:重新构造一个AABB可以,但是我们可能无法接受O(n)的构造算法,那么我们就使用爬山法重新构造AABB,在一定条件下降低构造的时间复杂度。这个算法我也不是很懂,也没给代码,后面会给代码后面再看就是了,只需要记住和第二种是一类。
第四种:基于初始AABB进行重对齐。在局部坐标下我们算出一个初始的AABB,当物体旋转后AABB在相交测试前需要进行重对齐,我们基于AABB和旋转的信息构造出旋转后的AABB,这种算法需要9次循环,因此是常量时间。
(4)AABB计算方法之间的比较
- 鱼和熊掌不可兼得,包围盒的算法时间和紧密度是背道而驰的。第一种方法最简单最快但肯定是较为宽松的,第四种方法较快且较为紧密,第二和第三种方法是较慢但最为紧密的。毕竟计算出顶点集最小包围盒的标准方法就是:遍历所有顶点一遍记录各维度最值。
三、球体
- 与包围盒相比,球体是另一类较为常用的包围体。如同包围盒,球体也具备快速相交测试这一特征,同时球体不受旋转变化的影响,只需要简单的平移到物体所在的位置即可。
- 球体按照球心和半径的方式加以定义:
struct Sphere
{
Point c;
float r;
};
- 包围球的数据结构只使用4个float分量,因此包围球是一类节省内存的包围体。通常情况下,物体对象的中心或原点基本符合包围球的球心,因此只需存储上述结构中的半径分量。
- 与AABB一样,计算一个优化的包围球并非易事,下面几个小节将讨论集中计算包围球的相关算法,为了提高精度还将讨论最小包围球的计算方法。这一类非优化的逼近算法也适用于其他类型的包围体。
3.1 包围球的相交测试
- 包围球的相交测试非常简单,判断两球心之间的距离是否大于两球半径之和即可。其中计算距离后时可以直接使用距离的平方,和半径和的平方比较即可,这样可以避免成本较高的平方根操作。
- 算法如下:
int TestSphere(Sphere a,Sphere b)
{
Vector d = a.c - b.c;
float dist2 = Vector::Dot(d, d);
float radiusSum = a.r + b.r;
return dist2 <= radiusSum * radiusSum;
}
- 由于比较简单,上文并未给出Pint等类的实现,Point和Vector等类的实现可参考如下代码:
struct Vector
{
Vector(float vx, float vy, float vz) :x(vx), y(vy), z(vz) {}
Vector() :x(0), y(0), z(0) {}
static float Dot(const Vector& a,const Vector& b)
{
return a.x * b.x + a.y * b.y + a.z * b.z;
}
Vector operator-(const Vector& b)
{
return Vector(x - b.x, y - b.y, z - b.z);
}
float x, y, z;
};
typedef Sphere Point;
struct Sphere
{
Point c;
float r;
};
- 如果您想了解更多有关矩阵、向量运算的代码,可以参考此例的完整工程中的源代码部分。
- 与AABB相比,虽然球体多出几个算术操作,但其包含较少的分支语句以及较少的数据存取。因此在现代计算机体系结构中,球体测试可能比AABB测试稍快一点。但是对于这类简单的相交测试,比起算法的速度,数据计算的严谨性往往更加重要。
3.2 计算包围球
(1)简单构造算法
- 可通过计算顶点集的AABB获取一个逼近的包围球,即AABB的中心为包围球球心,球体的半径就是球心与最远顶点间的距离。
- 采用所有顶点的几何中心(顶点坐标平均值)将产生错误的包围球,这是因为各个顶点的权值(贡献度)不同。虽然该算法速度较快,但是适用性较差。
(2)增量算法
- 一种较好的算法可参见【Ritter90】,该算法初期尝试计算一个近似完整的包围球,并通过后续步骤完善该球体直至包围全部顶点。
- 该算法分两步执行,第一步:获取顶点集在坐标轴上的6个端点,选取距离最远的两个顶点的中点为球心,两顶点间距离的一半为球的半径。算法如下:
// 获取pt点集在最长维度上的两个端点min和max
void MostSeparatedPointsOnAABB(int& min, int& max, Point pt[], int numPts)
{
int minx = 0, maxx = 0, miny = 0, maxy = 0, minz = 0, maxz = 0;
for (int i = 0; i < numPts; i++)
{
if (pt[i].x < pt[minx].x)minx = i;
if (pt[i].x > pt[maxx].x)maxx = i;
if (pt[i].y < pt[miny].y)miny = i;
if (pt[i].y > pt[maxy].y)maxy = i;
if (pt[i].z < pt[minz].z)minz = i;
if (pt[i].z > pt[maxz].z)maxz = i;
}
float dist2x = Vector::Dot(pt[maxx] - pt[minx], pt[maxx] - pt[minx]);
float dist2y = Vector::Dot(pt[maxy] - pt[miny], pt[maxy] - pt[miny]);
float dist2z = Vector::Dot(pt[maxz] - pt[minz], pt[maxz] - pt[minz]);
min = minx;
max = maxx;
if (dist2y > dist2x && dist2y > dist2z)
{
min = miny;
max = maxy;
}
if (dist2z > dist2x && dist2z > dist2y)
{
min = minz;
max = maxz;
}
}
// 以求得的两个端点构造初始包围球
void SphereFromDistantPoints(Sphere& s, Point pt[], int numPts)
{
int min, max;
MostSeparatedPointsOnAABB(min, max, pt, numPts);
s.c = (pt[min] + pt[max]) * 0.5f;
s.r = Vector::Dot(pt[max] - s.c, pt[max] - s.c);
s.r = sqrt(s.r);
}
- 第二步:循环遍历全部顶点,当遍历到的顶点位于球体的外部时,将球体更新为:包含原球体和当前顶点的球体。即相对于原球心,新球体的直径将延申至外部顶点。算法如下:
void SphereOfSphereAndPt(Sphere& s, Point& p)
{
Vector d = p - s.c;
float dist2 = Vector::Dot(d, d);
if (dist2 > s.r * s.r)
{
float dist = sqrt(dist2);
float newRadius = (s.r + dist) * 0.5;
float k = (newRadius - s.r) / dist;
s.r = newRadius;
s.c += d * k;
}
}
void RitterSphere(Sphere& s, Point pt[], int numPts)
{
SphereFromDistantPoints(s, pt, numPts);
for (int i = 0; i < numPts; i++)
SphereOfSphereAndPt(s, pt[i]);
}
- 需要注意的是,如果初始构造的球体越逼近,那么最终计算出的球体会更加紧凑。如果构建一个更加逼近的初始球体将在下一节3.3中介绍。
- 如果你没有提前了解过包围球算法,那你可能对许多细节有疑惑,可以参考逐步深入包围球算法。
- 本节构造初始包围球是基于AABB的,但细节上略有不同。AABB中"中心-半径"的定义方式,中心指的是此点对于AABB,无论x、y、z都是中心,即c.x=(maxx-minx)/2、c.y=(maxy-miny)/2、c.z=(maxz-minz)/2。而上一节中选取初始包围球的中心点函数MostSeparatedPointsOnAABB,选取出来的是最长轴端点的索引,即如果选取出来的是x轴上的两个端点,那么c.x=(maxx-minx)/2、c.y=(imax.y+imin.y)/2、c.z=(imax.z+imin.z)/2。不需要深究,此处只是告诉大家区别,让大家在感到相似的同时不至于混淆。
3.3 最大离散方向上的包围球
- 这种方法的原理是:利用统计的方法分析顶点云确定最大离散方向。
- 上一节中我们选取点集在三个坐标轴中的最长方向构建球体,它的核心本质就是:选取一个方向,求点集在此方向上的两个端点,以两个端点中心为球体,以两端点距离的一半构造包围球。显而易见,选取任意一个方向我们都能构造出包围球,但是不同方向构建的包围球是不同的,因此初始包围球的构建问题就转换为了:轴的选取问题。
- 一种较好的方法是利用统计方法分析顶点云,求得最大离散方向,以此为轴。
- 正如数据集的平均值是数值居中趋势的一种计算方法,而方差则是计算数值的离中趋势(或称为离散度)的一种方法。下图展示了当选取不同轴时,点集离散度的不同。
- 均值u和方差σ²的计算方法如下所示:
- 方差的平方根记为标准差。对于沿某一轴上的离散值,方差可通过下列方式计算:首先得到数据值的平均值u,然后求得标准偏差的平方值,最后得到其平均值即方差值。算法如下: