1.包围盒描述
包围盒算法是一种求解离散点集最优包围空间的方法。
基本思想是用体积稍大且特性简单的几何体(称为包围盒)来近似地代替复杂的几何对象。
最常见的包围盒算法有AABB包围盒(Axis-aligned bounding box),包围球(Sphere),方向包围盒OBB(Oriented bounding box) 以及固定方向凸包FDH(Fixed directions hulls或k-DOP)。
2、AABB包围盒
AABB 包围盒就是采用一个长方体将物体包裹起来,进行两个物体的相交性检测时仅检测物体对应包围盒(包裹物体的长方体)的相交性。
另外,AABB 包围盒有一个重要特性,那就是包围盒对应的长方体每一个面都是与某个坐标轴平面平行的,因此,AABB 包围盒又称了 轴对齐包围盒 。
有了上述约定后,确定 AABB 包围盒就简单多了,仅需要记录 6 个值即可,这 6 个值分别代表包围盒在每个坐标轴上的最小值与最大值,即 xmin、xmax、ymin、ymax、zmin、zmax。
也就是说,实际物体上所有的点都必须满足以下条件:
另外,可以将表示 AABB 包围盒的 6 个参数分为以下两组:
其中,Pmin 是 3 个轴坐标最小值的集合,Pmax 是 3 个轴坐标最大值的集合。
知道了表示 AABB 包围盒的 6 个参数后就可以非常方便地求得 AABB 包围盒的几何中心,公式如下:
3、AABB 包围盒计算
了解了 AABB 包围盒的原理之后,求 AABB 包围盒 6 个参数的方法就简单多了,只需要对物体中的所有顶点坐标进行扫描,求出各个轴分量的最大值与最小值即可。
首先引入一个辅助类 Vector3f,其对象可以记录 3 个浮点数组成的三元组,用来表示物体的位置、速度等。
public class Vector3f {
float x;//三维变量中的x值
float y;//三维变量中的y值
float z;//三维变量中的z值
public Vector3f(float x,float y,float z)
{
this.x=x;
this.y=y;
this.z=z;
}
public void add(Vector3f temp)
{
this.x+=temp.x;
this.y+=temp.y;
this.z+=temp.z;
}
}
有了此辅助类之后,物体的位置、速度等就可以直接用此类的对象表示了。
接下来就是计算物体的 AABB 包围盒,定义一个对象 AABBBox 来表示包围盒。
public class AABBBox {
float minX;//x轴最小位置
float maxX;//x轴最大位置
float minY;//y轴最小位置
float maxY;//y轴最大位置
float minZ;//z轴最小位置
float maxZ;//z轴最大位置
public AABBBox(float[] vertices)
{
init();
findMinAndMax(vertices);
}
public AABBBox(float minX,float maxX,float minY,float maxY,float minZ,float maxZ)
{
this.minX=minX;
this.maxX=maxX;
this.minY=minY;
this.maxY=maxY;
this.minZ=minZ;
this.maxZ=maxZ;
}
//初始化包围盒的最小以及最大顶点坐标
public void init()
{
minX=Float.POSITIVE_INFINITY; // 正无穷
maxX=Float.NEGATIVE_INFINITY; // 负无穷
minY=Float.POSITIVE_INFINITY;
maxY=Float.NEGATIVE_INFINITY;
minZ=Float.POSITIVE_INFINITY;
maxZ=Float.NEGATIVE_INFINITY;
}
//获取包围盒的实际最小以及最大顶点坐标
public void findMinAndMax(float[] vertices)
{
for(int i=0;i<vertices.length/3;i++)
{
//判断X轴的最小和最大位置
if(vertices[i*3]<minX)
{
minX=vertices[i*3];
}
if(vertices[i*3]>maxX)
{
maxX=vertices[i*3];
}
//判断Y轴的最小和最大位置
if(vertices[i*3+1]<minY)
{
minY=vertices[i*3+1];
}
if(vertices[i*3+1]>maxY)
{
maxY=vertices[i*3+1];
}
//判断Z轴的最小和最大位置
if(vertices[i*3+2]<minZ)
{
minZ=vertices[i*3+2];
}
if(vertices[i*3+2]>maxZ)
{
maxZ=vertices[i*3+2];
}
}
}
//获得物体平移后的AABB包围盒
public AABBBox getCurrAABBBox(Vector3f currPosition)
{
AABBBox result=new AABBBox
(
this.minX+currPosition.x,
this.maxX+currPosition.x,
this.minY+currPosition.y,
this.maxY+currPosition.y,
this.minZ+currPosition.z,
this.maxZ+currPosition.z
);
return result;
}
}
在 AABBBox 的构造函数中需要传入物体的顶点序列,在这些序列中分别找出 x 、y、z 坐标的最小值和最大值,也可以直接传入包围盒的 6 个顶点参数。
如果在程序运行过程中,物体发生了移动,那就需要根据移动后的位置计算产生新包围盒对象,getCurrAABBBox方法需要传递物体移动后位置的 3 个坐标分量,用于与原始包围盒的 6 个 参数进行运算,产生移动后包围盒的 6 个参数。
4、AABB 包围盒的碰撞检测
在上面求得了物体的包围盒,求包围盒的目的就是为了简化物体运动过程中的碰撞检测,接下来就是介绍 AABB 包围盒碰撞检测的策略。
由于任何一个 AABB 包围盒的各个面都平行于坐标平面,因此判断两个 AABB 包围盒是否发生碰撞仅需要分别判断 3 个轴方向的交叠部分大小是否大于设定的阈值,若大于则发生了碰撞,否则没有发生碰撞。
具体碰撞检测代码如下:
public boolean check(RigidBody ra, RigidBody rb)//true为撞上
{
float[] over = calOverTotal
(
// 两个物体的 AABB 包围盒
ra.collObject.getCurrAABBBox(ra.currLocation),
rb.collObject.getCurrAABBBox(rb.currLocation)
);
// 三个方向的交叠值与设定的阈值进行比较
return over[0] > V_UNIT && over[1] > V_UNIT && over[2] > V_UNIT;
}
// 传入两个物体的 AABB 包围盒
public float[] calOverTotal(AABBBox a, AABBBox b) {
float xOver = calOverOne(a.maxX, a.minX, b.maxX, b.minX);
float yOver = calOverOne(a.maxY, a.minY, b.maxY, b.minY);
float zOver = calOverOne(a.maxZ, a.minZ, b.maxZ, b.minZ);
return new float[]{xOver, yOver, zOver};
}
// 计算每个轴方向的交叠值
public float calOverOne(float amax, float amin, float bmax, float bmin) {
float minMax = 0;
float maxMin = 0;
if (amax < bmax)//a物体在b物体左侧
{
minMax = amax;
maxMin = bmin;
} else //a物体在b物体右侧
{
minMax = bmax;
maxMin = amin;
}
if (minMax > maxMin) {
return minMax - maxMin;
} else {
return 0;
}
}
在 calOverTotal 方法里面要传入两个物体的 AABB 包围盒。
然后在 calOverOne 方法里面分别计算两个包围盒 3 个轴的交叠值,思路就是先比较两个AABB 包围盒在对应轴的最大分量,此比较是为了得出两个物体的方位,哪个物体在此轴的正方向那一侧。然后用在轴正方向的那一侧物体的坐标最小值减去在轴负方向那一侧的物体的最大值,如果为负数,则说明没有发生交叠,如果其值为正数,则说明两个物体发生了交叠,交叠值和设定的阈值进行比较。
5、光线与包围盒(AABB)的相交检测算法
5.1 Cocos2dx中实现Ray-AABB相交(碰撞)检测的算法
bool Ray::intersects(const AABB& aabb) const
{
Vec3 ptOnPlane; //射线与包围盒某面的交点
Vec3 min = aabb._min; //aabb包围盒最小点坐标
Vec3 max = aabb._max; //aabb包围盒最大点坐标
const Vec3& origin = _origin; //射线起始点
const Vec3& dir = _direction; //方向矢量
float t;
//分别判断射线与各面的相交情况
//判断射线与包围盒x轴方向的面是否有交点
if (dir.x != 0.f) //射线x轴方向分量不为0 若射线方向矢量的x轴分量为0,射线不可能经过包围盒朝x轴方向的两个面
{
/*
使用射线与平面相交的公式求交点
*/
if (dir.x > 0)//若射线沿x轴正方向偏移
t = (min.x - origin.x) / dir.x;
else //射线沿x轴负方向偏移
t = (max.x - origin.x) / dir.x;
if (t > 0.f) //t>0时则射线与平面相交
{
ptOnPlane = origin + t * dir; //计算交点坐标
//判断交点是否在当前面内
if (min.y < ptOnPlane.y && ptOnPlane.y < max.y && min.z < ptOnPlane.z && ptOnPlane.z < max.z)
{
return true; //射线与包围盒有交点
}
}
}
//若射线沿y轴方向有分量 判断是否与包围盒y轴方向有交点
if (dir.y != 0.f)
{
if (dir.y > 0)
t = (min.y - origin.y) / dir.y;
else
t = (max.y - origin.y) / dir.y;
if (t > 0.f)
{
ptOnPlane = origin + t * dir;
if (min.z < ptOnPlane.z && ptOnPlane.z < max.z && min.x < ptOnPlane.x && ptOnPlane.x < max.x)
{
return true;
}
}
}
//若射线沿z轴方向有分量 判断是否与包围盒y轴方向有交点
if (dir.z != 0.f)
{
if (dir.z > 0)
t = (min.z - origin.z) / dir.z;
else
t = (max.z - origin.z) / dir.z;
if (t > 0.f)
{
ptOnPlane = origin + t * dir;
if (min.x < ptOnPlane.x && ptOnPlane.x < max.x && min.y < ptOnPlane.y && ptOnPlane.y < max.y)
{
return true;
}
}
}
return false;
}
5.2 Slabs method (最常用的方法)
观察上述三幅图可以得出,只要发生区间交叠,光线与平面就能相交,
那么区间交叠出现的条件便是:光线进入平面处的最大t值小于光线离开平面处的最小t值
那么问题就变成了如何求 光线进入平面处的最大t值 以及 光线离开平面处的最小t值
这个问题很简单,通过光线与平面相交的参数方程求解就可以了,
光线的参数方程为R(t) = O + t * Dir
一般平面方程为aX+bY+cZ+d=0,因为AABB的六个面分别平行于XY、XZ、YZ平面,所以平面的方程为X=d,Y=d,Z=d
光线与垂直于x轴的两个面相交时,t = (d - O.x) / Dir.x
光线与垂直于y轴的两个面相交时,t = (d - O.y) / Dir.y
光线与垂直于z轴的两个面相交时,t = (d - O.z) / Dir.z
注意到t<0时,交点位于光线的起点之后,则光线(射线)并未与盒体发生相交
bool BBox::hit(const Ray& ray) const
{
double ox = ray.o.x;double oy = ray.o.y;double oz = ray.o.z;
double dx = ray.d.x;double dy = ray.d.y;double dz = ray.d.z;
double tx_min,ty_min,tz_min;
double tx_max,ty_max,tz_max;
//x0,y0,z0为包围体的最小顶点
//x1,y1,z1为包围体的最大顶点
if(abs(dx) < 0.000001f)
{
//若射线方向矢量的x轴分量为0且原点不在盒体内
if(ox < x1 || ox > x0)
return false ;
}
else
{
if(dx>=0)
{
tx_min = (x0-ox)/dx;
tx_max = (x1-ox)/dx;
}
else
{
tx_min = (x1-ox)/dx;
tx_max = (x0-ox)/dx;
}
}
if(abs(dy) < 0.000001f)
{
//若射线方向矢量的x轴分量为0且原点不在盒体内
if(oy < y1 || oy > y0)
return false ;
}
else
{
if(dy>=0)
{
ty_min = (y0-oy)/dy;
ty_max = (y1-oy)/dy;
}
else
{
ty_min = (y1-oy)/dy;
ty_max = (y0-oy)/dy;
}
}
if(abs(dz) < 0.000001f)
{
//若射线方向矢量的x轴分量为0且原点不在盒体内
if(oz < z1 || oz > z0)
return false ;
}
else
{
if(dz>=0)
{
tz_min = (z0-oz)/dz;
tz_max = (z1-oz)/dz;
}
else
{
tz_min = (z1-oz)/dz;
tz_max = (z0-oz)/dz;
}
}
double t0,t1;
//光线进入平面处(最靠近的平面)的最大t值
t0=max(tz_min,max(tx_min,ty_min));
//光线离开平面处(最远离的平面)的最小t值
t1=min(tz_max,min(tx_max,ty_max));
return t0<t1;
}
6、参考
《OpenGL ES 3.x 游戏开发》碰撞检测之 AABB 包围盒
光线与包围盒(AABB)的相交检测算法
射线与包围盒的相交测试