Octree 瞭然於胸

       今天是北京時間 2020 年 6月25 - 端午節的下午4點33分,由於北京出現疫情的原因,今天沒有和女朋友出去玩,加上現在外面下着冰雹,固而心情很舒暢,便想着把之前一直想寫卻沒有時間寫的 octree  做一下筆記,以便以後閱讀。

       開始還是需要點開熟悉的wiki來查詢一下定義:

      An octree is a tree data structure in which each internal node has exactly eight children. Octrees are most often used to partition a three-dimensional space by recursively subdividing it into eight octants. Octrees are the three-dimensional analog of quadtrees. The name is formed from oct + tree, but note that it is normally written "octree" with only one "t". Octrees are often used in 3D graphics and 3D game engines.

下面用我撇腳的英語翻譯一下:Reference

https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/introduction-to-octrees-r3529/

八叉樹是一種樹數據結構,其中每個內部節點恰好具有八個子節點。八叉樹通常用於通過將三維空間遞歸細分爲八個 ”octants“來劃分三維空間。八叉樹是四叉樹的三維模擬。該名稱由oct +樹組成,但請注意,通常將其寫爲“ octree”,只有一個“ t”。八叉樹通常用於3D圖形和3D遊戲引擎中。  ”octants“ 這個東西是個什麼玩意?又不得不點開去wiki看看 。

octants:這個東西原來是八分圓的意思:實體幾何中的八分圓是歐幾里得三維座標系的八個劃分之一,該座標系由座標的符號定義。它類似於二維象限和一維射線。八分圓的一般化稱爲”orthant“,orthant 就是象限的意思。圖片長成了這樣:

 

看到這裏對octant 還是不理解的可以這樣想,四叉樹用來分隔二維空間,它的四個象限就可以把二維空間分成四份,而八叉樹用來分隔三維空間,多了一個z方向的維度,可以把空間分成八份看圖也可以很容易理解.

先看看今天主角八叉樹在空間和定義的數據結構中長成啥樣。

左邊是在3維空間中的劃分,右邊是樹形的數據結構上的樣子。

瞭解了八叉樹什麼樣子,下一步就是需要知道具體怎麼使用它了,首先找了一篇 介紹很好的八叉樹的文章,準備結合自己的理解翻譯一下 注:感覺作者有些很容易理解的地方說的太詳細了,對於這些地方我選擇省略並以一個初學者的角度來具體闡述一下,紅色字體是自己目前理解的不代表作者觀點。

八叉樹介紹:

什麼是八叉樹? 如果你不是完全瞭解 它,那麼我建議你讀這篇介紹他的wiki 文章(閱讀時間大概爲 5分鐘).

下面的這段話我認爲是一堆廢話 可以直接略過

這是一個充分描述了它是什麼但是幾乎不會的告訴你它用來做什麼以及怎麼執行它。通過這篇文章我會盡自己最大努力通過

概念上的解釋 圖片 代碼來一步步的引導你來創建八叉樹結構,並且向你展示每一個步驟都要考慮的事,我不希望這篇文章成爲權威的創建八叉樹的文章,但是它應該會作爲了解八叉樹的一個入口和好的借鑑。

條件

開始閱讀前,讓我來給作爲讀者的你做幾個假設(其實覺得應該翻譯爲 具備的條件):

