精通 C++游戏动画编程(OpenGL 和 Vulkan 的高级游戏动画技术) - 8 碰撞检测简介

在上一章中,我们扩展了实例动画系统。 我们首先为动画变换添加了查找表,并将计算移至 GPU。 接着,我们在应用程序中添加了运动状态和 UI 控件,以创建状态与动画片段之间的映射。 最后一步,我们更新了 YAML 解析器来保存和恢复动画片段映射。

本章中,我们将为实例实现一个双层碰撞检测系统。 我们将从探讨碰撞检测的复杂性开始,讲解如何通过基于距离剔除实例和简化实例表示来降低复杂度。 然后,我们将讨论进一步简化实例的方法,以最小化交叉检测的次数。 接着,我们将实现四叉树来限制需要检测的实例数量,最后为实例添加包围球来构建双层碰撞检测体系。

在本章中,我们将涵盖以下主题:

  • 碰撞检测的复杂性
  • 利用空间划分降低复杂度
  • 简化实例以加速碰撞检测
  • 添加四叉树来存储附近的模型实例
  • 实现包围球
  • 碰撞检测的复杂性

    在决定如何实现可视化选择时,我们已经在第 3 章讨论过碰撞检测的复杂性,无论是使用射线投射还是缓冲区绘制。 我们选择将实例绘制到单独的缓冲区中,完全避免了碰撞检测。

    现在是时候对这个复杂主题进行简要回顾,并提出加速实例间碰撞检测的解决方案了。

避免简单粗暴的方法

如果我们检查虚拟世界中每个实例的所有三角形与其他所有实例的三角形之间的碰撞, 这将带来巨大的处理成本。这种简单的暴力碰撞检测会呈指数级增长,当添加越来越多的实例时,将无法维持合理的帧时间。

与其采用简单粗暴的解决方案,我们应该退一步思考,在实现任何碰撞检测之前考虑可能的简化类型。

一个思路是减少需要检测的实例数量。 何必理会虚拟世界中遥远区域的实例? 即便我们的对象是子弹、火箭或其他飞行实体,所有可能碰撞的目标都只需位于"一臂之距"范围内。 其余对象皆可安全忽略。

为实现这种优化,我们可以将世界划分为不同区域。 这样突然只需检测当前所在区域。 根据所用算法,可能还需检测相邻区域,但需要检测的实例总数能大幅减少。

另一方面,通过减少待检测的表面元素数量,可以降低碰撞检测的计算量——只需判断是否接近实例的三角面片即可。 任何形式的简化都有帮助;需要测试的交点越少越好。

将实例表示为立方体或球体可能会产生许多误判结果,但如果针对实例外围立方体或球体的碰撞检测已经失败,我们可以立即将该实例从潜在碰撞目标列表中排除。

这两种思路相结合——减少需要检测的实例数量并降低实例检测的复杂度——有助于实现实时碰撞检测,即使面对大量细节模型时也能胜任。

让我们从虚拟世界的空间划分开始讲起。

使用空间分区降低复杂度

本节我们将探讨几种将世界空间划分为不同区域的方法,从而减少每个分区内的实例数量。 首先从二维或三维空间划分中最简单的变体——网格开始介绍。

网格

网格中,虚拟线条将虚拟世界划分为大小相等的正方形或矩形,或者大小相等的立方体和长方体。

图8.1:二维和三维网格

虽然网格易于创建,但图 8.1 已经展示了一些问题。 大于网格间距的对象必须被放置到所有重叠的网格区域中,这需要检查所有受影响的区域是否存在其他实例。

尽管网格的绝大部分区域仍将保持空白,但虚拟世界中的“拥挤区域”可能导致单个网格单元内出现多个实例。 实例数量越多意味着需要执行更多检查,而实例分布不均可能因计算量激增导致性能下降。

四叉树可视为网格结构的进阶方案。 它能有效解决网格系统中实例分布不均的问题。

四叉树

四叉树的基本构成单元是方形或矩形的独立单元格。 本文仅以方形单元格为例进行说明,但这些原理同样适用于矩形单元格。 对象会通过其位置和尺寸信息被插入根单元格,通常使用包裹对象范围的二维边界框来实现。

四叉树的神奇之处始于当每个单元格中的对象数量达到可配置阈值时。 受影响的单元格会被细分为四个大小相等的子单元格。 根据具体实现方式,与一个或多个子单元格重叠的对象可以保留在父单元格中,也可以添加到所有受影响的子单元格中。 其余所有对象则会被移至相应的子单元格。 参见图 8.2 中的四叉树示例:

图8.2:具有不同细分层级的四叉树

将正方形划分为四个子单元格,并将对象从父单元格移动到子单元格中, 使得每个单元格中的对象数量低于配置阈值,从而最小化需要检测碰撞的对象数量。

