一、AABB立方體邊界框檢測
用球體去近似地代表物體運算量很小,但在游戲中的大多數物體是方的或者長條形的,應該用方盒來代表物體。另一種常見的檢測模型是立方體邊界框,如圖10-31展示了一個AABB檢測盒和它里面的物體。
坐標軸平行(Axially-aligned)不僅指盒體與世界坐標軸平行,同時也指盒體的每個面都和一條坐標軸垂直,這樣一個基本信息就能減少轉換盒體時操作的次數。AABB技術在當今的許多游戲中都得到了應用,開發者經常用它們作為模型的檢測模型,再次指出,提高精度的同時也會降低速度。
因為AABB總是與坐標軸平行,不能在旋轉物體時簡單地旋轉AABB,而是應該在每一幀都重新計算。如果知道每個對象的內容,這個計算就不算困難,也不會降低游戲的速度。然而,還面臨着精度的問題。
假如有一個3D的細長剛性直棒,並且要在每一幀動畫中都重建它的AABB。可以看到每一幀中的包裝盒都不一樣而且精度也會隨之改變,如圖10-32所示。
圖10-31 3D模型與AABB檢測盒 圖10-32 不同方向的AABB
可以注意到AABB對物體的方向很敏感,同一物體的不同方向,AABB也可能不同(由於球體只有一個自由度,所以檢測球對物體方向不敏感)。
當物體在場景中移動時,它的AABB也需要隨之移動,當物體發生旋轉時,有兩種選擇:用變換后的物體來重新計算AABB,或者對AABB做和物體同樣的變換。
如果物體沒有發生扭曲,可以通過“變換后的AABB”重新計算,因為該方法要比通過“變換后的物體”計算快得多,因為AABB只有8個頂點。變換AABB得出新的AABB要比變換物體的運算量小,但是也會帶來一定的誤差,如圖10-33所示。
比較圖中原AABB(灰色部分)和新AABB(右邊比較大的方框),它是通過旋轉后的AABB計算得到的,新AABB幾乎是原來AABB的兩倍,注意,如果從旋轉后的物體而不是旋轉后的AABB來計算新AABB,它的大小將和原來的AABB相同。
先介紹AABB的表達方法,AABB內的點滿足以下條件:
xmin≤x≤xmax
ymin≤y≤ymax
zmin≤z≤zmax
因此只需要知道兩個特別重要的頂點(xmin,ymin,zmin)、(xmax,ymax,zmax),記作:
float[] min = new float []{0.0f,0.0f,0.0f};
float[] max = new float []{0.0f,0.0f,0.0f};
中心點是兩個頂點的中點,代表了包裝盒的質點。
float[] center = new float []{0.0f,0.0f,0.0f};
中心點的計算方法如下:
float [] center(){
center[0] = (min[0] + max[0])*0.5f;
center[1] = (min[1] + max[1])*0.5f;
center[2] = (min[2] + max[2])*0.5f;
return center;
}
通過這兩個頂點可以知道以下屬性。
float xSize() { return (max[0]-min[0]); }
float ySize() { return (max[1]-min[1]); }
float zSize() { return (max[2]-min[2]); }
float size(){ return (max[0]-min[0])*(max[1]-min[1])*(max[2]-min[2]);}
當添加一個頂點到包裝盒時,需要先與這兩個頂點進行比較。
void add(float []p) {
if (p[0] < min[0]) min[0] = p[0];
if (p[0] > max[0]) max[0] = p[0];
if (p[1] < min[1]) min[1] = p[1];
if (p[1] > max[1]) max[1] = p[1];
if (p[2] < min[2]) min[2] = p[2];
if (p[2] > max[2]) max[2] = p[2];
}
檢測包裝盒是否為空,可以將這兩個頂點進行比較。
boolean isEmpty() {
return (min[0] > max[0]) || (min[1] > max[1]) || (min[2] > max[2]);
}
檢測某個點是否屬於AABB范圍之內的代碼如下:
boolean contains(float []p){
return
(p[0] >= min[0]) && (p[0] <= max[0]) &&
(p[1] >= min[1]) && (p[1] <= max[1]) &&
(p[2] >= min[2]) && (p[2] <= max[2]);
}
AABB的靜態檢測比較簡單,檢測兩個靜止包裝盒是否相交,它是一種布爾測試,測試結果只有相交或者不相交。這里我們還提供了獲取相交范圍信息的方法,一般來說,這種測試的目的是為了返回一個布爾值。碰撞的示意如圖10-34所示。
圖10-34 包裝盒的碰撞
檢測靜態AABB碰撞的方法如下:
boolean intersectAABBs(AABB box2,AABB boxIntersect)
{
float []box2_min = box2.getMin();
float []box2_max = box2.getMax();
if (min[0] > box2_max[0]) return false;
if (max[0] < box2_min[0]) return false;
if (min[1] > box2_max[1]) return false;
if (max[1] < box2_min[1]) return false;
if (min[2] > box2_max[2]) return false;
if (max[2] < box2_min[2]) return false;
if (boxIntersect != null) {
float []box_intersect_min = new float[3];
float []box_intersect_max = new float[3];
box_intersect_min[0] = Math.max(min[0], box2_min[0]);
box_intersect_max[0] = Math.min(max[0], box2_max[0]);
box_intersect_min[1] = Math.max(min[1], box2_min[1]);
box_intersect_max[1] = Math.min(max[1], box2_max[1]);
box_intersect_min[2] = Math.max(min[2], box2_min[2]);
box_intersect_max[2] = Math.min(max[2], box2_max[2]);
}
return true;
}
可以利用AABB的結構來加快新的AABB的計算速度,而不用變換8個頂點,再從這8個頂點中計算新AABB。下面簡單地回顧4×4矩陣變換一個3D點的過程。
通過原邊界框(xmin,ymin,zmin,xmax,ymax,zmax)計算新邊界框(,,,,,),現在的任務是計算的速度。換句話說,希望找到m11x+m12y+m13z+m14的最小值。其中[x,y,z]是原8個頂點中的任意一個。
變換的目的是找出這些點經過變換后哪一個的x坐標最小。看第一個乘積m11x,為了最小化乘積,必須決定是用xmin還是xmax來替換其中的x。顯然,如果m11>0,用xmin能得到最小化的乘積;如果m11<0,則用xmax能得到最小化乘積。
比較方便的是,不管xmin還是xmax中哪一個被用來計算,都可以用另外一個來計算。可以對矩陣中的9個元素中的每一個都應用這個計算過程(其他元素不影響大小)。
根據變換矩陣和原有的AABB包裝盒計算新的AABB包裝盒的代碼如下:
void setToTransformedBox(Transform t)
{
if (isEmpty()) { //判斷包裝盒是否為空
return;
}
float[] m = new float [16];
t.get(m); //將變換矩陣存入數組
float minx=0,miny=0,minz=0;
float maxx=0,maxy=0,maxz=0;
minx += m[3]; //x方向上平移
maxx += m[3]; //x方向上平移
miny += m[7]; //y方向上平移
maxy += m[7]; //y方向上平移
minz += m[11]; //z方向上平移
maxz += m[11]; //z方向上平移
if (m[0] > 0.0f) {
minx += m[0] * min[0]; maxx += m[0] * max[0];
} else {
minx += m[0] * max[0]; maxx += m[0] * min[0];
}
if (m[1] > 0.0f) {
minx += m[1] * min[1]; maxx += m[1] * max[1];
} else {
minx += m[1] * max[1]; maxx += m[1] * min[1];
}
if (m[2] > 0.0f) {
minx += m[2] * min[2]; maxx += m[2] * max[2];
} else {
minx += m[2] * max[2]; maxx += m[2] * min[2];
}
if (m[4] > 0.0f) {
miny += m[4] * min[0]; maxy += m[4] * max[0];
} else {
miny += m[4] * max[0]; maxy += m[4] * min[0];
}
if (m[5] > 0.0f) {
miny += m[5] * min[1]; maxy += m[5] * max[1];
} else {
miny += m[5] * max[1]; maxy += m[5] * min[1];
}
if (m[6] > 0.0f) {
miny += m[6] * min[2]; maxy += m[6] * max[2];
} else {
miny += m[6] * max[2]; maxy += m[6] * min[2];
}
if (m[8] > 0.0f) {
minz += m[8] * min[0]; maxz += m[8] * max[0];
} else {
minz += m[8] * max[0]; maxz += m[8] * min[0];
}
if (m[9] > 0.0f) {
minz += m[9] * min[1]; maxz += m[9] * max[1];
} else {
minz += m[9] * max[1]; maxz += m[9] * min[1];
}
if (m[10] > 0.0f) {
minz += m[10] * min[2]; maxz += m[10] * max[2];
} else {
minz += m[10] * max[2]; maxz += m[10] * min[2];
}
min[0] = minx; min[1] = miny; min[2] = minz; //用新的AABB坐標替換原有坐標
max[0] = maxx; max[1] = maxy; max[2] = maxz; //用新的AABB坐標替換原有坐標
}
為了使用AABB包裝盒進行碰撞檢測,將這些方法和屬性封裝為AABB類,代碼如下:
import java.lang.Math;
import javax.microedition.m3g.Transform;
class AABB{
public AABB(){}
float [] getMin(){return min;}
float [] getMax(){return max;}
void setMin(float x,float y,float z){min[0]=x;min[1]=y;min[2]=z;}
void setMax(float x,float y,float z){max[0]=x;max[1]=y;max[2]=z;}
void reset(){
for(int i =0;i<3;i++)
{
min[i]=0;
max[i]=0;
}
}
//其他方法同上
}
為了檢驗碰撞檢測的使用構造了兩個立方體,並各自綁定了一個包裝盒。
/**************立方體1***************/
mesh1 = createCube(); //創建立方體1
mesh1.setTranslation(1.0f, 0.0f,0.0f) ; //平移
mesh1.setOrientation(90,0.0f,1.0f,0.0f); //旋轉
mesh1.setScale(0.5f,0.5f,0.5f); //縮放
box1 = new AABB(); //包裝盒
box1.setMin(-1.0f,-1.0f,-1.0f); //設置包裝盒1的最小頂點
box1.setMax(1.0f,1.0f,1.0f); //設置包裝盒1的最大頂點
mesh1.getCompositeTransform(cubeTransform); //獲取立方體1的混合矩陣
box1.setToTransformedBox(cubeTransform); //將變換矩陣應用到包裝盒中
world.addChild(mesh1); //將立方體1添加到場景中
/**************立方體2***************/
mesh2 = createCube(); //創建立方體2
mesh2.setTranslation(-0.5f, 0.0f,0.0f) ; //平移
mesh2.setScale(0.5f,0.5f,0.5f); //縮放
box2 = new AABB(); //包裝盒
box2.setMin(-1.0f,-1.0f,-1.0f); //設置包裝盒2的最小頂點
box2.setMax(1.0f,1.0f,1.0f); //設置包裝盒2的最大頂點
mesh2.getCompositeTransform(cubeTransform); //獲取立方體2的混合矩陣
box2.setToTransformedBox(cubeTransform); //將變換矩陣應用到包裝盒2中
world.addChild(mesh2); //將立方體2添加到場景中
檢測包裝盒1和包裝盒2是否碰撞的代碼如下:
isCollided = box1.intersectAABBs(box2,null); //檢測兩個AABB包裝盒是否碰撞
編譯運行程序,設置兩個立方體不同的位置和角度,可以比較精確地檢測出它們的碰撞情況,如圖10-35所示。
檢測兩個靜止AABB的碰撞情況比較簡單,只需要在每一維上單獨檢查它們的重合程度即可。如果在所有維上都沒有重合,那么這兩個AABB就不會相交。
AABB間的動態檢測稍微復雜一些,考慮一個由頂點smin和smax指定的靜態包裝盒和一個由頂點mmin和mmax指定的動態包裝盒(如果兩個都是動態的,可以根據相對運動視作如此)。運動的速度由向量s給出,運動時間t假定為0~1。
圖10-35 靜態物體碰撞檢測示意
移動檢測的目標是計算運動AABB碰撞到靜態AABB的時刻,因此需要計算出兩個AABB在所有維上的第一個點。為了簡化起見,可以把上述問題先歸結到某一維,然后再將三維結合到一起。假設把問題投影到x軸,如圖10-36所示。
圖10-36 AABB的動態檢測
黑色矩形代表沿坐標軸滑動的AABB,t=0時,運動AABB完全位於靜止AABB的左邊。當t=1時,運動AABB完全位於靜止AABB的右邊。當t=tenter時,兩個AABB剛剛相交,當t=tleave時,兩個AABB脫離碰撞。
對照上圖,可以推導出兩個AABB接觸和離開的時間:
,
AABB的動態檢測有3個要點。
n 如果速度為0,兩個包裝盒要么一直相交,要么一直分離。
n 不管物體從哪個方向運動,碰撞過程中,肯定是先入后出,所以有tenter<tleave。
n 如果tenter和tleave超出運動時間范圍,那么在此范圍內它們是不相交的。
檢測出某一維的碰撞還不夠,還需要進行其他兩維的檢測,然后取結果的交集。如果交集為空,那么兩AABB包裝盒沒有相交,如果區間范圍在時間段[0,1]之外,那么在此區間也不相交。對AABB進行動態檢測的方法定義如下:
float intersectMovingAABB(AABB stationaryBox,AABB movingBox,float []s)
{
float NoIntersection = 1e30f; //沒有碰撞則返回大數
float tEnter = 0.0f; //初始化碰撞時間
float tLeave = 1.0f; //初始化離開時間
float Swap = 0.0f; //交換操作中間變量
float [] sBoxmin= stationaryBox.getMin(); //靜止包裝盒的最小值頂點
float [] sBoxmax= stationaryBox.getMax(); //靜止包裝盒的最大值頂點
float [] mBoxmin= movingBox.getMin(); //運動包裝盒的最小值頂點
float [] mBoxmax= movingBox.getMax(); //運動包裝盒的最大值頂點
if (s[0] == 0.0f) { //如果x方向速度為0
if ((sBoxmin[0] >= mBoxmax[0]) ||(sBoxmax[0] <= mBoxmin[0])) {
return NoIntersection; //進行靜態檢測
}
} else {
float xEnter = (sBoxmin[0]-mBoxmax[0])/s[0]; //計算碰撞時間
float xLeave = (sBoxmax[0]-mBoxmin[0])/ s[0]; //計算離開時間
if (xEnter > xLeave) { //檢查順序
Swap = xEnter;
xEnter = xLeave;
xLeave = Swap;
}
if (xEnter > tEnter) tEnter = xEnter; //更新區間
if (xLeave < tLeave) tLeave = xLeave;
if (tEnter > tLeave) { //是否導致空重疊區
return NoIntersection; //沒有碰撞
}
}
if (s[1] == 0.0f) { //y軸速度為0
if ( (sBoxmin[1] >= mBoxmax[1]) || (sBoxmax[1] <= mBoxmin[1])) {
return NoIntersection; //沒有相交
}
} else {
float yEnter = (sBoxmin[1]-mBoxmax[1]) / s[1];
float yLeave = (sBoxmax[1]-mBoxmin[1]) / s[1];
if (yEnter > yLeave) {
Swap = yEnter;
yEnter = yLeave;
yLeave = Swap;
}
if (yEnter > tEnter) tEnter = yEnter; //更新區間
if (yLeave < tLeave) tLeave = yLeave;
if (tEnter > tLeave) {
return NoIntersection;
}
}
if (s[2] == 0.0f) { //z方向速度為0
if ((sBoxmin[2] >= mBoxmax[2]) ||(sBoxmax[2] <= mBoxmin[2])) {
return NoIntersection;
}
} else {
float oneOverD = 1.0f / s[2];
float zEnter = (sBoxmin[2]-mBoxmax[2]) / s[2];
float zLeave = (sBoxmax[2]- mBoxmin[2]) / s[2];
if (zEnter > zLeave) {
Swap = zEnter;
zEnter = zLeave;
zLeave = Swap;
}
if (zEnter > tEnter) tEnter = zEnter; //更新區間
if (zLeave < tLeave) tLeave = zLeave;
if (tEnter > tLeave) {
return NoIntersection;
}
}
return tEnter; //返回碰撞時間
}
為了對移動AABB進行檢測,創建兩個AABB如圖10-37所示。兩個包裝盒距離0.5,速度為3。
圖10-37 移動AABB檢測
檢測代碼如下:
float[] speed = new float []{3.0f,0.0f,0.0f};
float tEnter = intersectMovingAABB(box1,box2,speed);
輸出結果為0.16667,完全符合預期的猜測。
二、OBB包圍盒
前面提到了長條物體在旋轉時AABB盒的變化,那么是否有能夠在任意方向都更為精確的檢測方式,答案是肯定的,這是一種基於OBB即定向包容盒子(Oriented Bounding Box,OBB)的技術,它已經廣泛用於光線追蹤和碰撞檢測中。
OBB這種方法是根據物體本身的幾何形狀來決定盒子的大小和方向,盒子無須和坐標軸垂直。這樣就可以選擇最合適的最緊湊的包容盒子。OBB盒子的生成比較復雜。一般是考慮物體所有的頂點在空間的分布,通過一定的算法找到最好的方向(OBB盒子的幾個軸)。
一個2D示意圖如圖10-38所示。
這種技術比AABB技術更精確而且更健壯,但OBB實現起來比較困難,執行速度慢,並且不太適合動態的或柔性的物體。特別注意的是,當把一個物體分得越來越小的時候,事實上是在創建一棵有層次的樹,如圖10-39所示。
圖10-39 OBB樹的生成(曲折線為物體)
為任意的網格模型創建OBB樹可能是算法里最難的一個部分,而且它還要調整以適合特定的引擎或游戲類型。從圖中可以看出,不得不找出包圍給定模型的最近似的包裝盒(或者其他3D體)。
現在得到了所有的包裝盒,下一步將構造一棵樹。
從最初的AABB包裝盒開始從上至下地反復分割它。另外,還可以用從下至上的方式,逐步地合並小包裝盒從而得到最大的包裝盒。把大的包裝盒分割成小的包裝盒,應該遵守以下幾條原則。
(1)用一個面(這個面垂直於包裝盒中的一條坐標軸)來分割包裝盒上最長的軸,然后根據多邊形處在分割軸的哪一邊把多邊形分離開來(如圖10-38所示)。
(2)如果不能沿着最長的軸進行分割,那就沿第二長的邊分割。持續地分割直到包裝盒不能再分割為止。
(3)依據需要的精度(比如,是否真的要判斷單個三角形的碰撞),可以按選擇的方式(是按樹的深度或是按包裝盒中多邊形的數目)以任意的條件停止分割。
正如讀者所看到的,創建階段相當復雜,其中包括了大量的運算,很明顯不能實時地創建樹,只能是事先創建。事先創建可以免去實時改變多邊形的可能。另一個缺點是OBB要求進行大量的矩陣運算,不得不把它們定位在適當的地方,並且每棵子樹必須與矩陣相乘。
現在假設已經有了OBB或者AABB樹。那么該怎么進行碰撞檢測呢?首先檢測最大的包裝盒是否相交(AABB級別),如果相交了,它們可能(注意,只是可能)發生了碰撞,接下來將進一步地遞歸處理它們(OBB級別,不斷地遞歸用下一級進行處理)。
如果沿着下一級,發現子樹並沒有發生相交,這時就可以停止,並得出結論沒有發生碰撞。如果發現子樹相交,那么要進一步處理它的子樹直到到達葉子節點,並最終得出結論。
碰撞檢測最直觀的想法是把一個OBB盒子的每個邊都和另一個盒子的所有面來比較,如果這個邊穿過了另一個OBB盒子的一個面,則兩個OBB盒子發生了碰撞。顯然這種方法的計算量是比較大的,因為要進行12×6×2=144次邊和面的比較。
但是,在考察兩個沒有碰撞的OBB盒子時,人們發現一些規律來簡化比較。
(1)如果兩個OBB盒子不互相接觸,則應該可以找到一個盒子上的一個面,這個面所在的平面可以把3D空間分為兩部分,兩個OBB盒子各在兩邊。
(2)如果沒有這樣的表面存在,則一定可以在兩個OBB盒子上各找出一條邊,這兩條邊所在的平面可以把兩個OBB盒子分在兩邊。有了這個平面,就可以找到垂直於它的分割軸(separating axis),如圖10-40所示。
(3)進行相交測試時,可以把包裝盒投影到分割軸上,並檢查它們是否線性相交。兩個OBB盒子在這個分割軸上的投影將是分離的。
如上所述,要判斷兩個OBB盒子是否碰撞,只需要看兩個OBB盒子之間是否有這樣的平面和分割軸存在。如果存在,則沒有碰撞。如果不存在,則碰撞。對第一種情況,每個盒子有6個表面(其中每兩個平行),可以決定3個分割軸。兩個OBB盒子一共有6個可能的分割軸需要考慮。對第二種情況,兩個OBB盒子之間的邊的組合可以有3×3=9種情況,也就是有9個可能的分割軸。這樣對任意兩個OBB盒子,只需要考察15個分割軸就可以了。如果在任一分割軸上的陰影不重合,則OBB盒子之間沒有碰撞。
選擇AABB還是選擇OBB應該根據所需的精確程度而定。對一個需要快速反應的3D射擊游戲來說,可能用AABB來進行碰撞檢測更好些——可以犧牲一些精度來換取速度和實現的簡單化,因此總能在游戲中看到一些小疏漏。當然隨着硬件能力的提高,OBB處理會逐漸被重視起來。
在做碰撞檢測時應當遵循以下的優化理論,這樣可以改善檢測速度。
n 分兩步檢驗,距離遠時看作質點,距離近時采用包裝盒。
n 距離很遠的物體不會在短時間內相撞(可以采用BSP樹分割空間)。
n 一個物體不能隔着第二個物體和第三個物體相撞。
n 一旦一個物體檢測到和另一物體碰撞,另一物體對這個物體不再檢測。
n 靜止的物體不主動與其他物體碰撞。
osg交流群:145907921
再分享一下我老師大神的人工智能教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智能的隊伍中來!https://www.cnblogs.com/captainbed