AABB

Axis Aligned Bounding Box (AABB)

在有关物体碰撞,相交检测的时候,我们往往使用一些简单的几何体来替代复杂的几何体,通过近似的方法来决定场景中的相交情况。实际中有很多类似几何体,比如球就会往往作为粗略碰撞检测的第一步近似,而该文章的AABB就是其中一种方法。

AABB——即使用边界与轴平行的长方体去包围一个复杂几何体。

uTools_1678966789855

上图是一个2D的AABB包围盒,可以看出能够决定一个2DAABB的关键点就是它的左下角和右上角。

所以类似地,如果我们抽象一个3D的AABB类,它的数据结构应该是:

struct AABB{
	Vector3 min_corner;
	Vector3 max_corner;
};

min_corner代表着AABB盒的左下角(即更靠近原点的一角,它的各个坐标值都是最小的)。同理,max_corner代表着AABB的右上角。

有了数据结构我们就可以进行实际性的操作了。

如果我们已知一个复杂几何体,如何求出它的AABB盒?

那在计算机中,一个几何体的表示方法是由一个点集构成的,所以该问题就变成了给定一个点集,求它的AABB盒。

uTools_1678967444956

现在我们需要调整AABB盒,让它也将P点囊括进去。很明显,我们只需要调整它的右上角的位置就可以了。而且不难理解,为了囊括新加入的点,我们需要取AABB角的最大值最小值与P点比较并更新,所以。

struct AABB{
	void merge(const Vector3& p){
		min_corner = Min(p, min_corner);
		max_corner = Max(p, max_corner);
	}
	
	//fields
	Vector3 min_corner;
	Vector3 max_corner;
};

Min()和Max()是取各个分量的最小值和最大值。

同样的,假如我想将另外一个AABB包围进来,生成一个新的AABB。我们可以对AABB的两个顶点都做一次merge操作。不过因为我们已知AABB顶点的位置关系,所以我们可以简化一下。

struct AABB{
	void merge(const Vector3& p){
		min_corner = Min(p, min_corner);
		max_corner = Max(p, max_corner);
	}
	
	void merge(const AABB& b){
		//min_corner = Min(b.max_corner, min_corner);
		min_corner = Min(b.min_corner, min_corner);
		max_corner = Max(b.max_corner, max_corner);
		//max_corner = Max(b.max_corner, max_corner);
	}
	
	//fields
	Vector3 min_corner;
	Vector3 max_corner;
};

所以假定有一个点集set,求M的AABB:

AABB MakeAABB(const std::vector<Vector3>& set){
	AABB boundingBox;
	for(const auto& p : set){
		boundingBox.merge(p);
	}
	return boundingBox;
}

使用AABB时注意当前AABB究竟应该和哪个坐标系对齐。

uTools_1678968416128

如果我想将本来与红色坐标系对齐的AABB转换到蓝色坐标系下,应该如何转变。

思路其实也很简单,我们将本来在红色坐标系下的AABB的坐标转到蓝色坐标系下,然后在蓝色坐标系下进行merge操作,这样我们就得到了蓝色坐标系下的AABB盒。

struct AABB{
	void merge(const Vector3& p){
		min_corner = Min(p, min_corner);
		max_corner = Max(p, max_corner);
	}
	
	void merge(const AABB& b){
		//min_corner = Min(b.max_corner, min_corner);
		min_corner = Min(b.min_corner, min_corner);
		max_corner = Max(b.max_corner, max_corner);
		//max_corner = Max(b.max_corner, max_corner);
	}
	
	/*这里将Matrix4x4的参数解释为TRS矩阵,实际上你可以随意更改它的含义,只要最后能做到类似将红色坐标系坐标转到蓝色坐标系即可*/
	void transformTo(const Matrix4x4& TRS){
		Vector3 corners[8] = {
			{min_corner.x, min_corner.y, min_corner.z},
			{min_corner.x, max_corner.y, min_corner.z},
			{max_corner.x, min_corner.y, min_corner.z},
			{max_corner.x, max_corner.y, min_corner.z},
			{min_corner.x, min_corner.y, max_corner.z},
			{min_corner.x, max_corner.y, max_corner.z},
			{max_corner.x, min_corner.y, max_corner.z},
			{max_corner.x, max_corner.y, max_corner.z}
		};
		Matrix4x4 invTRS = TRS.inverse();
		min_corner = {FLT_MAX, FLT_MAX, FLT_MAX};
		max_corner = {-FLT_MAX, -FLT_MAX, -FLT_MAX};
		for(int i{0};i<8;++i){
			corners[i] = Vector3{invTRS * Vector4{corners[i], 1.f}};
			merge(corners[i]);
		}
	}
	
	//fields
	Vector3 min_corner;
	Vector3 max_corner;
};

需要注意的是,每次转换新坐标系都会使AABB变得更大一圈,如果连续变换就会导致AABB比其包裹的几何体大出几倍。所以在使用变换前我们最好将连续的变换合成起来,然后对AABB进行变换。

了解了如果生成和变换AABB,接下来就是相交检测。