如果四个子单元格内所有对象的总和低于阈值,所有对象将再次移至父单元格,同时清空的子单元格会被删除。 这种动态行为有助于将每个单元格中的对象数量维持在零到阈值之间,不受单元格大小、父级数量或位置的影响。

四叉树仅能存储物体位置和大小的二维信息。 要将相同逻辑扩展到三维空间,可以使用八叉树。

八叉树

四叉树与八叉树的基本功能完全相同。 唯一区别在于树结构元素所使用的维度数量: 四叉树以正方形或矩形作为单元格,而八叉树则由立方体或长方体构成。 如图 8.3 所示为一个简单八叉树:

 

图8.3:一个带有细分结构的八叉树

插入的对象在内部以三维轴对齐边界框的形式维护,表示对象的范围。 当达到阈值时进行分割操作,会创建八个子立方体作为子单元(或八个子长方体)。

八叉树是一种高效的方法,可在检查潜在碰撞时排除大部分三维空间。 另一种处理二维和三维空间分割的数据结构是二进制空间分割 BSP)。

二进制空间分割

你可能听说过老游戏中常提到的 BSP 这三个字母。 最早使用 BSP 树来管理关卡数据的游戏之一是 1993 年 《毁灭战士》,由 Id Software 开发。

尽管《毁灭战士》中的关卡数据仅是二维的,但游戏引擎营造出了完全三维游戏的错觉。

BSP 树通过递归地使用直线(2D)或平面(3D)作为超平面来分割世界空间,从而创建前侧和后侧。 这种前后分割会持续进行,直到剩余分区满足某些退出条件;对于游戏而言,这个条件通常是当分区完全被填充或为空时。

如果在细分过程中碰到其他直线或平面,这些直线或平面会被分成两部分,一部分位于前侧,另一部分位于后侧。

图 8.4 展示了空间细分过程及生成的 BSP 树:

 

图 8.4:一个示例对象及其生成的 BSP 树

图 8.4 中,以线段 A 为起点,其正面朝下。 将空间分割成两半的同时,线段 B 和 C 也被分割,从而得到线段 B1、B2、C1 和 C2。 线段 B1 位于 A 的背面,将作为左子节点添加到 A 上;而 B2 同样位于 A 的背面,则作为 A 的右子节点。 线段 C1 和 C3 都位于 B1 和 B2 的背面,因此它们分别作为左子节点添加到 B1 和 B2 上。

虽然解析 BSP 树来寻找分区非常快速,但生成同样的 BSP 树却是一项耗时的任务。 在大多数情况下,树的生成是离线完成的,预先计算好的树会随游戏或应用程序一起发布。 检查所有线段或平面对其他所有线段或平面的关系,这与我们在碰撞检测中遇到的问题类型相同。

由于无法快速更改或更新 BSP 树的元素,这种树仅适用于静态数据(例如游戏中的关卡数据)。 动态游戏元素,如门或玩家,需要使用不同的数据结构,例如八叉树。

与 BSP 树类似,k-d 树在搜索元素时速度很快,但在创建或更新时速度较慢。

K-d 树

k-d 树用于存储 k 维空间中对象的信息。 该算法相比之前的树结构稍显复杂。 每次插入数据点时,剩余空间会被分割成两部分,分割完成后将切换至下一个待处理维度。

对于二维 k-d 树,分割维度在 X 轴和 Y 轴之间交替进行;三维 k-d 树则按照 X、Y、Z 轴的顺序循环分割;更高维度的情况以此类推。 图 8.5 展示了二维和三维 k-d 树的结构示意图:

 

图 8.5:二维和三维的 k-d 树

红色线条和对应的蓝色线条代表二维空间中的分割维度:红色表示 X 轴分割,蓝色表示 Y 轴分割。 在三维空间中,这一模式同样适用,我们会在连续分割时使用"下一个"维度。 由于移除了部分剩余空间,在 k-d 树中搜索元素非常高效。k-d 树的主要用途是处理点云数据以及搜索给定点的最近邻。

另一种略有不同的空间划分方法是使用包围体层次结构。

包围体层次结构

与之前的树变体不同, 包围体层次结构可以通过不同类型的几何表示来实现。 例如,我们使用二维包围圆,如图 8.6 所示:

 

图8.6:由圆形构成的包围体层次结构

通过将两个或多个边界圆封装到一个更大的边界圆中,可以减少碰撞检测的次数。 如果可能发生碰撞的对象没有碰到外圆,就无需检查内部任何圆是否可能发生碰撞。 对于碰撞对象而言,内部圆是不可能触及的。

只有当我们触及外圈时,才需要进行更深入的检查。 与其他树结构类似,包围体层次结构能够将世界空间中的大部分区域从后续碰撞检测中排除。

在开始实现空间分区算法之前,我们需要探讨加速碰撞检测的第二种方法:使用实例的简化表示以进行更快检测。

简化实例以加速碰撞检测

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

akluse

失业老程序员求打赏,求买包子钱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值