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;
}

最终效果:

 

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