演算法與遊戲之OBB碰撞演算法
筆者介紹:姜雪偉,IT公司技術合夥人,IT高階講師,CSDN社群專家,特邀編輯,暢銷書作者,國家專利發明人;已出版書籍:《手把手教你架構3D遊戲引擎》電子工業出版社和《實戰核心技術詳解》電子工業出版社等。
包圍盒是一個簡單的立體幾何空間,它裡面可包含著複雜形狀的物體。給物體新增包圍體的目的是快速的進行碰撞檢測,如果讀者使用過Unity3D引擎,該引擎一共分為以下幾種碰撞體:球狀碰撞體、立方體碰撞體、膠囊體、Mesh碰撞體等。它們真正的實現原理也是是OBB包圍盒。本章主要是告訴讀者3D模型的碰撞體演算法一般採用的是OBB包圍盒演算法或者AABB包圍盒演算法,這兩種碰撞演算法在3D引擎中經常使用,Cocos2d-x
目前廣泛應用的是AABB和OBB包圍盒,其中AABB包圍盒更常見,因為它的生成方法很簡單,因它與座標軸是對齊的。但它也有不足,它不隨物體旋轉,對於較精確的碰撞檢測效果並不是特別好。這時就需要OBB包圍盒,因為它始終沿著物體的主方向生成最小的一個矩形包圍盒,並且可以隨物體旋轉,適用於較精確的碰撞檢測。OBB演算法在座標軸的表示如下圖:
想要標識一個OBB包圍盒我們大概會想到,使用8個頂點的集合、6個面的集合、1個頂點和3個彼此正交的邊向量,又或者是1箇中心點、1個旋轉矩陣和3個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
一種減少開銷的方案是:只儲存旋轉矩陣的兩個軸,只是在測試時利用叉積計算第三個軸,這樣可以減少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(constVec3* verts, intnum)
{
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<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(constVec3*verts, intnum)函式的引數提供了頂點的座標和頂點個數,在函式中使用了矩陣的旋轉變換。旋轉變換後,重新呼叫函式computeExtAxis()重新設定三個座標軸,函式實現如下所示:
void computeExtAxis()
{
_extentX = _xAxis * _extents.x;
_extentY = _yAxis * _extents.y;
_extentZ = _zAxis * _extents.z;
}
OBB碰撞盒演算法在計算方面比較精確,因此大部分引擎都是使用該演算法。下面把引擎的原始檔給讀者展示一下:
//生成協方差矩陣
staticMat4 _getConvarianceMatrix(constVec3* 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; i<vertCount; i++)
{
S1[0] += vertPos[i].x;
S1[1] += vertPos[i].y;
S1[2] += vertPos[i].z;
S2[0][0] += vertPos[i].x * vertPos[i].x;
S2[1][1] += vertPos[i].y * vertPos[i].y;
S2[2][2] += vertPos[i].z * vertPos[i].z;
S2[0][1] += vertPos[i].x * vertPos[i].y;
S2[0][2] += vertPos[i].x * vertPos[i].z;
S2[1][2] += vertPos[i].y * vertPos[i].z;
}
float n = (float)vertCount;
// 獲取協方差
Cov.m[0] = (float)(S2[0][0] - S1[0]*S1[0] / n) / n;
Cov.m[5] = (float)(S2[1][1] - S1[1]*S1[1] / n) / n;
Cov.m[10] = (float)(S2[2][2] - S1[2]*S1[2] / n) / n;
Cov.m[4] = (float)(S2[0][1] - S1[0]*S1[1] / n) / n;
Cov.m[9] = (float)(S2[1][2] - S1[1]*S1[2] / n) / n;
Cov.m[8] = (float)(S2[0][2] - S1[0]*S1[2] / n) / n;
Cov.m[1] = Cov.m[4];
Cov.m[2] = Cov.m[8];
Cov.m[6] = Cov.m[9];
return Cov;
}
static float& _getElement( Vec3& point, int index)
{
if (index == 0)
return point.x;
if (index == 1)
return point.y;
if (index == 2)
return point.z;
CC_ASSERT(0);
return point.x;
}
static void _getEigenVectors(Mat4* vout, Vec3* dout, Mat4 a)
{
int n = 3;
int j,iq,ip,i;
double tresh, theta, tau, t, sm, s, h, g, c;
int nrot;
Vec3 b;
Vec3 z;
Mat4 v;
Vec3 d;
v = Mat4::IDENTITY;
for(ip = 0; ip < n; ip++)
{
_getElement(b, ip) = a.m[ip + 4 * ip];
_getElement(d, ip) = a.m[ip + 4 * ip];
_getElement(z, ip) = 0.0;
}
nrot = 0;
for(i = 0; i <50; i++)
{
sm = 0.0;
for(ip = 0; ip < n; ip++) for(iq = ip+1; iq < n; iq++) sm += fabs(a.m[ip + 4 * iq]);
if( fabs(sm) <FLT_EPSILON )
{
v.transpose();
*vout = v;
*dout = d;
return;
}
if (i <3)
tresh = 0.2 * sm / (n*n);
else
tresh = 0.0;
for(ip = 0; ip < n; ip++)
{
for(iq = ip + 1; iq < n; iq++)
{
g = 100.0 * fabs(a.m[ip + iq * 4]);
float dmip = _getElement(d, ip);
float dmiq = _getElement(d, iq);
if( i>3 && 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);
}
}
//取邊的向量
Vec3OBB::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;
}
//取面的方向向量
Vec3 OBB::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碰撞盒在遊戲場景中的實現的效果如下圖: