转载说明:
原作者:KillerAery
出处:https://www.cnblogs.com/KillerAery/p/10878367.html
四叉树索引的基本思想是将地理空间递归划分为不同层次的树结构。它将已知范围的空间等分成四个相等的子空间,如此递归下去,直至树的层次达到一定深度或者满足某种要求后停止分割。
// 示例:一个四叉树节点的简单结构
struct QuadtreeNode {
Data data;
QuadtreeNode* children[2][2];
int divide; //表示这个区域的划分长度
};
// 示例:找到x,y位置对应的四叉树节点
QuadTreeNode* findNode(int x,int y,QuadtreeNode * root){
if(!root)return;
QuadtreeNode* node = root;
for(int i = 0; i < N && n; ++i){
// 通过diliver来将x,y归纳为0或1的值,从而索引到对应的子节点。
int divide = node->divide;
int divideX = x / divide;
int divideY = y / divide;
QuadtreeNode* temp = node->children[divideX][divideY];
if(!temp){break;}
node = temp;
// 如果归纳为1的值,还需要减去该划分长度,以便进一步划分
x -= (divideX == 1 ? divide : 0);
y -= (divideY == 1 ? divide : 0);
}
return node;
}
四叉树的结构在空间数据对象分布比较均匀时,具有比较高的空间数据插入和查询效率(复杂度O(logN))。
而八叉树的结构和四叉树基本类似,其拥有8个节点(三维2元素数组),其构建方法与查询方法也大同小异,不多描述。
四叉树/八叉树的一个问题是,物体有可能在边界处来回,从而导致物体总是在切换节点,从而不得不更新四叉树/八叉树。而松散四叉树/八叉树正是解决这种边界问题的一种方式:首先它定义一个节点有入口边界(inner boundary),出口边界(outerboundary)。那么如何判定一个物体现在在哪个节点呢?
在非松散的四叉树/八叉树中,入口边界和出口边界是一样的。而松散四叉树/八叉树的松散,是指出口边界比入口边界要稍微宽些(各节点的出口边界也会发生部分重叠,松散比较符合这种描述),从而使节点不容易越过出口边界,减少了物体切换节点的次数。
随之而来一个问题就是,如何定义出口边界的长度。因为太短会退化成正常四叉树/八叉树,太长又可能会导致节点存储冗余的物体。而在一篇关于松散四叉树/八叉树的论文里,实验表明出口边界长度为入口边界2倍时可以表现得很好。
相比网格,四叉树/八叉树主要是多了层次,它们可以进行区域较大的划分,然后可以对各种检测算法进行分区域的剪枝/过滤。下面提几个应用(实际应用面很广):
【1】《游戏编程精粹2(Game Programming Gems 2)》 Mark A. Deloura [2003-12]
【2】松散八叉树 | CLOUDGRASSLAND
【3】一篇论文:Loose octree: a data structure for the simulation of polydisperse particle packings
层次包围盒树(BVH树)是一棵多叉树,用来存储包围盒形状。它的根节点代表一个最大的包围盒,其多个子节点则代表多个子包围盒。此外为了统一化层次包围盒树的形状,它只能存储同一种包围盒形状(计算机的包围盒形状有球体/AABB/OBB/k-DOP,若不清楚这些形状术语可以自行搜索了解)。常用的层次包围盒树有AABB层次包围盒树和球体树。
下图为层次AABB包围盒树。把不同形状粗略用AABB形状围起来看作一个AABB形状(为了统一化形状),然后才建立层次AABB包围盒树。
在物理引擎里,由于物理模拟,大部分形状都是会动态更新的,例如位移/旋转都会改变形状。于是就又有一种支持动态更新的层次包围盒树,称之为动态层次包围盒树。它的算法核心大概:形状的位移/旋转/伸缩更新对应的叶节点,然后一级一级更新上面的节点,使它们的包围体包住子节点。
球体是最容易计算的一类包围盒,而且球体树构造速度可以很快,因此球体树可被用作粗略松散但快速的空间划分结构。快速构造松散球体树的步骤(以三角形物体为例):
在步骤2中,还可以按X轴,Y轴,Z轴的顺序轮流划分,即第一次步骤2划分用X轴,第二次步骤2划分用Y轴…
这样生成的球体树是粗糙的,但是其平衡效果并不差,且最重要的是它的构造时间复杂度只有O(NlogN)。
【1】Game Physics: Broadphase – Dynamic AABB Tree | Ming-Lun “Allen” Chou | 周明倫
【2】《游戏编程精粹5(Game Programming Gems 5)》 Kim.Pallister [2007-9]
【3】Real-Time Collision Detection for Dynamic Virtual Environments
BSP tree是一棵二叉树,中文译名为二维空间分割树,在游戏工业算是老功臣了,第一次被应用用是在1993年的商业游戏《DOOM》上,可是随时渲染硬件的进步,基于BSP树的渲染慢慢淘汰。但是即使在今天,BSP仍是在其他分支(引擎编辑器)不可或缺的一个重要数据结构。
BSP tree在3D空间下其每个节点表示一个平面,其代表的平面将当前空间划分为前向和背向两个子空间,分别对应左儿子和右儿子。
2D空间下,BSP树每个节点则表示一条边,也可以将2D空间划分成前后两部分。
// BSP tree节点结构示例
class BSPTreeNode {
Plane plane; // 平面
BSPTreeNode* front; // 前向的节点
BSPTreeNode* back; // 后向的节点
//Data data; // 数据
};
3D空间下要构造一棵较平衡的BSP树,则需要尽可能每次划分出一个节点时,让其左子树节点数和右子树节点数相差不多:
由于需要进行N次划分,每次划分后,要在子集合里一个个挑选合适的平面(需要logN次遍历),为了评定合适又需要与子集合里所有其它形状比较前后位置(需要logN次比较),因此可以知道BSP树构造的平均时间复杂度为 O(Nlog²N)
判断点在平面前后算法:平面的法向量为(A,B,C),则平面方程为:Ax+By+Cz+D=0
将点(x0,y0,z0)代入方程,得distance=Ax0+By0+Cz0+D
若distance<0,则在平面背后;
若distance=0,则在平面中;
若distance>0,则在平面前方。
由于BSP树构造的平均时间复杂度为O(Nlog²N),因此其往往更适合针对静态物体进行离线构造(预处理)。但在每次对关卡进行细微的改动时,设计师可能需要等待几分钟,这时间虽然不影响程序运行效率,但拖延了开发效率。一个比较好的办法就是快速构造一棵粗略的球体树,借此结构更快的构造BSP树。
此方法虽然生成的是一棵粗略、可能不是最平衡的BSP树,但是只需要O(nlogn)的时间复杂度,每次对关卡做出改动时,其用时可能只有几秒,这对设计者来说是相当理想的等待时间了。
大型室内场景游戏引擎基本离不开portal系统:
但是对于关卡编辑师来说,对每个房间/大厅/走廊/门…手动放置每个portal无疑是极大的工作量。于是有一种利用BSP树自动生成portal的做法,大致做法是:
建议结合看图理解,一个示例:
根据定义,在BSP树找到了3个凸多边形房间。
在各个相邻房间之间创建好portal点对(2个绿点,绿线表示portal平面):
基于portal系统运算得到的视野(进行了2次额外的视野剔除):
portal系统实际上是非常复杂的,但非常有价值(良好优化的室内FPS游戏基本不会缺少它)。由于其适合离线构造的特性,这种系统往往是编辑器程序员所需要使用,这里仅仅只能点下自动生成portal的皮毛,更具体的细节可看本节参考。
从远处到eyeNode处的遍历顺序:
第一次遍历,左中右顺序,从根节点开始,直到eyeNode停止;
第二次遍历,右中左顺序,从根节点开始,直到eyeNode停止。
该BSP树节点代表的数据应该是一个三角形(渲染的基本图元),因为恰好三角形也是个平面形状,因此该BSP树节点代表的平面也就是其数据本身。
而今天对于现代渲染硬件来说,虽然对BSP树近到远渲染(从摄像机位置到远处)物体可以减少overdraw(即对像素的重复覆写)开销,但是并不实用,花费昂贵的CPU代价换来少量GPU优化。
【1】BSPTreesGameEngines-1
【2】BSPTreesGameEngines-2
【3】Quake3BSPRendering
【4】Real-Time Rendering SPEEDING UP RENDERING Lecture 04 Marina Gavrilova. - ppt download
【5】BSP技术详解1 - Dreams - 博客园
【6】BSP技术详解2 - Dreams - 博客园
【7】BSP技术详解3 - Dreams - 博客园
【8】BSP技术详解(补充)–pvs算法 - Dreams - 博客园
【9】场景管理–BSP - bitbit - 博客园
【10】《游戏编程精粹5(Game Programming Gems 5)》Kim.Pallister [2007-9]
【11】《游戏编程精粹6(Game Programming Gems 6)》Michael Dickheiser [2007-11]
k-d树是一棵二叉树,其每个节点都代表一个 k维坐标点:
实际上,k-d树就是一种特殊形式的BSP树(轴对齐的BSP树)。
// 一种实现方式示例:二维k-d树节点
class KdTreeNode{
Vector2 position; // 位置
int dimension; // 当前所属层的维度
KdTreeNode* children[2]; // 两个子树
//Data data; // 数据
};
举例,一棵k-d树(k=2)的结构如图:
根据第一层划分维度为X,第二层为Y,第三层为X,所以该k-d树(k=2)对应代表划分的空间,看起来应该是这样的:
【1】k-d tree算法原理及实现 – 磊磊落落的博客
一个自定义区域一般是一个凸多边形,然后可通过一些编辑器手动设置好其各顶点位置,最终手工划分出一块凸多边形区域。3D凸多面体一般很少用,即使在要求划分区域属于同一XOZ面不同高度的3D世界里,考虑到性能,可能更适合用凸多边形+高度来划分区域。
此外一提,能不用凹多边形就不用。因为许多程序算法都可以应用在凸多边形上,而相对应用于凹多边形上可能行不通或者得用更低效的算法
为了达到自定义区域之间的无缝衔接,游戏程序还往往采用图(或者树)结构来存储这些自定义区域,表示它们之间的联系。
// 自定义区块示例
class Chunk{
Data data; // 区域数据
std::vector<Vector2> vertexs; // 区域凸多边形顶点
std::vector<Chunk*> neighbors; // 邻近区域
};
既然用到了凸多边形区域,那就顺便提一提如何判断目标点是否在凸多边形区域,而且也不是很难:
目标点p对凸多边形每个顶点之间建立一个向量vec(如:v1-p),该向量与其对应的顶点的边edge(如:v2-v1)进行叉乘,得到一个叉积值。若每个叉积值的符号都一样(都是正数/都是负数),则证明点在凸多边形内。否则,则证明点不再凸多边形内。
举个例子:
因为 sign((v4−p)×(v5−v4))≠sign((v2−p)×(v3−v2)),所以可知目标点不在凸多边形内。
bool Chunk::inChunk(Vector2 p){
int size = vertexs.size();
for(int i = 0; i < size; ++i){
// 假设凸多边形的边edge都是逆时针方向
Vector2 edge = vertex[(i+1)%size]-vertex[i];
Vector2 vec = vertex[i] - p;
int result = cross(edge,vec);
// 若点在凸多边形内,得到的叉积值应都是正数
if(sign(result) == 0)return false;
}
return true;
}
显而易见的,该算法时间复杂度为O(|V|),V为凸多边形顶点数。
自定义区域是非常灵活的,往往可以应用于任何游戏,特别适合非规则世界的游戏。
这样可以实现一些基本的地图载入衔接,在相应的Chunk能渲染远处本该看到的地图块。
总的来说,游戏开发最常用的空间数据结构是四叉/八叉树、BVH树,而BSP树基本上只能应用于编辑器上,k-d树则几乎没有可以应用的地方。简单整理了如下表格:
数据结构 | 适用情形 | n的数量级 | 构造所需时间 | 是否可以动态更新 | 占用空间 |
---|---|---|---|---|---|
四叉树 | 场景管理(基于地形或不含高度)、渲染 | 物体数量 | O(n*logn) | 是 | 大(取决于区域大小和物体数量) |
八叉树 | 场景管理、渲染 | 物体数量 | O(n*logn) | 是 | 大(取决于区域大小和物体数量) |
BVH树 | 任何情形(包括物理、渲染) | 物体数量 | O(n*logn) | 是 | 中(取决于物体数量) |
BSP树 | 编辑器、复杂室内场景 | 平面数量 | O(n*log²n) | 否 | 大(取决于平面数量) |
k-d树 | - | 物体数量 | O(knlogn) | 否 | 中(取决于物体数量) |