AABB与AABB的相交检测非常简单,只需要判断两个AABB顶点的位置关系即可:

struct AABB{
	//....
    
	bool isIntersect(const AABB& b){
		return 
            b.min_corner.x <= max_corner.x && 
            b.min_corner.y <= max_corner.y && 
            b.min_corner.z <= max_corner.z &&
            b.max_corner.x >= min_corner.x &&
            b.max_corner.y >= min_corner.y &&
            b.max_corner.z >= min_corner.z;
	}
	
	//fields
	Vector3 min_corner;
	Vector3 max_corner;
};

于射线的检测就有些技巧了。我们通过二维平面讲解其思想。

一个2D射线可以通过原点\(O(x_o,y_o,z_o)\)和方向\(\vec n = (x_n,y_n,z_n)\)来表示。

一个2DAABB盒通过\(min(x_{min}, y_{min})\)\(max(x_{max},y_{max})\)表示。

射线和AABB相交检测可以通过联立射线参数方程和AABB边方程来求交点,然后查看交点是否在边界内。

uTools_1678971903286

现在假设有两条射线,他们分别与AABB边界延长线有四个交点,我们可以判断这四个交点是否在AABB上,如果在则相交,如果都不在则相离。

射线的参数方程可以写作\(l = O + t\vec n\),在求交点时t的结果可能小于0,但是没关系,剔除掉就好了。

2D情况尚可,3D下不仅要与6个平面进行计算,而且每个交点都需要与至少2个顶点进行位置判断,比较费力。于是我们更改一下算法,原理是这样的。

假设我们已知射线上的四个交点。

uTools_1678972448359

t0和t1是与两个y边界相交,t2和t3与两个x相交。我们通过交点位置对该AABB进行还原。

uTools_1678972603519

可以看到还原后的AABB在射线之外,也就是说射线交点位置其实决定了AABB和射线的关系,这样我们就可以通过t值来判断AABB和射线是否相交。

现在假设我们可以随意移动t2和t3的位置(保持相对位置)。当t2跑到t1和t0之间时,AABB就会与射线相交。

当情况升上3D空间时也是如此。现在射线有六个交点了,六个交点可以分成三组,每组都是射线与两个平行面相交产生的交点的t值对,我们一步一步分析该如何写这个算法。

假设我们先让射线同两个x平面联立,求出两个交点。

uTools_1678973098558

这两个交点我们求出\(t_{min}\)\(t_{max}\),这两个值暂时圈定了AABB包围盒的范围,我们之后每一次相交测试都是同这两个值进行判断。

现在我们对射线同两个y平面联立,得到两个t值。

uTools_1678973293505

新的t2和t3同之前的t1,t0生成了一个2DAABB,再一次框定了3DAABB的范围。与此同时,两组t值代表的范围产生了重叠,射线与该2DAABB相交。这时我们需要更新\(t_{min}\)\(t_{max}\),好让下一组t值进行判断。问题在于我们如何更新?

注意,我们此时的2DAABB限制了3DAABB的范围。你可以把这个2DAABB代表的空间想象成一个电梯井,而真正的3DAABB是井内的电梯,我们接下来要对z平面进行相交测试就是想知道该射线是否穿过了电梯。而且,我们知道了t2和t1的值,也就是井内射线的样子。所以接下来我们应该将z平面求得的t值对同井内的射线进行相交判断,如果相交,说明3DAABB被井内射线穿过,否则没相交。

举个例子,假设我们发现z平面联立的结果是这样的。

uTools_1678973717689

t4和t5是z平面联立的结果,我们转换一个视角。

uTools_1678974568102

(不知道够不够直观)真正的AABB盒我用绿色实线画了出来,四个交点位置如图所示,t4和t5的位置都落在了的AABB的z平面的延长面上造成了如上图所示的结果,如果我们移动t4和t5的位置,我们需要把它们代表的范围与t2和t1范围相交,这样AABB才能与射线相交。

依据这样的原理,我们可以将射线同每一组平行边进行联立求一对t值,然后判断这一对t值代表的范围是否与当前相交t值对产生交叉。

struct AABB{
	//...
	
	bool isIntersect(Ray r){
		float tmin = 0.f;
		float tmax = 0.f;
		for(int i{0};i<3;++i){
			if(std::fabs(r.direction[i]) <= FLT_EPSILON){
				if(!(r.origin[i] >= min_corner[i] && r.origin[i] <= max_corner[i])){
					return false;
				}
				continue;
			}
			float t1 = (min_corner[i] - r.origin[i]) / r.direction[i];
			float t2 = (max_corner[i] - r.origin[i]) / r.direction[i];
			if(t1 > t2) std::swap(t1,t2);
			if(t1 > tmin) tmin = t1;
			if(t2 < tmax) tmax = t2;
			if(tmin > tmax or tmax < 0.f) return false;
		}
		return true;
	}
	
	//fields
	Vector3 min_corner;
	Vector3 max_corner;
};
posted @ 2023-03-16 22:36  ᴮᴱˢᵀ  阅读(109)  评论(0)    收藏  举报