1 你很喜歡用c風格語法來編程(我將使用 c#和 XNA)注: XNA是微軟提供的 基於.net framework 的一個可託管的 框架集,但是我將用 unity 和 c#來編程,因爲XNA我不會用,呵呵

2  你過去曾經編寫過類似與樹形結構的結構體,例如二叉樹並且熟悉遞歸,知道它的優點和缺點。注:遞歸好處編寫程序簡潔,缺點需要創建佔用一大堆的棧空間,來入棧,出棧,效率很低。

3 您知道如何使用邊界矩形,邊界球和邊界平截頭體進行碰撞檢測。注:通過 AABB(軸對齊包圍盒)或者 OBB(有向包圍盒) 或者Bound Sphere 包圍球。AABB 相對於 OBB的優勢是 OBB會根據物體的旋轉會重新計算,而 AABB不會,但是很顯然 OBB計算的會更加精確

4 你已經很好的掌握了 數據結構(數組,鏈表等等)並且懂得 大O 表示法(你也可以通過 this GDnet article注也就是 用大O表示法來表示 空間複雜度時間複雜度。

5 您有一個開發環境項目,其中包含需要碰撞測試的空間對象。

準備

假設我們正在構建一個非常大的遊戲世界,其中可以包含成千上萬個各種類型,形狀和大小的物理對象,其中某些對象必須相互碰撞。我們需要在每一幀中找出哪些對象彼此相交,並有某種方式來處理相交。我們如何在不影響性能的情況下做到這一點

 

蠻力碰撞檢測

最簡單的方法是將每個對象與世界上的每個其他對象進行比較。通常,您可以使用兩個for循環來執行此操作。代碼看起來像這樣:


foreach(gameObject myObject in ObjList)
		{
			foreach(gameObject otherObject in ObjList) 
			{ 
				if(myObject == otherObject) continue; //avoid self collision check 
				if(myObject.CollidesWith(otherObject)) 
				{ 
				//code to handle the collision 
				} 
			} 
		}

 

從概念上講,這就是我們在圖片中所做的:

 

   每一個紅線都是一個很耗的cpu檢測碰撞測試。自然的,執行上面的代碼會讓你感覺很恐怖,因爲它的時間複雜度是 O(N ^2).

    如果你有 10,000個物體的話,你將會執行 100,000,000(一億)此碰撞測試。我不在乎您的CPU速度有多快或數學代碼的調整程度如何,這些代碼會使您的計算機運行緩慢。如果您以每秒60幀的速度運行遊戲,則每秒計算60 * 1億次。瘋了太瘋狂了這很瘋狂。如果可以避免,則不要這樣做,至少不要使用大量對象。僅當我們僅相互檢查10個項目時纔可接受(100個檢查是可口的)。如果您事先知道您的遊戲只會有很少的對象(即小行星),那麼您可能可以避免使用這種蠻力方法進行碰撞檢測並完全忽略八叉樹,當您開始注意到由於每幀碰撞檢查過多而導致的性能問題時,請考慮一些簡單的針對性優化。

  1. 您當前的碰撞程序需要多少計算?您是否在其中隱藏了平方根計算(即距離檢查)?您是否要進行細粒度的碰撞檢查(像素與像素,三角形與三角形等)一種常見的技術是在測試細粒度碰撞檢查之前對碰撞進行粗略檢查。您可以給您的對象一個封閉的邊界矩形或邊界球,並在細粒度檢查測試之前測試與這些對象的相交,這可能會涉及更多的數學和計算時間。
  2. 您可以減少計算碰撞檢查的次數?如果您的遊戲以每秒60幀的速度運行,您是否可以跳過幾幀?如果您知道某些對象是確定性的,則您可以提前“解決”他們發生的碰撞(即撞球與撞球檯側面這句話不是特別好理解但是不影響下面的閱讀)。您是否可以減少需要檢查碰撞的對象數量?一種技術是將對象分成幾個列表。一個列表可能是您的“固定”對象列表。他們再也不必測試彼此之間是否發生碰撞。另一個列表可能是您的“移動”對象,需要針對所有其他移動對象和所有固定對象進行測試。這樣可以減少必要的碰撞測試次數,以達到可接受的性能水平。
  3.  當性能出現問題的時候您可以移除一些物體的碰撞測試?例如,煙霧粒子可以與物體表面相交互並遵循其輪廓以創建良好的美學效果,但是如果您達到預定義的碰撞檢查限制並決定不再忽略煙霧粒子碰撞,則不會破壞遊戲性。忽略必要的遊戲對象移動當然會破壞遊戲玩法(即玩家的子彈不再與怪物相交)。因此,也許維護要計算的衝突檢查優先級列表將會有所幫助。首先,您要處理高優先級的碰撞測試,如果您沒有達到你設置的閾值,則可以處理較低優先級的碰撞測試。達到閾值時,您將優先級列表中的其餘項目轉儲或推遲到以後進行測試。
  4.  您可以使用更快但仍然簡單的方法來進行衝突檢測以擺脫O(N ^ 2)運行時嗎?如果去除已經進行了碰撞檢查的對象,則可以將運行時間減少到O(N(N + 1)/ 2),這是更快的並且仍然易於實現。 (但從技術上講,它仍然是O(N ^ 2))在軟件工程方面,您結果可能會花費更多的時間,而不是微調錯誤的算法和數據結構來提高性能。成本與收益之比變得越來越不利,現在是時候選擇一種更好的數據結構來處理碰撞檢測了。空間劃分算法是目前總所周知的可以解決運行時檢測碰撞問題的核武器。以較低的前期性能成本,它們會將您的運行時的碰撞檢測減少到對數級。可擴展的優勢和性能優勢很容易抵消開發時間和CPU開銷的前期成本。

 引用:

使用“距離平方”檢查來比較對象之間的距離,以避免使用平方根方法。平方根計算通常使用牛頓逼近法,並且在計算上可能會很昂貴。

空間劃分的概念背景

讓我們退後一步,在深入研究Octrees之前,先了解一下空間分區和樹。如果我們不理解概念上的想法,那麼我們就不希望通過花大量的代碼來實現它。查看上面的蠻力實施,我們實質上是拿遊戲中的每個對象並將它們的位置與遊戲中的所有其他對象進行比較,以查看是否有任何物體在碰撞。所有這些對象都在空間上包含在我們的遊戲世界中。好吧,如果我們在遊戲世界中創建一個封閉框,並弄清楚該封閉框內包含哪些對象,那麼我們將獲得一個空間區域,其中包含一個包含對象的列表。在這種情況下,它將包含遊戲中的每個對象。

我們可以注意到,如果我們在世界的一個角落有一個對象,而在另一邊有另一個對象,則我們並不需要,也不想在每一幀上對它們進行碰撞檢查。浪費寶貴的CPU時間

因此,讓我們嘗試一些有趣的事情!如果我們將世界精確地分爲兩半,則可以創建三個單獨的對象列表。第一個對象列表List A包含世界左半部分的所有對象。第二個列表,列表B,包含世界右半部分的對象。一些對象可能會碰到分界線,因此它們位於分界線的每一側,因此我們將爲這些對象創建第三個列表List C。

 

我們可以注意到,在每個細分中,我們在空間上將世界縮小了一半,並在一半中收集了對象列表。我們可以優雅地創建一個二叉搜索樹來包含這些列表。從概念上講,這棵樹應該看起來像這樣:

 

僞代碼表示 樹形數據結構如下所示

public class BinaryTree 
{ 
	//包含這個節點裏面的所有物體的鏈表
 
	private List m_objectList; 
	
	//這個節點的左右孩子節點
	private BinaryTree m_left, m_right; 
	
	//指向父節點的指針(爲了向上遍歷)
	private BinaryTree m_parent; 
}

我們知道列表A中的所有對象都不會與列表B中的任何對象相交,因此我們幾乎可以消除一半的碰撞檢測次數。我們仍然可以在列表C中找到可以碰到列表A或B中的對象的對象,因此我們必須對照列表A,B和C中的所有對象來檢查列表C中的所有對象。將世界分成越來越小的部分,我們可以進一步將每次必要的碰撞檢查次數減少一半。這是空間分區背後的一般思想。有很多方法可以將世界細分爲樹狀數據結構(BSP樹,四叉樹,K-D樹,OctTree等 這些以後會慢慢寫)。

現在,默認情況下,我們只是假設最佳的劃分是一劃分半,也就是在中間進行分割,這是因爲我們假設所有對象都會在整個世界範圍內均勻分佈。這並不是一個壞的假設,但是某些空間劃分算法可能會根據情況進行分割,以使每一側都有相等數量的對象(比如加權切割),從而使生成的樹更加平衡。但是,如果所有這些對象四處移動,會發生什麼?爲了保持幾乎均勻的劃分,您必須移動分割平面或每幀都要重建樹結構。複雜性的情況會更混亂。因此,爲了實現空間分割樹,我決定每次都刪減中間部分。結果,有些樹最終可能會比另一些樹稀疏一些,但這沒關係--這不會花費太多。

細分還是不細分?是個問題

  • 假設我們有一個只有幾個對象的稀疏區域。我們可以繼續細分空間,直到找到該對象可能的最小封閉區域。但這真的有必要嗎?讓我們記住,創建樹的全部原因是爲了減少執行每一幀所需的碰撞檢查次數-而不是爲每個對象創建一個完美的封閉區域。這是我用來決定是否細分的規則:
  • 如果我們創建一個僅包含一個對象的細分,即使我們可以繼續細分,也應該停止細分。該規則將成爲在八叉樹中定義“葉節點”的標準的重要組成部分。
  • 其他重要標準是爲區域設置最小值。如果您有一個很小的對象,它只有納米大小(或者,天哪,您犯了一個錯誤忘記了初始化對象的大小!),那麼您將繼續細分到可能導致調用堆棧溢出的地步。對於我自己的實現,我將最小的包含區域定義爲1x1x1立方體.這個小立方體中的任何對象都只需使用O(N ^ 2)蠻力運行.
  • 如果一個包含區域不包含任何對象,則不應將其包含在樹中。

我們可以進一步進行一半的空間細分,將2D世界空間劃分爲多個象限。邏輯本質上是相同的,但是現在我們正在測試四個正方形而不是兩個矩形的碰撞。我們可以繼續細分每個正方形,直到滿足終止規則。四叉樹的世界空間表示形式和相應的數據結構如下所示:

 如果四叉樹的細分和數據結構有意義,那麼八叉樹也應該非常簡單。我們只是添加第三個維度,使用邊界立方體而不是正方形,並且具有八個可能的子節點,而不是四個。你們中的有些人可能想知道如果您的遊戲世界具有非立方體尺寸(例如200x300x400),該怎麼辦。您仍然可以使用具有立方尺寸的八叉樹-如果遊戲世界中沒有任何子節點,則某些子節點最終將變爲空。顯然,您需要將八叉樹的尺寸設置爲至少是遊戲世界的最大尺寸。

八叉樹構建

因此,正如您所閱讀的,八叉樹是細分樹的一種特殊類型,通常用於3D空間(或3維尺寸的任何物體)中的對象。我們的封閉區域將是一個三維矩形(通常是一個立方體)。然後,我們將在上面應用細分邏輯​​,並將封閉區域切成八個較小的矩形。如果遊戲對象完全適合這些細分區域之一,我們將其沿樹向下推入該節點的包含區域。然後,我們將遞歸地繼續細分每個結果區域,直到滿足我們終止條件。最後,我們應該期望有一個不錯的樹狀數據結構。

我的八叉樹實現可以包含具有邊界球和或邊界矩形的對象。您會看到很多我用來決定使用它們中那種方式的代碼

就我們的Octree類數據結構而言,我決定爲每棵樹執行以下操作:

  • 每個節點都有一個邊界區域,該邊界區域定義了封閉區域
  • 每個節點都有對父節點的引用
  • 包含八個子節點的數組(使用數組可簡化代碼並提高緩存性能) 包含當前封閉區域內包含的對象列表
  • 我使用字節大小的位掩碼來確定正在使用哪些子節點(以額外複雜性爲代價的優化好處尚有待商榷)
  • 我使用一些靜態變量來指示樹的狀態

下面就是我的 八叉樹執行代碼:

public class OctTree
{ 
	BoundingBox m_region;
	List m_objects; 
	/// 
	/// These are items which we're waiting to insert into the data structure. 
	/// We want to accrue as many objects in here as possible before we inject them into the tree. This is slightly more cache friendly. 
	///  這是我們將要插入到數據結構裏面的一些項
    //我們希望在將它們注入樹之前在這裏儘可能多地累積對象。這對緩存更友好
	
	static Queue m_pendingInsertion = new Queue(); 
	
	/// 
	/// These are all of the possible child octants for this node in the tree. 
	/// 這些是樹中此節點的所有可能的子八分圓
	OctTree[] m_childNode = new OctTree[8]; 
	
	///
	/// This is a bitmask indicating which child nodes are actively being used. 
	/// It adds slightly more complexity, but is faster for performance since there is 
    //only one comparison instead of 8. 
	/// 這是一個位掩碼,指示正在使用哪些子節點
    //它增加了一些複雜性,但是由於只有一個比較而不是8個,因此性能更快。
	byte m_activeNodes = 0;
	
	///
	/// The minumum size for enclosing region is a 1x1x1 cube. 
	/// 最小的封閉區域的大小是長寬高都是1的立方體
	const int MIN_SIZE = 1; 
	
	///
	/// this is how many frames we'll wait before deleting an empty tree branch. Note 
    //that this is not a constant. The maximum lifespan doubles
	/// every time a node is reused, until it hits a hard coded constant of 64 
	/// 這是刪除空樹枝之前我們要等待的幀數。請注意,這不是常數。
    //每次重用節點時,最大壽命都會翻倍,直到達到硬編碼常數64
	int m_maxLifespan = 8; // 
	int m_curLife = -1; //this is a countdown time showing how much time we have left to 
    live 
	
	/// 
	/// A reference to the parent node is nice to have when we're trying to do a tree update. 
	/// 
	OctTree _parent; 
	static bool m_treeReady = false; //the tree has a few objects which need to be inserted before it is complete 
	static bool m_treeBuilt = false; //there is no pre-existing tree yet. 
} 

初始化封閉區域

建立八叉樹的第一步是爲整個樹定義封閉區域。這將是樹的根節點的邊界框,該樹的根節點最初包含遊戲世界中的所有對象。在開始初始化此邊界體積之前,我們需要做出一些設計決策:

  1. 如果對象移到根節點的邊界體積之外怎麼辦?我們是否要調整整個八叉樹的大小,以便包圍所有對象?如果這樣做,我們將不得不從頭開始完全重建八叉樹。如果不這樣做,我們將需要某種方式來處理超出範圍的對象或確保對象永遠不會超出範圍。
  2. 我們如何爲八叉樹創建封閉區域?我們是否要使用預設尺寸,例如200x400x200(X,Y,Z)矩形?還是我們要使用2的冪的立方維?無法細分的最小允許封閉區域應該是什麼?

我個人決定,我將使用尺寸爲2的冪且足夠大以完全封閉我的世界的立方封閉區域。最小的立方體是1x1x1單位區域。有了這個,我知道我總是可以完全細分我的世界並獲得整數(即使Vector3使用浮點數)。我還決定讓我的封閉區域將整個遊戲世界封閉起來,因此,如果某個物體離開該區域,則應將其銷燬。在最小的八分圓內,我將不得不對所有其他對象進行暴力碰撞檢查,但是我實際上並不希望一次有3個以上的對象同時佔據一個小的區域,因此O(N ^ 2)是完全可以接受的。因此,我通常只使用構造函數初始化八叉樹,該構造函數需要區域大小和要插入樹中的項目列表。我覺得這部分代碼非常基礎,因此幾乎不值得顯示這部分代碼,但是爲了完整起見,我將其包括在內。這是我的構造函數:

/*Note: we want to avoid allocating memory for as long as possible since there can be lots of nodes.*/ 
/// 
/// Creates an oct tree which encloses the given region and contains the provided objects. 
/// 

///The bounding region for the oct tree. 
///The list of objects contained within the bounding region 
private OctTree(BoundingBox region, List objList) 
{ 
	m_region = region; 
	m_objects = objList; 
	m_curLife = -1; 
} 

public OctTree() 
{ 
	m_objects = new List(); 
	m_region = new BoundingBox(Vector3.Zero, Vector3.Zero); 
	m_curLife = -1; 
} 

/// 
/// Creates an octTree with a suggestion for the bounding region containing the items. 
/// 

///The suggested dimensions for the bounding region. 
///Note: if items are outside this region, the region will be automatically resized. 
public OctTree(BoundingBox region) 
{ 
	m_region = region;
	m_objects = new List(); 
	m_curLife = -1; 
} 

建立一個初始八叉樹

我是懶惰初始化的忠實粉絲。除非有必要,我儘量避免分配內存或進行工作。對於我的八叉樹,我儘量避免構建數據結構。我們將接受用戶將對象插入數據結構的請求,但實際上直到對它查詢之前,我們才需要構建樹。

這對我們有什麼作用?好吧,我們可以想象構造和遍歷我們的樹的過程計算上多麼昂貴。如果用戶想讓我們把1,000個對象插入到樹中,那麼重新計算每個子序列的封閉區域一千次是否有意義?或者,我們可以節省一些時間進行批量計算呢?我創建了一個“待處理”的項目隊列和一些標誌來指示樹的構建狀態。所有插入的項目都放入待處理隊列中,並且在進行查詢時,這些待處理請求將被刷新並注入到樹中。這在遊戲加載序列中特別方便,因爲您很可能一次插入數千個對象。載入遊戲世界後,注入樹中的對象數量減少了幾個數量級。我的惰性初始化例程包含在我的UpdateTree()方法中。它檢查是否已構建樹,如果樹不存在且有未插入的對象,則構建數據結構。


 /// 
 /// Processes all pending insertions by inserting them into the tree. 
 /// 
 /// Consider deprecating this? 
 private void UpdateTree() //complete & tested 
 { 
	 if (!m_treeBuilt) 
	 { 
		 while (m_pendingInsertion.Count != 0) 
			m_objects.Add(m_pendingInsertion.Dequeue()); 
		BuildTree(); 
	 } 
	 else
	 { 
		 while (m_pendingInsertion.Count != 0)
			Insert(m_pendingInsertion.Dequeue()); 
	 } 
	 m_treeReady = true;
 } 

至於構建樹本身,這可以遞歸完成。因此,對於每個遞歸迭代,我都從邊界區域中包含的對象列表開始,遵循我的終止規則,如果通過,則會創建八個細分邊界區域,這些邊界區域完全包含在封閉區域內。然後,我遍歷給定列表中的每個對象並進行測試,以查看它們中的任何一個是否完全適合我的任何八位圓。如果它們適合,我將它們插入該八分之一的相應列表中。最後,我檢查相應八位字節列表上的計數,並創建新的八叉樹並將其附加到當前節點,並標記我的位掩碼以表示這些子八位字節正在被激活使用。所有剩餘的對象都已從父對象下被取了下來,但不能放到任何子對象上,因此從邏輯上講,這必須是可以包含該對象的最小八分圓。

/// 
/// Naively builds an oct tree from scratch.
/// 從頭構建一顆二叉樹
private void BuildTree() //complete & tested
{
	//terminate the recursion if we're a leaf node 如果是一個葉子節點終止遞歸
   
	if (m_objects.Count <= 1)
		return;
	
	Vector3 dimensions = m_region.Max - m_region.Min;
	
	if (dimensions == Vector3.Zero)
	{
		FindEnclosingCube();
		dimensions = m_region.Max - m_region.Min;
	}
	
	//Check to see if the dimensions of the box are greater than the minimum dimensions
    // 檢測看包圍盒的大小是否超過設定的最小值
	if (dimensions.X <= MIN_SIZE && dimensions.Y <= MIN_SIZE && dimensions.Z <= MIN_SIZE)
	{
		return;
	}
	
	Vector3 half = dimensions / 2.0f;
	Vector3 center = m_region.Min + half;
	
	//Create subdivided regions for each octant
    //爲每一個八分圓創建細分區域
	BoundingBox[] octant = new BoundingBox[8];
	octant[0] = new BoundingBox(m_region.Min, center);
	octant[1] = new BoundingBox(new Vector3(center.X, m_region.Min.Y, m_region.Min.Z), new Vector3(m_region.Max.X, center.Y, center.Z));
	octant[2] = new BoundingBox(new Vector3(center.X, m_region.Min.Y, center.Z), new Vector3(m_region.Max.X, center.Y, m_region.Max.Z));
	octant[3] = new BoundingBox(new Vector3(m_region.Min.X, m_region.Min.Y, center.Z), new Vector3(center.X, center.Y, m_region.Max.Z));
	octant[4] = new BoundingBox(new Vector3(m_region.Min.X, center.Y, m_region.Min.Z), new Vector3(center.X, m_region.Max.Y, center.Z));
	octant[5] = new BoundingBox(new Vector3(center.X, center.Y, m_region.Min.Z), new Vector3(m_region.Max.X, m_region.Max.Y, center.Z));
	octant[6] = new BoundingBox(center, m_region.Max);
	octant[7] = new BoundingBox(new Vector3(m_region.Min.X, center.Y, center.Z), new Vector3(center.X, m_region.Max.Y, m_region.Max.Z));
	
	//This will contain all of our objects which fit within each respective octant.
    //八分圓物體集合
	List[] octList = new List[8];
	
	for (int i = 0; i < 8; i++) 
		octList = new List();
		
	//this list contains all of the objects which got moved down the tree and can be 
    //delisted from this node.
    // 這個鏈表裏面包含了該節點樹中所有移除下來的並且可以刪除的物體
	List delist = new List();
	
	foreach (Physical obj in m_objects)
	{
		if (obj.BoundingBox.Min != obj.BoundingBox.Max)
		{
			for (int a = 0; a < 8; a++)
			{
				if (octant[a].Contains(obj.BoundingBox) == ContainmentType.Contains)
				{
					octList[a].Add(obj);
					delist.Add(obj);
					break;
				}
			}
		}
		else if (obj.BoundingSphere.Radius != 0)
		{
			for (int a = 0; a < 8; a++)
			{
				if (octant[a].Contains(obj.BoundingSphere) == ContainmentType.Contains)
				{
					octList[a].Add(obj);
					delist.Add(obj);
					break;
				}
			}
		}
	}
	
	//delist every moved object from this node.
    //刪除每一個移動的節點
	foreach (Physical obj in delist)
		m_objects.Remove(obj);
		
	//Create child nodes where there are items contained in the bounding region
   // 創建包含在包圍盒區域裏面的 孩子節點
	for (int a = 0; a < 8; a++)
	{
		if (octList[a].Count != 0)
		{
			m_childNode[a] = CreateNode(octant[a], octList[a]);
			m_activeNodes |= (byte)(1 << a);
			m_childNode[a].BuildTree();
		}
	}
	
	m_treeBuilt = true;
	m_treeReady = true;
}
	
private OctTree CreateNode(BoundingBox region, List objList) //complete & tested
{
	if (objList.Count == 0)
		return null;
	OctTree ret = new OctTree(region, objList);
	ret._parent = this;
	return ret;
}

private OctTree CreateNode(BoundingBox region, Physical Item)
{
	List objList = new List(1); //sacrifice potential CPU time for a smaller memory footprint
	objList.Add(Item);
	OctTree ret = new OctTree(region, objList);
	ret._parent = this;
	return ret;
}

更新樹

我們假設我們的樹有很多移動的物體。如果其中任何一個物體移動了,很有可能這些物體會移動出這個封閉的八分圓區域。們如何在保持對象樹結構完整性的同時處理對象位置的變化?

法1:超級簡單的方式,分類並重建所有內容。

Octree的某些實現方式將是在每幀中完全重建整個樹,並丟棄舊的樹。這非常簡單,並且可以正常工作,如果您只需要這種方法,那就選擇簡單的技術。普遍的共識是,每幀重建樹的前期CPU成本比運行暴力衝突檢查便宜得多,並且程序員的時間非常寶貴,無法花費在不必要的優化上。對於那些喜歡挑戰和過度設計事物的人,“垃圾和重建”技術存在一些小問題:

  1. 每次重建樹時,您都會不斷分配和回收內存。分配新內存的成本很小。如果可能,您想通過重用內存來最大程度地減少分配和回收的內存的次數。
  2. 大部分樹都是不變的,因此一遍又一遍地重建相同的樹的分支浪費了CPU時間。

法二:保留已有的樹,更新變化的樹分支

我注意到一棵樹的大多數分支都不需要更新。它們只包含靜止的物體。如果不是隻在每一幀重建整個樹,而是更新樹中需要更新的部分,那不是很好嗎?此技術保留現有樹並僅更新具有移動對象的分支。實施起來有點複雜,但是也很有趣,所以讓我們開始吧!

第一次嘗試時,我錯誤地認爲子節點中的對象只能在樹中上下移動。錯在如果子節點中的對象到達該節點的邊緣,並且該邊緣也恰好是封閉的父節點的邊緣,則該對象需要插入其父節點的上方,甚至可能更遠。但是,重要的是我們不知道需要將這個對象插入多遠的位置。同樣,對於移動的對象,我們想將其整齊地封裝在子節點或該子節點的子節點中。但是我們也不知道把它放到樹下的哪個位置。

幸運的是,由於我們包含了對每個節點父節點的引用,因此我們可以用最少的計算使用遞歸輕鬆地解決此問題!更新算法的基本思想是首先讓樹中的所有對象自行更新。有些可能會移動或改變大小。我們想要獲取每個移動的物體的列表,因此那些有更新的物體應向通過一個方法給我們返回一個布爾值,指示其邊界區域是否已更改。在獲得所有已移動對象的列表之後,我們從當前節點開始並開始遍歷樹,直到找到一個完全包圍了已移動對象的節點(大多數情況下,當前節點仍能封閉該物體)。如果物體沒有被當前節點完全包圍,我們將繼續將其移至下一個父節點。在最壞的情況下,是用根節點去封閉這個物體。

將一些物體儘可能地移到樹上之後,我們也應該將嘗試將其他的儘可能地移到樹下。在大多數情況下,如果我們將物體向上移動後,則無法將其向下移動。但是,如果對象移動了,以便當前節點的子節點可以包含它,我們就有機會將其移動到樹下。能夠將對象也向下移動到樹上很重要,否則所有移動的對象最終都將遷移到頂部,會在碰撞檢測的初期遇到一些性能問題。

移除分支

在某些情況下,對象將移出節點,並且該節點中將不再包含任何對象,也不會再有任何包含對象的子對象。就應該把它移除掉。 這裏隱藏着一個有趣的問題:應該什麼時候移除它比較好?既然分配新內存會花費時間,考慮到如果我們要在幾個週期內重用這個這個節點的區域,爲什麼不讓它停留一會兒呢?但是在維護這個不再使用的節點需要一些成分,我們應該維護多久呢?因此我決定給我的每個節點一個死亡計時器,該計時器在分支失效時激活。如果在死亡計時器處於活動狀態時將某個對象移入該節點的八分圓,將它的壽命加倍,並重置死亡計時器。這樣可以確保經常使用的八分圓會長久的保留下來並且被循環利用,並且不常用的點將會被刪除。

一個常用的例子就是 當您使用機關槍射擊子彈流時。由於這些子彈物體彼此緊隨其後,因此在第一個子彈物體離開節點後立即刪除節點,而第二個進入時候創建新的節點,這是很可惜的。所以如果子彈很多,我們應該儘可能將這些八分圓保留一段時間。如果一個子分支是空的並且已經有一段時間沒有使用了,可以將其從我們的樹中修剪下來是安全的。 無論如何,讓我們看一下實現的代碼。首先,我們有Update()方法。這是在所有子樹上遞歸調用的方法。它會移動所有對象,對數據結構進行一些內部處理,然後將每個移動的對象移動到其正確的節點(父節點或子節點)中。

public void Update(coreTime time)
{
	if (m_treeBuilt == true && m_treeReady == true)
	{
		//Start a count down death timer for any leaf nodes which don't have objects or children.
		//when the timer reaches zero, we delete the leaf. If the node is reused before death, we double its lifespan.
		//this gives us a "frequency" usage score and lets us avoid allocating and deallocating memory unnecessarily
		if (m_objects.Count == 0)
		{
			if (HasChildren == false)
			{
				if (m_curLife == -1)
					m_curLife = m_maxLifespan;
				else if (m_curLife > 0)
				{
					m_curLife--;
				}
			}
		}
		else
		{
			if (m_curLife != -1)
			{
				if (m_maxLifespan <= 64)
					m_maxLifespan *= 2;
				m_curLife = -1;
			}
		}

		List<Physical> movedObjects = new List<Physical>(m_objects.Count);

		//go through and update every object in the current tree node
		foreach (Physical gameObj in m_objects)
		{
			//we should figure out if an object actually moved so that we know whether we need to update this node in the tree.
			if (gameObj.Update(time) == 1)
			{
				movedObjects.Add(gameObj);
			}
		}

		//prune any dead objects from the tree.
		int listSize = m_objects.Count;
		for (int a = 0; a < listSize; a++)
		{
			if (!m_objects[a].Alive)
			{
				if (movedObjects.Contains(m_objects[a]))
					movedObjects.Remove(m_objects[a]);
				m_objects.RemoveAt(a--);
				listSize--;
			}
		}

		//prune out any dead branches in the tree
		for (int flags = m_activeNodes, index = 0; flags > 0; flags >>= 1, index++)
			if ((flags & 1) == 1 && m_childNode[index].m_curLife == 0)
			{
				if (m_childNode[index].m_objects.Count > 0)
				{
					//throw new Exception("Tried to delete a used branch!");
					m_childNode[index].m_curLife = -1;
				}
				else
				{
					m_childNode[index] = null;
					m_activeNodes ^= (byte)(1 << index);       //remove the node from the active nodes flag list
				}
			}

		//recursively update any child nodes.
		for (int flags = m_activeNodes, index = 0; flags > 0; flags >>= 1, index++)
		{
			if ((flags & 1) == 1)
			{
				if(m_childNode!=null && m_childNode[index] != null)
					m_childNode[index].Update(time);
			}
		}



		//If an object moved, we can insert it into the parent and that will insert it into the correct tree node.
		//note that we have to do this last so that we don't accidentally update the same object more than once per frame.
		foreach (Physical movedObj in movedObjects)
		{
			OctTree current = this;
			

			//figure out how far up the tree we need to go to reinsert our moved object
			//we are either using a bounding rect or a bounding sphere
			//try to move the object into an enclosing parent node until we've got full containment
			if (movedObj.EnclosingBox.Max != movedObj.EnclosingBox.Min)
			{
				while (current.m_region.Contains(movedObj.EnclosingBox) != ContainmentType.Contains)
					if (current._parent != null) current = current._parent;
					else
					{
						break; //prevent infinite loops when we go out of bounds of the root node region
					}
			}
			else
			{
				ContainmentType ct = current.m_region.Contains(movedObj.EnclosingSphere);
				while (ct != ContainmentType.Contains)//we must be using a bounding sphere, so check for its containment.
				{
					if (current._parent != null)
					{
						current = current._parent;
					}
					else
					{
						//the root region cannot contain the object, so we need to completely rebuild the whole tree.
						//The rarity of this event is rare enough where we can afford to take all objects out of the existing tree and rebuild the entire thing.
						List<Physical> tmp = m_root.AllObjects();
						m_root.UnloadContent();
						Enqueue(tmp);//add to pending queue

						
						return;
					}

					ct = current.m_region.Contains(movedObj.EnclosingSphere);
				}
			}

				//now, remove the object from the current node and insert it into the current containing node.
			m_objects.Remove(movedObj);
			current.Insert(movedObj);   //this will try to insert the object as deep into the tree as we can go.
		}

		//now that all objects have moved and they've been placed into their correct nodes in the octree, we can look for collisions.
		if (IsRoot == true)
		{
			//This will recursively gather up all collisions and create a list of them.
			//this is simply a matter of comparing all objects in the current root node with all objects in all child nodes.
			//note: we can assume that every collision will only be between objects which have moved.
			//note 2: An explosion can be centered on a point but grow in size over time. In this case, you'll have to override the update method for the explosion.
			List<IntersectionRecord> irList = GetIntersection(new List<Physical>());

			foreach (IntersectionRecord ir in irList)
			{
				if (ir.PhysicalObject != null)
					ir.PhysicalObject.HandleIntersection(ir);
				if (ir.OtherPhysicalObject != null)
					ir.OtherPhysicalObject.HandleIntersection(ir);
			}
		}
	}//end if tree built
	else
	{
		if (m_pendingInsertion.Count > 0)
		{
			ProcessPendingItems();
			Update(time);   //try this again...
		}
	}
}

 

請注意,我們爲移動的對象調用了一個Insert()方法。將對象插入樹的方式非常類似於用於構建初始樹。 Insert()將嘗試將對象儘可能往下推。請注意,如果可以使用子節點中的現有邊界區域,我也將嘗試避免創建新的邊界區域。


/// <summary>
/// A tree has already been created, so we're going to try to insert an item into the tree without rebuilding the whole thing
/// </summary>
/// <typeparam name="T">A physical object</typeparam>
/// <param name="Item">The physical object to insert into the tree</param>
private bool Insert<T>(T Item) where T : Physical
{
	/*if the current node is an empty leaf node, just insert and leave it.*/
	//if (m_objects.Count == 0 && m_activeNodes == 0)
	if(AllTreeObjects.Count == 0)
	{
		m_objects.Add(Item);
		return true;
	}

	//Check to see if the dimensions of the box are greater than the minimum dimensions.
	//If we're at the smallest size, just insert the item here. We can't go any lower!
	Vector3 dimensions = m_region.Max - m_region.Min;
	if (dimensions.X <= MIN_SIZE && dimensions.Y <= MIN_SIZE && dimensions.Z <= MIN_SIZE)
	{
		m_objects.Add(Item);
		return true;
	}

	//The object won't fit into the current region, so it won't fit into any child regions.
	//therefore, try to push it up the tree. If we're at the root node, we need to resize the whole tree.
	if (m_region.Contains(Item.EnclosingSphere) != ContainmentType.Contains)
	{
		if (this._parent != null)
			return this._parent.Insert(Item);
		else
			return false;
	}
	
	//At this point, we at least know this region can contain the object but there are child nodes. Let's try to see if the object will fit
	//within a subregion of this region.

	Vector3 half = dimensions / 2.0f;
	Vector3 center = m_region.Min + half;

	//Find or create subdivided regions for each octant in the current region
	BoundingBox[] childOctant = new BoundingBox[8];
	childOctant[0] = (m_childNode[0] != null) ? m_childNode[0].m_region : new BoundingBox(m_region.Min, center);
	childOctant[1] = (m_childNode[1] != null) ? m_childNode[1].m_region : new BoundingBox(new Vector3(center.X, m_region.Min.Y, m_region.Min.Z), new Vector3(m_region.Max.X, center.Y, center.Z));
	childOctant[2] = (m_childNode[2] != null) ? m_childNode[2].m_region : new BoundingBox(new Vector3(center.X, m_region.Min.Y, center.Z), new Vector3(m_region.Max.X, center.Y, m_region.Max.Z));
	childOctant[3] = (m_childNode[3] != null) ? m_childNode[3].m_region : new BoundingBox(new Vector3(m_region.Min.X, m_region.Min.Y, center.Z), new Vector3(center.X, center.Y, m_region.Max.Z));
	childOctant[4] = (m_childNode[4] != null) ? m_childNode[4].m_region : new BoundingBox(new Vector3(m_region.Min.X, center.Y, m_region.Min.Z), new Vector3(center.X, m_region.Max.Y, center.Z));
	childOctant[5] = (m_childNode[5] != null) ? m_childNode[5].m_region : new BoundingBox(new Vector3(center.X, center.Y, m_region.Min.Z), new Vector3(m_region.Max.X, m_region.Max.Y, center.Z));
	childOctant[6] = (m_childNode[6] != null) ? m_childNode[6].m_region : new BoundingBox(center, m_region.Max);
	childOctant[7] = (m_childNode[7] != null) ? m_childNode[7].m_region : new BoundingBox(new Vector3(m_region.Min.X, center.Y, center.Z), new Vector3(center.X, m_region.Max.Y, m_region.Max.Z));

	//First, is the item completely contained within the root bounding box?
	//note2: I shouldn't actually have to compensate for this. If an object is out of our predefined bounds, then we have a problem/error.
	//          Wrong. Our initial bounding box for the terrain is constricting its height to the highest peak. Flying units will be above that.
	//             Fix: I resized the enclosing box to 256x256x256. This should be sufficient.
	if (Item.EnclosingBox.Max != Item.EnclosingBox.Min && m_region.Contains(Item.EnclosingBox) == ContainmentType.Contains)
	{
		bool found = false;
		//we will try to place the object into a child node. If we can't fit it in a child node, then we insert it into the current node object list.
		for(int a=0;a<8;a++)
		{
			//is the object fully contained within a quadrant?
			if (childOctant[a].Contains(Item.EnclosingBox) == ContainmentType.Contains)
			{
				if (m_childNode[a] != null)
				{
					return m_childNode[a].Insert(Item);   //Add the item into that tree and let the child tree figure out what to do with it
				}
				else
				{
					m_childNode[a] = CreateNode(childOctant[a], Item);   //create a new tree node with the item
					m_activeNodes |= (byte)(1 << a);
				}
				found = true;
			}
		}

		//we couldn't fit the item into a smaller box, so we'll have to insert it in this region
		if (!found)
		{
			m_objects.Add(Item);
			return true;
		}
	}
	else if (Item.EnclosingSphere.Radius != 0 && m_region.Contains(Item.EnclosingSphere) == ContainmentType.Contains)
	{
		bool found = false;
		//we will try to place the object into a child node. If we can't fit it in a child node, then we insert it into the current node object list.
		for (int a = 0; a < 8; a++)
		{
			//is the object contained within a child quadrant?
			if (childOctant[a].Contains(Item.EnclosingSphere) == ContainmentType.Contains)
			{
				if (m_childNode[a] != null)
				{
					return m_childNode[a].Insert(Item);   //Add the item into that tree and let the child tree figure out what to do with it
				}
				else
				{
					m_childNode[a] = CreateNode(childOctant[a], Item);   //create a new tree node with the item
					m_activeNodes |= (byte)(1 << a);
				}
				found = true;
			}
		}

		//we couldn't fit the item into a smaller box, so we'll have to insert it in this region
		if (!found)
		{
			m_objects.Add(Item);
			return true;
		}
	}

	//either the item lies outside of the enclosed bounding box or it is intersecting it. Either way, we need to rebuild
	//the entire tree by enlarging the containing bounding box
	return false;
}

碰撞檢測

最後,我們的八叉樹已經建成,它也有了它應有的樣子。那麼我們如何對其執行碰撞檢測?首先,讓我們列出要查找碰撞的不同方法

  • 視錐相交。我們可能有一個與世界某個區域相交的截錐體。我們只想要與給定的視錐相交的對象。這對於剔除攝像機視圖空間外部的區域以及確定鼠標選擇區域內的對象特別有用。
  • 射線相交。我們可能想要從任何給定點發射定向射線,並想知道最近的相交對象或獲取與該射線相交的所有對象的列表(例如軌道炮)。這對於鼠標拾取非常有用。如果用戶單擊屏幕,則我們向世界繪製光線並找出它碰撞到的東西。
  • 邊界框相交。我們想知道世界上哪些對象與給定的邊界框相交。這對於“盒”形遊戲對象(房屋,汽車等)最有用。
  • 邊界球相交。我們想知道哪些對象與給定的邊界球相交。大多數對象可能會使用邊界球進行粗略的碰撞檢測,因爲數學上的計算成本最低且較爲簡單。

八叉樹的遞歸碰撞檢測處理的主要思想是,您從根/當前節點開始,並讓該節點中所有對象與檢測物做碰撞檢測。讓所有的孩子節點與檢測物做包圍盒測試。如果某個孩子節點的包圍盒測試失敗,那麼就可以忽略還孩子節點下所有的孩子節點。如果孩子節點通過了測試,那麼就可以以同樣的方式來遞歸該孩子節點來做碰撞檢測。每一個節點都應該把這些碰撞記錄傳遞給它上面的調用者,這樣遞歸結束的時候我們就可以拿到與檢測物碰撞到的所有物體,這樣不僅代碼量少,而且性能高。在許多此碰撞中,我們可能會獲得很多結果。但是依據碰撞的對象類型不同,做出不同的類型的碰撞結果。

例如 火藥擊中物體應該發生爆炸,但是它玩家的獎勵的物品時候,就不應該發生爆炸。爲了方便處理碰撞信創建了一個新的類,用來記錄碰撞的結果。它包含碰撞物的引用,碰撞點,碰撞點的法線,當你需要去處理碰撞的結果的時候,這些會很有用。

public class IntersectionRecord
{
	readonly Vector3 m_position, m_normal;

	readonly Ray m_ray;

	readonly Physical m_intersectedObject1, m_intersectedObject2;
	
	readonly double m_distance;

	public class Builder
	{
		public Vector3 Position, Normal;
		public Physical Object1, Object2;
		public Ray hitRay;
		public double Distance;

		public Builder()
		{
			Distance = double.MaxValue;
		}

		public Builder(IntersectionRecord copy)
		{
			Position = copy.m_position;
			Normal = copy.m_normal;
			Object1 = copy.m_intersectedObject1;
			Object2 = copy.m_intersectedObject2;
			hitRay = copy.m_ray;
			Distance = copy.m_distance;
		}

		public IntersectionRecord Build()
		{
			return new IntersectionRecord(Position, Normal, Object1, Object2, hitRay, Distance);
		}
	}

	#region Constructors
	IntersectionRecord(Vector3 pos, Vector3 normal, Physical obj1, Physical obj2, Ray r, double dist)
	{
		m_position = pos;
		m_normal = normal;
		m_intersectedObject1 = obj1;
		m_intersectedObject2 = obj2;
		m_ray = r;
		m_distance = dist;
	}
	#endregion

	#region Accessors
	/// <summary>
	/// This is the exact point in 3D space which has an intersection.
	/// </summary>
	public Vector3 Position { get { return m_position; } }

	/// <summary>
	/// This is the normal of the surface at the point of intersection
	/// </summary>
	public Vector3 Normal { get { return m_normal; } }

	/// <summary>
	/// This is the ray which caused the intersection
	/// </summary>
	public Ray Ray { get { return m_ray; } }

	/// <summary>
	/// This is the object which is being intersected
	/// </summary>
	public Physical PhysicalObject
	{
		get { return m_intersectedObject1; }
	}

	/// <summary>
	/// This is the other object being intersected (may be null, as in the case of a ray-object intersection)
	/// </summary>
	public Physical OtherPhysicalObject
	{
		get { return m_intersectedObject2; }
	}

	/// <summary>
	/// This is the distance from the ray to the intersection point. 
	/// You'll usually want to use the nearest collision point if you get multiple intersections.
	/// </summary>
	public double Distance { get { return m_distance; } }

	#endregion

	#region Overrides
	public override string ToString()
	{
		return "Hit: " + m_intersectedObject1.ToString();
	}
	public override int GetHashCode()
	{
		return base.GetHashCode();
	}
	/// <summary>
	/// check the object identities between the two intersection records. If they match in either order, we have a duplicate.
	/// </summary>
	/// <param name="otherRecord">the other record to compare against</param>
	/// <returns>true if the records are an intersection for the same pair of objects, false otherwise.</returns>
	public override bool Equals(object otherRecord)
	{
		IntersectionRecord o = (IntersectionRecord)otherRecord;
		//
		//return (m_intersectedObject1 != null && m_intersectedObject2 != null && m_intersectedObject1.ID == m_intersectedObject2.ID);
		if (otherRecord == null)
			return false;
		if (o.m_intersectedObject1.ID == m_intersectedObject1.ID && o.m_intersectedObject2.ID == m_intersectedObject2.ID)
			return true;
		if (o.m_intersectedObject1.ID == m_intersectedObject2.ID && o.m_intersectedObject2.ID == m_intersectedObject1.ID)
			return true;
		return false;
	}
	#endregion
}

平截頭檢測


/// <summary>
/// Gives you a list of all intersection records which intersect or are contained within the given frustum area
/// </summary>
/// <param name="frustum">The containing frustum to check for intersection/containment with</param>
/// <returns>A list of intersection records with collisions</returns>
private List<IntersectionRecord> GetIntersection(BoundingFrustum frustum, PhysicalType type = PhysicalType.ALL)
{
	if (!m_treeBuilt) return new List<IntersectionRecord>();

	if (m_objects.Count == 0 && HasChildren == false)   //terminator for any recursion
		return null;

	List<IntersectionRecord> ret = new List<IntersectionRecord>();

	//test each object in the list for intersection
	foreach (Physical obj in m_objects)
	{

		//skip any objects which don't meet our type criteria
		if ((int)((int)type & (int)obj.Type) == 0)
			continue;

		//test for intersection
		IntersectionRecord ir = obj.Intersects(frustum);
		if (ir != null) 
			ret.Add(ir);
	}

	//test each object in the list for intersection
	for (int a = 0; a < 8; a++)
	{
		if (m_childNode[a] != null && (frustum.Contains(m_childNode[a].m_region) == ContainmentType.Intersects || frustum.Contains(m_childNode[a].m_region) == ContainmentType.Contains))
		{
			List<IntersectionRecord> hitList = m_childNode[a].GetIntersection(frustum, type);
			if (hitList != null) ret.AddRange(hitList);
		}
	}
	return ret;
}

攝像機平截頭體常用來做裁剪,只渲染場景中攝像機能看到的物體。下面是一小段渲染的代碼

/// 
/// This renders every active object in the scene database /// 
/// 
public int Render()
{ 
	int triangles = 0; 
	//Renders all visible objects by iterating through the oct tree recursively and testing for intersection 
	//with the current camera view frustum 
	foreach (IntersectionRecord ir in m_octTree.AllIntersections(m_cameras[m_activeCamera].Frustum)) 
	{ 
		ir.PhysicalObject.SetDirectionalLight(m_globalLight[0].Direction, m_globalLight[0].Color); 
		ir.PhysicalObject.View = m_cameras[m_activeCamera].View; 
		ir.PhysicalObject.Projection = m_cameras[m_activeCamera].Projection; 
		ir.PhysicalObject.UpdateLOD(m_cameras[m_activeCamera]); 
		triangles += ir.PhysicalObject.Render(m_cameras[m_activeCamera]); 
	} 
	return triangles; 
} 

射線檢測

/// <summary>
/// Gives you a list of intersection records for all objects which intersect with the given ray
/// </summary>
/// <param name="intersectRay">The ray to intersect objects against</param>
/// <returns>A list of all intersections</returns>
private List<IntersectionRecord> GetIntersection(Ray intersectRay, PhysicalType type = PhysicalType.ALL)
{
	if (!m_treeBuilt) return new List<IntersectionRecord>();

	if (m_objects.Count == 0 && HasChildren == false)   //terminator for any recursion
		return null;

	List<IntersectionRecord> ret = new List<IntersectionRecord>();

	//the ray is intersecting this region, so we have to check for intersection with all of our contained objects and child regions.
	
	//test each object in the list for intersection
	foreach (Physical obj in m_objects)
	{
		//skip any objects which don't meet our type criteria
		if ((int)((int)type & (int)obj.Type) == 0)
			continue;

		IntersectionRecord ir = obj.Intersects(intersectRay);
		if (ir != null)
			ret.Add(ir);
	}

	// test each child octant for intersection
	for (int a = 0; a < 8; a++)
	{
		if (m_childNode[a] != null && m_childNode[a].m_region.Intersects(intersectRay) != null)
		{
			m_lineColor = Color.Red;
			List<IntersectionRecord> hits = m_childNode[a].GetIntersection(intersectRay, type);
			if (hits != null && hits.Count > 0)
			{
				ret.AddRange(hits);
			}
		}
	}

	return ret;
}

與一些物體的碰撞檢測

這是一個測試當前節點下的一組物體與當前孩子節點是否有碰撞的一個特別有用的遞歸方法(看:Update 方法的使用)。這個方法會使用很頻繁也很有效率。

  • 創建一個記錄碰撞結果的鏈表  從根節點出發先讓當前節點之間與父物體之間進行碰撞檢測, 並把碰撞結果插入到這個鏈表中。
  • 節點下的物體之間進行碰撞檢測,並把碰撞結果插入鏈表中,
  • 最後把根節點連同當前節點一起作爲父物體,傳遞到當前節點的子物體中,重複步驟1 進行碰撞檢測。

就上圖而言 通過這種方式我們可以把 11 * 11 的檢測次數減少到 29次,並一共檢測到 4 次碰撞。


private List<IntersectionRecord> GetIntersection(List<Physical> parentObjs, PhysicalType type = PhysicalType.ALL)
{
	List<IntersectionRecord> intersections = new List<IntersectionRecord>();
	//assume all parent objects have already been processed for collisions against each other.
	//check all parent objects against all objects in our local node
	foreach (Physical pObj in parentObjs)
	{
		foreach (Physical lObj in m_objects)
		{
			//We let the two objects check for collision against each other. They can figure out how to do the coarse and granular checks.
			//all we're concerned about is whether or not a collision actually happened.
			IntersectionRecord ir = pObj.Intersects(lObj);
			if (ir != null)
			{

				//ir.m_treeNode = this;


				intersections.Add(ir);
			}
		}
	}

	//now, check all our local objects against all other local objects in the node
	if (m_objects != null && m_objects.Count > 1)
	{
		#region self-congratulation
		/*
		 * This is a rather brilliant section of code. Normally, you'd just have two foreach loops, like so:
		 * foreach(Physical lObj1 in m_objects)
		 * {
		 *      foreach(Physical lObj2 in m_objects)
		 *      {
		 *           //intersection check code
		 *      }
		 * }
		 * 
		 * The problem is that this runs in O(N*N) time and that we're checking for collisions with objects which have already been checked.
		 * Imagine you have a set of four items: {1,2,3,4}
		 * You'd first check: {1} vs {1,2,3,4}
		 * Next, you'd check {2} vs {1,2,3,4}
		 * but we already checked {1} vs {2}, so it's a waste to check {2} vs. {1}. What if we could skip this check by removing {1}?
		 * We'd have a total of 4+3+2+1 collision checks, which equates to O(N(N+1)/2) time. If N is 10, we are already doing half as many collision checks as necessary.
		 * Now, we can't just remove an item at the end of the 2nd for loop since that would break the iterator in the first foreach loop, so we'd have to use a
		 * regular for(int i=0;i<size;i++) style loop for the first loop and reduce size each iteration. This works...but look at the for loop: we're allocating memory for
		 * two additional variables: i and size. What if we could figure out some way to eliminate those variables?
		 * So, who says that we have to start from the front of a list? We can start from the back end and still get the same end results. With this in mind,
		 * we can completely get rid of a for loop and use a while loop which has a conditional on the capacity of a temporary list being greater than 0.
		 * since we can poll the list capacity for free, we can use the capacity as an indexer into the list items. Now we don't have to increment an indexer either!
		 * The result is below.
		 */
	#endregion

	List<Physical> tmp = new List<Physical>(m_objects.Count);
	tmp.AddRange(m_objects);
	while (tmp.Count > 0)
	{
		foreach (Physical lObj2 in tmp)
		{
			if (tmp[tmp.Count - 1] == lObj2 || (tmp[tmp.Count - 1].IsStationary && lObj2.IsStationary))
				continue;
			IntersectionRecord ir = tmp[tmp.Count - 1].Intersects(lObj2);
			if (ir != null)
			{
				//ir.m_treeNode = this;
				intersections.Add(ir);
			}
		}

		//remove this object from the temp list so that we can run in O(N(N+1)/2) time instead of O(N*N)
		tmp.RemoveAt(tmp.Count-1);
	}
}

//now, merge our local objects list with the parent objects list, then pass it down to all children.
foreach (Physical lObj in m_objects)
	if (lObj.IsStationary == false)
		parentObjs.Add(lObj);
//parentObjs.AddRange(m_objects);

//each child node will give us a list of intersection records, which we then merge with our own intersection records.
for (int flags = m_activeNodes, index = 0; flags > 0; flags >>= 1, index++)
{
	if ((flags & 1) == 1)
	{
		if(m_childNode != null && m_childNode[index] != null)
			intersections.AddRange(m_childNode[index].GetIntersection(parentObjs, type));
	}
}

return intersections;
}

最終效果:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章