轉自Complex_ok
第04章 碰撞模塊(Collision Module)
碰撞模塊包含了形狀, 和操作形狀的函數。該模塊還包含了動態樹(dynamic tree)和broad-phase, 用於加快大型系統的碰撞處理速度。
形狀描述了可相互碰撞的幾何對象, 就算不進行物理模擬,也可獨立使用。你可以在shape上執行一些操作。
b2Shape是個基類, Box2D的各種形狀都實現了這個基類。此基類定義了幾個函數:
• 判斷一個點與形狀是否有重疊
• 在形狀上執行光線投射(ray cast)
• 計算形狀的AABB
• 計算形狀的質量
另外, 每個形狀都有兩成員變量: 類型(type)和半徑(radius)。 對於多邊形,半徑也是有意義的, 下面會進行討論。
圓形有位置和半徑。
圓形是實心的,你沒有辦法使圓形變成空心。但是,你可以使用多邊形來創建一系列線段,讓這些線段首尾相連,串成一串,就可以模擬出空心的圓形。
b2CircleShape circle;
circle.m_p.Set(1.0f, 2.0f, 3.0f);
circle.m_radius = 0.5f;
Box2D的多邊形是實心的凸(Convex)多邊形。在多邊形內部任意選擇兩點,作一線段,如果所有的線段跟多邊形的邊都不相交,這個多邊形就是凸多邊形。多邊形是實心的,不可能空心。但是,你可以使用兩個點來創建多邊形,這樣就退化成線段。
創建多邊形時,使用的點必須是逆時針排列(CCW)。我們必須很小心,逆時針是相對於右手座標系統來說的,這座標系下,Z軸指向平面外面。有可能相對於你的屏幕,就變成順時針了,這取決於你自己的座標系統是怎麼規定的。
多邊形的成員變量具有public訪問權限,但是你也應該使用初始化函數來創建多邊形。初始化函數會創建法向量(normal vectors)並檢查參數的合法性。
創建多邊形時,你可以傳遞一個包含頂點的數組。數組大小最多是b2_maxPolygonVertices,這數值默認是8。這已足夠描述大多數的凸多邊形了。
// 按逆時針順序定義一個三角形
b2Vec2 vertices[3];
vertices[0].Set(0.0f, 0.0f);
vertices[1].Set(1.0f, 0.0f);
vertices[2].Set(0.0f, 1.0f);
int32 count = 3;
b2PolygonShape polygon;
polygon.Set(vertices, count);
多邊形有一些定義好的初始化函數來創建箱(box)和邊(edge,也就是線段)。
void SetAsBox(float32 hx, float32hy);
void SetAsBox(float32 hx, float32hy, const b2Vec2& center, float32 angle);
void SetAsEdge(const b2Vec2& v1,const b2Vec2& v2);
多邊形從b2Shape中繼承了半徑。通過半徑,在多邊形的周圍創建了一個保護層(skin)。堆疊的情況下,此保護層讓多邊形之間保持稍微分開。這使得可以在覈心多邊形上執行連續碰撞。
(譯註:這一段我不太明白,原文是
Polygons inherit a radius from b2Shape. The radius creates a skin around the polygon. The skin is used in stacking scenarios to keep polygons slightly separated. This allows continuous collision to work against the core polygon.)
你可以測試一個點是否與形狀有所重疊。使用這個函數, 需要提供一個形狀的變換以及世界座標上的一個點。
b2Transfrom transform;
transform.SetIdentity();
b2Vec2 point(5.0f, 2.0f);
bool hit =shape->TestPoint(transform, point);
(譯註: Box2D中,形狀附加在物體之上,它存儲的數據是在物體的局部座標系下定義的,而傳進來要測試的點是在世界座標系下,座標系不同,就沒有辦法比較。這個transform用於將形狀從局部座標系轉到世界座標系,之後纔可進行比較。而transform的逆轉換就是將世界座標系轉到局部座標系。故要實現這個函數,也可以先求逆轉換,將傳過來的點轉到局部座標系,這同樣可以進行比較,要看哪一個方便。
看Box2D的源碼,它實現b2CircleShape::TestPoint時,是將圓心轉成世界座標系,再比較。而b2PolygonShape::TestPoint,是將傳進來的點先轉成局部座標再比較。因爲轉成世界座標,多邊形要同時轉換多個點,而圓形就只轉換圓心。
使用局部座標系,不管物體怎麼移動,旋轉及縮放,改變的只是這個轉換矩陣,形狀存儲的點不用修改,這樣就很方便了。下面文檔中,你可以看到形狀的很多函數,都會傳進一個轉換,道理是一樣的。)
你可以用光線射向形狀,得到它們之間的交點和法向量。如果在形狀內部開始投射,就當成沒有交點,返回false。
b2Transfrom transform;
transform.SetIdentity();
b2RayCastInput input;
input.p1.Set(0.0f, 0.0f);
input.p2.Set(1.0f, 0.0f);
input.maxFraction = 1.0f;
b2RayCastOutput output;
bool hit =shape->RayCast(&output, input, transform);
if (hit)
{
b2Vec2 hitPoint = input.p1 +output.fraction * (input.p2 - input.p1);
}
看看b2RayCastInput的定義,除指定了兩個點p1, p2外,還有個maxFraction。這個maxFraction是什麼意思呢? 我們知道,兩點決定一個直線,在數學上知道了兩點,再定義直線上的其它點,常使用參數方程。也就是定義 P(fraction) = p1 + fraction * (p2 - p1)。當fraction爲0時,就代表p1, 當fraction=1時,就代表p2。這樣的定義下,兩點之間的線段就是參數從0到1之間變化。參數小於0,表示反向的點,大於1就表示正向超出線段的點。maxFraction就表示要測試的點對應的參數是在[0, maxFraction]內。b2RayCastOutput也有個fraction,意思是一樣的。
數學上很喜歡將一些變量歸結成0到1之間變化,這叫做規範化。處理問題的常用手段是用某個變換(這裏說的變換是廣義的)將變量歸結成0到1之間,再在規範化之下計算, 之後再用個反變換得到原問題的答案。上面說的直線參數化,可以看成規範化的一種。那爲什麼要規範化呢?因爲這樣計算起來方便。那爲什麼會方便呢?我就答不出來了。Box2D中凡是涉及到向量的,那個單詞fraction應該都是上面說的意思。)
碰撞模塊含有對等函數,要傳遞兩個形狀,計算出結果。包括:
• 接觸manifolds
• 距離
• 撞擊時間
(譯註: 我不知道Manifold應該翻譯成什麼,我猜Manifold指有相同屬性的一堆東西,可以理解成集合,但翻譯成集合又不好。故直接保留英文)
Box2D有些函數用來計算重合形狀之間的接觸點。考慮一下圓與圓,圓與多邊形的碰撞,我們只會得到一個接觸點和一個向量。多邊形與多邊形的碰撞,我們可以得到兩個接觸點。這些接觸點都具有相同的法向量,所以Box2D就將它們歸成一組,構成manifold結構。接觸求解器進一步處理這結構,以改善物體堆疊在一起時,系統的穩定性。
通常你不需要直接計算接觸manifold, 但你可能會希望使用這模擬過程中已處理好的結果。
b2Manifold結構含有一個法向量和最多兩個的接觸點。向量和接觸點都是相對於局部座標系。爲方便接觸求解器處理,每個接觸點都存儲了法向衝量和切向(摩擦)衝量。
b2WorldManifold結構可以用來生成世界座標下的接觸向量和點。你需要提供b2Manifold結構和形狀的轉換及半徑。
b2WorldManifold worldManifold;
worldManifold.Initialize(&manifold,transformA, shapeA.m_radius,
transformB,shapeB.m_radius);
for (int32 i = 0; i <manifold.pointCount; ++i)
{
b2Vec2 point =worldManifold.points[i];
}
模擬過程中,形狀會移動而manifold可能會改變。接觸點有可能會添加或移除。你可以使用b2GetPointStates來檢查狀態。
b2PointState state1[2], state2[2];
b2GetPointStates(state1, state2,&manifold1, &manifold2);
if (state1[0] == b2_removeState)
{
// process event
}
b2Distance函數可以用來計算兩個形狀之間的距離。距離函數需要兩個形狀,轉成b2DistanceProxy。There is also some caching used to warm start the distance function for repeated calls.(看不明白,見諒)。詳細見b2Distance.h文件。
如果兩個形狀快速移動,它們可能會在一個時間步內穿過對方。
b2TimeOfImpact函數用於確定兩個形狀運動時碰撞的時間。這稱爲撞擊時間(time of impact, TOI)。b2TimeOfImpact的主要目地是防止隧穿效應。特別是,它設計來防止運動的物體隧穿過靜態的幾何形狀而出到外面。
這個函數考慮了形狀的旋轉和平移,但如果旋轉足夠大,這函數還是會錯過碰撞。函數會報告一個非重疊的時間,並捕捉到所有的平移碰撞。
撞擊時間函數最開始時定義了一條的分離軸,並確保形狀沒有超過這條軸。這可能會在結束位置錯過一些碰撞。但這方法很快,在防止隧穿方面也已經足夠了。
很難去限定旋轉角的範圍,有些情況下,就算是很小的旋轉角也會導致錯過碰撞。通常,就算錯過了一些碰撞,也不會影響到遊戲的好玩性。
這函數需要兩個形狀(轉成b2DistanceProxy)和兩個b2Sweep結構。b2Sweep結構定義了形狀的開始和結束時的轉換。
你可以在固定旋轉角的情況下去執行這個計算撞擊時間的函數,這樣就不會錯過任何碰撞。
Box2D使用b2DynamicTree來高效地組織大量的形狀。這個類並不知道形狀的存在。取而代之,它通過用戶數據指針來操作軸對齊包圍框(AABB)。
動態樹是分層的AABB樹。樹的每個內部節點都有兩個子節點。左邊的子節點是用戶的AABB。
這樹結構支持了高效的光線投射(ray casts)和區域查詢(region queries)。比如,場景中有數百個形狀,你想對場景執行光線投射,如果採用蠻力,就需要對每個形狀都進行投射。這是很低效的,並沒有利用到形狀的分佈信息。替代方法是,你維護一棵動態樹,並對樹進行光線投射。在遍歷樹的時候,可以跳過大量的形狀。
區域查詢使用樹來找到跟需查詢的AABB有重疊的所有葉節點。這比蠻力算法高效得多,很多形狀會被直接跳過。
通常你並不會直接用到動態樹。你會通過b2World類來執行光線投射和區域查詢。如果你想創建自己的動態樹,你可以去看看Box2D是怎麼使用動態樹的。
物理步內的碰撞處理可以分成兩個階段: narrow-phase和broad-phase。narrow-phase時,我們去計算兩個形狀之間的接觸點。假設有N個形狀,使用蠻力算法的話,就需要執行 N*N/2次narrow-phase。
Tb2BroadPhase類使用了動態樹來減少管理數據方面的開銷。這可以大幅度減少narrow-phase的調用次數。
通常你不會直接和broad-phase交互。Box2D自己會在內部創建並管理broad-phase。另外要注意,b2BroadPhase是設計用於Box2D中的物理模擬,它可能不適合處理其它情況。