空间数据结构
在游戏程序中,利用空间数据结构加速计算往往是非常重要的优化思想,空间数据结构可以应用于场景管理、渲染、物理、游戏逻辑等方面
link
19.1 空间数据结构 - Justin的文章 - 知乎
四叉树/八叉树
四叉树索引的基本思想是将地理空间递归划分为不同层次的树结构。
它将已知范围的空间等分成四个相等的子空间,如此递归下去,直至树的层次达到一定深度或者满足某种要求后停止分割
松散四叉树/八叉树:解决边界问题
四叉树/八叉树的一个问题是,物体有可能在边界处来回,从而导致物体总是在切换节点,从而不得不更新四叉树/八叉树。
而松散四叉树/八叉树正是解决这种边界问题的一种方式:
首先它定义一个节点有入口边界(inner boundary),出口边界(outerboundary)。
那么如何判定一个物体现在在哪个节点呢?
若物体还没添加进四叉树/八叉树,则检测现在位于哪个节点的入口边界内;
若物体先前已经存在于某个节点,则先检测现在是否越出该节点的出口边界,若越出再检测位于哪个节点的入口边界内。
在非松散的四叉树/八叉树中,入口边界和出口边界是一样的。
而松散四叉树/八叉树的松散,是指出口边界比入口边界要稍微宽些(各节点的出口边界也会发生部分重叠,松散比较符合这种描述),从而使节点不容易越过出口边界,减少了物体切换节点的次数
在一篇关于松散四叉树/八叉树的论文里,实验表明出口边界长度为入口边界2倍时可以表现得很好
link
应用
· 场景管理
四叉树
· 碰撞检测
不同划分区域保证不会碰撞的情况下,就能快速过滤与本物体不同区域的其他潜在物体碰撞
· 光线追踪
用八叉树来划分3D空间区域,从而过滤掉大量不必要的区域
四叉树实现
层次包围盒树(BVH)
层次包围盒树(BVH树)是一棵多叉树,用来存储包围盒形状。
它的根节点代表一个最大的包围盒,其多个子节点则代表多个子包围盒。
此外为了统一化层次包围盒树的形状,它只能存储同一种包围盒形状。
计算机的包围盒形状有球体/AABB/OBB/k-DOP,若不清楚这些形状术语可以自行搜索了解。
常用的层次包围盒树有AABB层次包围盒树和球体树。
AABB包围盒树
把不同形状粗略用AABB形状围起来看作一个AABB形状(为了统一化形状),然后才建立层次AABB包围盒树
在物理引擎里,由于物理模拟,大部分形状都是会动态更新的,例如位移/旋转都会改变形状。于是就又有一种支持动态更新的层次包围盒树,称之为动态层次包围盒树。它的算法核心大概:形状的位移/旋转/伸缩更新对应的叶节点,然后一级一级更新上面的节点,使它们的包围体包住子节点
层次包围盒实现
球体包围盒树
球体是最容易计算的一类包围盒,而且球体树构造速度可以很快,因此球体树可被用作粗略松散但快速的空间划分结构(复杂物体计算圆心挺麻烦的…)
构建步骤
- 计算出包围所有三角边顶点的最小球体包围盒,作为根节点
- 以球心为坐标系原点,其坐标系X轴划分在该X轴左右的三角形,并将这些分别放入左子节点、右子节点中
- 重复步骤1、2,最后得到一颗球体树
在步骤2中,还可以按X轴、Y轴、Z轴的顺序轮流划分,即第一次步骤2划分用X轴,第二次步骤2划分用Y轴
球体树粗糙,但是平衡效果不差,且它构造时间复杂度只有O(NlogN)
动态层次包围盒
构建步骤
1.构建一个初始的BVH:使用一种构建算法(例如SAH、Binned SAH等),将所有物体递归地分割成较小的组,并将这些组放入树中的叶子节点中。在这个过程中,您需要为每个节点计算它所包含的物体的包围盒
2.监听物体变化:如果您的应用程序中的物体是动态的,那么您需要能够监听它们的变化(例如,当物体的位置或形状发生变化时)。一旦发现这些变化,您需要执行下面的步骤
3.更新叶节点:找到包含被修改物体的叶节点,并更新它的包围盒。这可能会导致该节点的父节点和祖先节点的包围盒也需要更新
4.展开树:从被修改物体的叶节点开始,展开整个树,一直到根节点。对于每个节点,您需要计算其子节点的包围盒,然后更新该节点的包围盒。这可能会导致该节点的父节点和祖先节点的包围盒也需要更新
5.重建树:如果在展开树的过程中发现某些节点不再满足BVH的质量标准(例如,SAH或Binned SAH),则需要对这些节点进行重建。重建过程涉及到递归地将节点分割成更小的组,并创建新的节点来替换旧节点
节点的添加
1 |
|
节点的删除
1 |
|
节点的更新
1 |
|
层次包围盒树的应用
1.碰撞检测
在Bullet、Havok等物理引擎的碰撞粗测阶段,使用一种叫做 动态层次AABB包围盒树(Dynamic Bounding Volume Hierarchy Based On AABB Tree) 的结构来存储动态的AABB形状。
然后通过该包围盒树的性质(不同父包围盒必定不会碰撞),快速过滤大量不可能发生碰撞的形状对
2.射线检测/挑选几何体
射线检测从层次包围盒树自顶向下检测是否射线通过包围盒,若不通过则无需检测其子包围盒。
这种剪裁可让每次射线检测平均只需检测O(logN)数量的形状。
通过一个点位置快速挑选该点的几何体也是类似的原理
3.视锥剔除
对BVH树进行中序遍历的视锥测试,如果一个节点所代表的包围盒不在视锥范围内,那么其所有子节点所代表的包围盒都不会在视锥范围内,则可以跳过测试其子节点。在这个遍历过程中,通过测试的节点所代表的几何体才可以发送渲染命令
4.光线追踪过滤
光线追踪渲染,可使用BVH来进行基于物体的划分,这样光线测试也可以过滤掉大量不必要的碰撞物体,效率一般比基于空间的划分还要好上一些
101中的部分截图
AABB核心代码
1 |
|
BVH核心代码
1 |
|
5.辅助BSP树的构建
在BSP树的构建中,利用球体树辅助,可以将复杂度从O(Nlog^2^N)下降为O(NlogN)的复杂度
BSP树
随时渲染硬件的进步,基于BSP树的渲染慢慢淘汰。但是即使在今天,BSP仍是在其他分支(引擎编辑器)不可或缺的一个重要数据结构
在计算机图形学中以两种明显不同的形式存在,轴对齐树、多边形对齐树
BSP tree在3D空间下其每个节点表示一个平面,其代表的平面将当前空间划分为前向和背向两个子空间,分别对应左儿子和右儿子。
2D空间下,BSP树每个节点则表示一条边,也可以将2D空间划分成前后两部分
多边形对齐BSP
1 | public class BSPTree |
多边形对齐的BSP树有一些有用的属性。 一个是,对于给定的视图,树结构可以严格从后到前(或从前到后)遍历。 这与轴对齐的BSP树相比,后者通常只给出粗略排序的顺序。 确定相机位于根平面的哪一边。 在这个远平面的多边形集合在平表面的集合之外。 对于远平面,采取下一层的划分平面,并确定相机在哪一边。 远平面的子集也是离摄像机最远的子集。 通过继续递归,这个过程建立了严格的前后顺序,可以使用画家的算法来渲染场景。 画家的算法不需要z缓冲区。 如果所有对象都是按照前后顺序绘制的,那么每一个较近的对象就会被画在它后面的对象的前面,这样就不需要z-depth比较了。
link
轴对齐BSP树(K-D树)
整个场景被包围在一个轴对齐的包围盒(AABB)中。 接下来的想法是递归地将这个包围盒划分为更小的包围盒
粗略的前后排序是如何使用轴对齐BSP树的一个例子。 这对于遮挡剔除算法是有用的(章节19.7和23.7),以及通过最小化像素overdraw来减少像素着色器成本。 假设当前遍历一个名为N的节点。 这里,N是遍历开始时的根。 检查N的分割平面,然后递归地在查看器所在平面的一侧继续遍历树。 因此,只有当整个树的一半被遍历时,我们才开始遍历另一边。 由于叶节点的内容没有排序,而且对象可能位于树的许多节点中,因此这种遍历不会给出精确的前后排序。 然而,它给出了一个粗略的排序,这通常是有用的。 与观察者的位置相比,从节点平面的另一侧开始遍历,可以得到粗略的前后排序。 这对于透明排序很有用。 BSP遍历也可以用来测试光线对场景几何的影响,射线的位置只是用来交换观察者的位置
link
K-DOP
KSP的一种
UE4 K-DOP
AssetStore
k-DOP是一种包围体,是K离散导向多面体(K discrete oriented polytope)的缩写,它是沿着K个方向的范围的布尔交集(zhuanlan.zhihu.com)(docs.unrealengine.com)。也就是说,一个k-DOP是k个包围平板的布尔交集,是一个包含对象的凸多面体(在2-D中是多边形,在3-D中是多面体)(en.wikipedia.org)。
要构建一个k-DOP,首先要选择k个方向,通常是沿着主轴或对角线的正负方向。然后,对于每个方向,计算对象上的所有顶点在该方向上的最小和最大投影值。这些值定义了沿着该方向的一个包围平板。最后,将所有的包围平板相交,得到一个k-DOP。
Bing大小姐给的代码
1 | # Assume obj is a list of vertices of the object |
插件的思路
1.数据的定义
1 | // Math.Sqrt(2)/2 |
2.核心功能
1 | // Their either was no existing mesh or the user wanted to overwrite it, |
KDOPPolygon.GenerateTangentSpace
1 |
|
KDOPPolygon.Split
方法中的参数planeOrigin表示平面的原点(一个点),planeNormal表示平面的法线方向(一个单位向量)。
首先,使用一个循环遍历多边形的所有边(由顶点Vertices[i]和Vertices[j]组成),并检查它们是否与平面相交。对于每条边,通过计算边向量t与平面法线的点积t_dot_n,来判断是否可能存在交点。如果点积的绝对值大于一个较小的阈值(1e-4f),表示存在可能的交点。
如果存在可能的交点,再计算交点参数d,通过计算平面原点到顶点Vertices[i]的向量与平面法线的点积,除以边向量t与平面法线的点积。如果d的值在0和1之间,表示交点在边上(不包括顶点),则在这个位置将交点插入到多边形的顶点列表中。
接下来,将平面原点沿着平面法线方向稍微向外偏移(planeOrigin -= planeNormal * 0.001f)。
最后,再次遍历多边形的顶点列表,将位于平面负(错误)一侧的顶点从列表中移除,即将多边形中位于平面错误一侧的部分去除。
这样,通过在给定平面上进行分割操作,可以将多边形切割为两个或更多部分,其中一部分位于平面的正(正确)一侧,另一部分位于平面的负(错误)一侧。
1 | // 计算平面和线的交点 |
KDOPPolygon.GenerateKDopPolygons
1 | // Generate the polygons for a K-DOP by iteratively intersecting planes with each other. |
KDOPPolygon.MeshFromPolygons
Mesh顶点数据生成
1 |
|