笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人,已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社 和《Unity3D实战核心技术详解》电子工业出版社等书籍
包围盒是一个简单的立体几何空间,它里面可包含着复杂形状的物体。给物体添加包围体的目的是快速的进行碰撞检测,如果读者使用过Unity3D引擎,该引擎一共分为以下几种碰撞体:球状碰撞体、立方体碰撞体、胶囊体、Mesh碰撞体等。它们真正的实现原理也是是OBB包围盒。本章主要是告诉读者3D模型的碰撞体算法一般采用的是OBB包围盒算法或者AABB包围盒算法,这两种碰撞算法在3D引擎中经常使用,Cocos2d-x引擎对3D模型的碰撞检测也是采用AABB算法和OBB算法,先介绍OBB算法。
目前广泛应用的是AABB和OBB包围盒,其中AABB包围盒更常见,因为它的生成方法很简单,因它与坐标轴是对齐的。但它也有不足,它不随物体旋转,对于较精确的碰撞检测效果并不是特别好。这时就需要OBB包围盒,因为它始终沿着物体的主方向生成最小的一个矩形包围盒,并且可以随物体旋转,适用于较精确的碰撞检测。OBB算法在坐标轴的表示如下图所示。
想要标识一个OBB包围盒我们大概会想到,使用8个顶点的集合、6个面的集合、1个顶点和3个彼此正交的边向量,又或者是1个中心点、1个旋转矩阵和3个1/2边长(注:一个旋转矩阵包含了三个旋转轴,若是二维的OBB包围盒则是一个中心点,两个旋转轴,两个1/2边长)。
上述最后一种方法就是最常用的方法,下面来看一段Cocos2d-x 3.x中CCOBB.h中的代码:
Vec3 _center; // 中心点
/*
以下三个变量为正交单位向量,
定义了当前OBB包围盒的x,y,z轴
用于计算矢量投影
*/
Vec3 _xAxis; // 包围盒x轴方向单位矢量
Vec3 _yAxis; // 包围盒y轴方向单位矢量
Vec3 _zAxis; // 包围盒z轴方向单位矢量
Vec3 _extents; // 3个1/2边长,半长、半宽、半高
Cocos2d-x 3.x中,在CCOBB.h中定义了五个成员变量,每一个数据类型都是一个三维向量,包含了3个浮点数。也就是说,表达一个OBB包围盒需要15个float类型的变量,占用60个字节,然而表示一个AABB包围盒仅需要两个顶点,24个字节,从这一点上来说,OBB的内存消耗算很高了。
一种减少开销的方案是:只存储旋转矩阵的两个轴,只是在测试时利用叉积计算第三个轴,这样可以减少CPU操作开销并节省3个浮点数分量,降低20%内存消耗。
在Cocos2d-x 中使用了两种方法去计算OBB,第一种方法是简化的OBB构建算法,由一个AABB包围盒来确定最终OBB包围盒,另外一种方法是通过协方差矩阵来确定一个方向包围盒(实际上无论是AABB包围盒还是OBB包围盒,真正的难点便在于包围盒的构建上)。
在 Cocos2d-x 中第一种方法用起来更为简单一些,例如:
//获取一个Sprite3D对象的aabb包围盒
AABB aabb = _sprite->getAABB();
//创建obb包围盒
OBB _obbt = OBB(aabb);
不论立方体如何旋转其碰撞体都会随着转动。说到转动,读者不难想象到利用矩阵可以实现旋转变换,函数如下:
OBB::OBB(const Vec3* verts, int num)
{
if(!verts) return;
reset();
Mat4 matTransform = _getOBBOrientation(verts, num);
matTransform.transpose();
Vec3 vecMax = matTransform * Vec3(verts[0].x, verts[0].y, verts[0].z);
Vec3 vecMin = vecMax;
for (int i = 1; i < num; i++)
{
Vec3 vect = matTransform * Vec3(verts[i].x, verts[i].y, verts[i].z);
vecMax.x = vecMax.x> vect.x ? vecMax.x : vect.x;
vecMax.y = vecMax.y>vect.y ? vecMax.y : vect.y;
vecMax.z = vecMax.z>vect.z ? vecMax.z : vect.z;
vecMin.x = vecMin.x< vect.x ? vecMin.x : vect.x;
vecMin.y = vecMin.y
OBB(const Vec3*verts, int num)函数的参数提供了顶点的坐标和顶点个数,在函数中使用了矩阵的旋转变换。旋转变换后,重新调用函数computeExtAxis()重新设置三个坐标轴,函数实现如下所示:
void computeExtAxis()
{
_extentX = _xAxis * _extents.x;
_extentY = _yAxis * _extents.y;
_extentZ = _zAxis * _extents.z;
}
//生成协方差矩阵
static Mat4 _getConvarianceMatrix(const Vec3* vertPos, int vertCount)
{
int i;
Mat4 Cov;
double S1[3];
double S2[3][3];
S1[0] = S1[1] = S1[2] = 0.0;
S2[0][0] = S2[1][0] = S2[2][0] = 0.0;
S2[0][1] = S2[1][1] = S2[2][1] = 0.0;
S2[0][2] = S2[1][2] = S2[2][2] = 0.0;
for(i=0; i3&&fabs(dmip) + g == fabs(dmip) &&fabs(dmiq) + g == fabs(dmiq) )
{
a.m[ip + 4 * iq] = 0.0;
}
else if (fabs(a.m[ip + 4 * iq]) > tresh)
{
h = dmiq - dmip;
if (fabs(h) + g == fabs(h))
{
t=(a.m[ip + 4 * iq])/h;
}
else
{
theta = 0.5 * h / (a.m[ip + 4 * iq]);
t=1.0 / (fabs(theta) + sqrt(1.0 + theta * theta));
if (theta <0.0) t = -t;
}
c = 1.0 / sqrt(1+t*t);
s = t*c;
tau = s / (1.0+c);
h = t * a.m[ip + 4 * iq];
_getElement(z, ip) -= (float)h;
_getElement(z, iq) += (float)h;
_getElement(d, ip) -= (float)h;
_getElement(d, iq) += (float)h;
a.m[ip + 4 * iq]=0.0;
for(j = 0; j < ip; j++) { ROTATE(a,j,ip,j,iq); }
for(j = ip + 1; j < iq; j++) { ROTATE(a,ip,j,j,iq); }
for(j = iq + 1; j < n; j++) { ROTATE(a,ip,j,iq,j); }
for(j = 0; j < n; j++) { ROTATE(v,j,ip,j,iq); }
nrot++;
}
}
}
for(ip = 0; ip < n; ip++)
{
_getElement(b, ip) += _getElement(z, ip);
_getElement(d, ip) = _getElement(b, ip);
_getElement(z, ip) = 0.0f;
}
}
v.transpose();
*vout = v;
*dout = d;
return;
}
//创建OBB包围盒取向矩阵
static Mat4 _getOBBOrientation(const Vec3* vertPos, int num)
{
Mat4 Cov;//创建一个 4*4 矩阵
if (num <= 0)
return Mat4::IDENTITY;//返回单位矩阵
Cov = _getConvarianceMatrix(vertPos, num);//创建协方差矩阵
Mat4 Evecs;
Vec3 Evals;
_getEigenVectors(&Evecs, &Evals, Cov);
Evecs.transpose();//转置
return Evecs;
}
//默认构造函数
OBB::OBB()
{
//数据复位
reset();
}
//由一个AABB包围盒生成OBB包围盒
OBB::OBB(const AABB& aabb)
{
//数据复位
reset();
//中心点
_center = (aabb._min + aabb._max);
_center.scale(0.5f);
//各轴旋转矩阵的单位矩阵
_xAxis.set(1.0f, 0.0f, 0.0f);
_yAxis.set(0.0f, 1.0f, 0.0f);
_zAxis.set(0.0f, 0.0f, 1.0f);
//半尺存半长半宽半高
_extents = aabb._max - aabb._min;
_extents.scale(0.5f);
computeExtAxis();
}
//构造函数根据点信息初始化一个OBB包围盒
OBB::OBB(const Vec3* verts, int num)
{
if (!verts) return;//如果verts不存在返回
reset();//数据复位
//创建包围盒取向矩阵
Mat4 matTransform = _getOBBOrientation(verts, num);
/*
matTransform是一个正交矩阵,所以它的逆矩阵就是它的转置;
AA'=E(E为单位矩阵,A'表示“矩阵A的转置矩阵”) A称为正交矩阵
*/
matTransform.transpose();//计算matTransform矩阵的转置(此处相当于求逆矩)
Vec3 vecMax = matTransform * Vec3(verts[0].x, verts[0].y, verts[0].z);
Vec3 vecMin = vecMax;
for (int i = 1; i < num; i++)
{
Vec3 vect = matTransform * Vec3(verts[i].x, verts[i].y, verts[i].z);
vecMax.x = vecMax.x> vect.x ? vecMax.x : vect.x;
vecMax.y = vecMax.y> vect.y ? vecMax.y : vect.y;
vecMax.z = vecMax.z> vect.z ? vecMax.z : vect.z;
vecMin.x = vecMin.x< vect.x ? vecMin.x : vect.x;
vecMin.y = vecMin.y< vect.y ? vecMin.y : vect.y;
vecMin.z = vecMin.z< vect.z ? vecMin.z : vect.z;
}
matTransform.transpose();
_xAxis.set(matTransform.m[0], matTransform.m[1], matTransform.m[2]);
_yAxis.set(matTransform.m[4], matTransform.m[5], matTransform.m[6]);
_zAxis.set(matTransform.m[8], matTransform.m[9], matTransform.m[10]);
_center = 0.5f * (vecMax + vecMin);
_center *= matTransform;
_xAxis.normalize();
_yAxis.normalize();
_zAxis.normalize();
_extents = 0.5f * (vecMax - vecMin);
computeExtAxis();
}
//判断一点是否在OBB包围盒内
bool OBB::containPoint(const Vec3& point) const
{
//相当于将点坐标从世界坐标系中转换到了OBB包围盒的物体坐标系中
Vec3 vd = point - _center;
/*
dot方法为求点积
由于_xAxis为单位矢量
vd与_xAxis的点击即为在_xAxis方向的投影
*/
float d = vd.dot(_xAxis);//计算x方向投影d
//判断投影是否大于x正方向的半长或小于x负方向半长
if (d >_extents.x || d < -_extents.x)
return false;//满足条件说明不在包围盒内
d = vd.dot(_yAxis);//计算y方向投影
if (d >_extents.y || d < -_extents.y)
return false;
d = vd.dot(_zAxis);//计算z方向投影
if (d >_extents.z || d < -_extents.z)
return false;
return true;
}
//指定OBB包围盒的变量值
void OBB::set(const Vec3& center, const Vec3& xAxis, const Vec3& yAxis, const Vec3& zAxis, const Vec3& extents)
{
_center = center;
_xAxis = xAxis;
_yAxis = yAxis;
_zAxis = zAxis;
_extents = extents;
}
//复位
void OBB::reset()
{
memset(this, 0, sizeof(OBB));//将OBB所在内存块置零
}
//获取顶点信息
void OBB::getCorners(Vec3* verts) const
{
verts[0] = _center - _extentX + _extentY + _extentZ; //左上顶点坐标
//z轴正方向的面
verts[1] = _center - _extentX - _extentY + _extentZ; //左下顶点坐标
verts[2] = _center + _extentX - _extentY + _extentZ; //右下顶点坐标
verts[3] = _center + _extentX + _extentY + _extentZ; //右上顶点坐标
//z轴负方向的面
verts[4] = _center + _extentX + _extentY - _extentZ; //右上顶点坐标
verts[5] = _center + _extentX - _extentY - _extentZ; //右下顶点坐标
verts[6] = _center - _extentX - _extentY - _extentZ; //左下顶点坐标
verts[7] = _center - _extentX + _extentY - _extentZ; //左上顶点坐标
}
//将点投影到坐标轴
float OBB::projectPoint(const Vec3& point, const Vec3& axis)const
{
float dot = axis.dot(point);//点积
float ret = dot * point.length();
return ret;
}
//计算最大最小投影值
void OBB::getInterval(const OBB& box, const Vec3& axis, float &min, float &max) const
{
Vec3 corners[8];
box.getCorners(corners);//获取包围盒顶点信息
float value;
//分别投影八个点,取最大和最小值
min = max = projectPoint(axis, corners[0]);
for(int i = 1; i <8; i++)
{
value = projectPoint(axis, corners[i]);
min = MIN(min, value);
max = MAX(max, value);
}
}
//取边的矢量
Vec3 OBB::getEdgeDirection(int index) const
{
Vec3 corners[8];
getCorners(corners);//获取八个顶点信息
Vec3 tmpLine;
switch(index)
{
case 0:// x轴方向
tmpLine = corners[5] - corners[6];
tmpLine.normalize();
break;
case 1:// y轴方向
tmpLine = corners[7] - corners[6];
tmpLine.normalize();
break;
case 2:// z轴方向
tmpLine = corners[1] - corners[6];
tmpLine.normalize();
break;
default:
CCASSERT(0, "Invalid index!");
break;
}
return tmpLine;
}
//取面的方向矢量
Vec3OBB::getFaceDirection(int index) const
{
Vec3 corners[8];
getCorners(corners);
Vec3 faceDirection, v0, v1;
switch(index)
{
case 0:// //前/后计算结果为一个与z轴平行的矢量
v0 = corners[2] - corners[1];朝向+z的面左下点->右下点的矢量
v1 = corners[0] - corners[1];//左下点->左上点的矢量
/*
两个矢量的叉积得到的结果
是垂直于原来两个相乘矢量的矢量
*/
Vec3::cross(v0, v1, &faceDirection);//计算v0,v1的叉积结果存储到faceDirection
/*
归一化
此处相当于求x,y轴所在平面的法矢量
*/
faceDirection.normalize();
break;
case 1:// 左/右计算结果为一个与x轴平行的矢量
v0 = corners[5] - corners[2];
v1 = corners[3] - corners[2];
Vec3::cross(v0, v1, &faceDirection);
faceDirection.normalize();
break;
case 2:// 上/下计算结果为一个与y轴平行的矢量
v0 = corners[1] - corners[2];
v1 = corners[5] - corners[2];
Vec3::cross(v0, v1, &faceDirection);
faceDirection.normalize();
break;
default:
CCASSERT(0, "Invalid index!");
break;
}
return faceDirection;//返回方向矢量
}
//检测两个OBB包围盒是否重合
bool OBB::intersects(const OBB& box) const
{
float min1, max1, min2, max2;
//当前包围盒的三个面方向相当于取包围盒的三个坐标轴为分离轴并计算投影作比较
for (int i = 0; i <3; i++)
{
getInterval(*this, getFaceDirection(i), min1, max1);//计算当前包围盒在某轴上的最大最小投影值
getInterval(box, getFaceDirection(i), min2, max2);//计算另一个包围盒在某轴上的最大最小投影值
if (max1 < min2 || max2 < min1) return false;//判断分离轴上投影是否重合
}
//box包围盒的三个面方向
for (int i = 0; i <3; i++)
{
getInterval(*this, box.getFaceDirection(i), min1, max1);
getInterval(box, box.getFaceDirection(i), min2, max2);
if (max1 < min2 || max2 < min1) return false;
}
for (int i = 0; i <3; i++)
{
for (int j = 0; j <3; j++)
{
Vec3 axis;
Vec3::cross(getEdgeDirection(i), box.getEdgeDirection(j), &axis);//边的矢量并做叉积
getInterval(*this, axis, min1, max1);
getInterval(box, axis, min2, max2);
if (max1 < min2 || max2 < min1) return false;
}
}
return true;
}
//由一个给定矩阵对OBB包围盒进行变换
void OBB::transform(const Mat4& mat)
{
// 新的中心点
Vec4 newcenter = mat * Vec4(_center.x, _center.y, _center.z, 1.0f);// center;
_center.x = newcenter.x;
_center.y = newcenter.y;
_center.z = newcenter.z;
//变换向量
_xAxis = mat * _xAxis;
_yAxis = mat * _yAxis;
_zAxis = mat * _zAxis;
_xAxis.normalize();
_yAxis.normalize();
_zAxis.normalize();
Vec3 scale, trans;
Quaternion quat;//四元数单位长度的四元数可以表示三维旋转
mat.decompose(&scale, &quat, &trans);
//半长半宽半高
_extents.x *= scale.x;
_extents.y *= scale.y;
_extents.z *= scale.z;
computeExtAxis();
}
利用
OBB
碰撞盒在游戏场景中的实现的效果如下图: