Ray-AABB交叉檢測算法
??最近在解決三維問題時,需要判斷線段是否與立方體交叉,這個問題可以引申為:射線是否穿過立方體AABB。
??在3D遊戲開發中碰撞檢測普遍采用的算法是軸對齊矩形邊界框(Axially Aligned Bounding Box, AABB)包裝盒方法,其基本思想是用一個立方體或者球體完全包裹住3D物體對象,然後根據包裝盒的距離、位置等相關信息來計算是否發生碰撞。
slab的碰撞檢測算法
??本文接下來主要討論射線與AABB的關系,主要對box2d碰撞檢測使用的slab的碰撞檢測算法(Slabs method)進行介紹,然後使用python語言實現slab碰撞檢測方法,該方法可以用於3D物體拾取等應用場景。
??Slab英文翻譯是“平板”,本文是指兩個平行平面/直線之間的空間。在2D空間中slab可以理解為平行於坐標軸的兩條直線間的區域,3D空間中為平行於xy平面(或者yz面,xz面)的兩個平面之間的區域。由此,我們可以把3D空間中的AABB盒子看做是由AABB的3組平行面形成的3個方向的slab的交集。
??另外,引入候選面的概念:在3D空間中,我們先確定正對著射線的三個面,也就是說,我們可以通過某種方式將AABB相對於射線Ray的背面給忽略掉,從而確定三個候選的面。這三個候選的面,就是有可能和射線Ray發生交叉的最近的面。
??根據這個定義,我們可以得到以下三個結論:
- 性質一:如果一個點在AABB中,那麽這個點必定同時在這3個slab中。
- 性質二:如果一條射線和AABB相交,那麽這條射線和3個slab的相交部分必定有重合部分。
- 性質三:當射線與這三個候選面中的一個發生交叉之後,射線Ray的原點到這個面的距離要比到其他幾個面的距離要長。
??性質一和性質二比較容易理解,如果射線和3個slab的相交線段沒有重合,那麽這些線段就不可能同時存在於3個slab中,也就不可能在AABB盒子中。
??為了方便理解性質三,使用2D圖形來講解:
??在上圖中,我們的射線在右下角,向左上角發射,射線經過一個A點,其中候選面是y1面和x2面。
??根據上述性質,可以看到A點同時在2D空間中的2個slab中;此外,根據性質二,因為射線與平面相交,那麽這條射線與slab的相交部分必有重合部分,因為A點在射線上,且在平面中,那麽可以得到max(t1,t2)<=tA<=min(t3,t4);根據性質三:當交叉後,可以看出t2>t1。
??同理,我們可以把上述的驗證過程推廣到三維中。在三維空間中,假設射線到3個候選面的距離分別是t1、t2、t3,到候選面對應的面的距離分別為t4、t5、t6,那麽根據性質二,射線與AABB碰撞的條件是max(t1,t2,t3)<=min(t4,t5,t6);如果發生交叉,那麽根據性質三,射線到最近的交叉面的距離是是max(t1,t2,t3)。
??在上述性質基礎上,確定射線與AABB是否交叉需要三步驟:
- 如何確定候選面:只要將平面方程帶入射線Ray的方程,求出這兩個平面的t值,然後t值較小的那個自然先與射線交叉,那麽就表示它是一個候選面。射線可以用參數方程表示為R(t) = P0 + t·d, (其中P0為射線起點,d為射線的方向向量)
- 如何確定候選面的方程。平面由隱式定義方程X·n=D, (其中X為平面上的點,n為平面法向量,D為原點到平面的距離)給出。由於AABB的slab平面都分別和兩個坐標軸平行,它的面的法線總是有兩個分量是0,而另外一個分量總是為1,所以我們一致使用某個軸分量為1的法線。如果上面的方程表示的是AABB盒的左面的面,那麽公式中的n表示的就是(1,0,0),但上面的公式表示的是AABB盒的右邊的面的時候,n表示的值依然是(1,0,0)。
- 如何對交叉點是否在AABB盒上進行判斷。根據性質二判斷,即射線與AABB碰撞的條件是max(t1,t2,t3)<=min(t4,t5,t6)。
碰撞檢測算法公式推導
??求取t值的公式推導如下:
碰撞檢測算法Python源代碼
最後,附上我的Python代碼片段,代碼實時更新於GitHub
# Ray-AABB方法 相交返回True,否則返回False
# TDPoint = collections.namedtuple("TDPoint", ["x", "y","z"])
# AABB有最大和最小點組成,數據結構為{max=TDPoint,min=TDPoint}
# Ray由原點和方向組成,其中方向矢量為1,數據結構為{TDPoint,TDPoint}
def intersectWithAABB(AABB,Ray):
tmin=0
tmax=10000
# <editor-fold desc="平行於x軸">
if(math.fabs(Ray[1].x)<0.000001):
if (Ray[0].x<AABB.min.x) or (Ray[0].x>AABB.max.x):
return False
else:
ood=1.0/Ray[1].x
t1=(AABB.min.x-Ray[0].x)*ood
t2=(AABB.max.x-Ray[0].x)*ood
# t1做候選平面,t2做遠平面
if (t1>t2):
temp=t1
t1=t2
t2=temp
if t1>tmin:
tmin=t1
if t2<tmax:
tmax=t2
if tmin>tmax:
return False
# </editor-fold>
# <editor-fold desc="平行於y軸">
if(math.fabs(Ray[1].y)<0.000001):
if (Ray[0].y<AABB.min.y) or (Ray[0].y>AABB.max.y):
return False
else:
ood=1.0/Ray[1].y
t1=(AABB.min.y-Ray[0].y)*ood
t2=(AABB.max.y-Ray[0].y)*ood
# t1做候選平面,t2做遠平面
if (t1>t2):
temp=t1
t1=t2
t2=temp
if t1>tmin:
tmin=t1
if t2<tmax:
tmax=t2
if tmin>tmax:
return False
# </editor-fold>
# <editor-fold desc="平行於z軸">
if(math.fabs(Ray[1].z)<0.000001):
if (Ray[0].z<AABB.min.z) or (Ray[0].z>AABB.max.z):
return False
else:
ood=1.0/Ray[1].z
t1=(AABB.min.z-Ray[0].z)*ood
t2=(AABB.max.z-Ray[0].z)*ood
# t1做候選平面,t2做遠平面
if (t1>t2):
temp=t1
t1=t2
t2=temp
if t1>tmin:
tmin=t1
if t2<tmax:
tmax=t2
if tmin>tmax:
return False
# </editor-fold>
return True
from 3D空間中射線與軸向包圍盒AABB的交叉檢測算法
from Box2D 射線和AABB的碰撞檢測
Ray-AABB交叉檢測算法