Logo
Box2D-碰撞(五)

Box2D提供了几种几何类型和函数。其中包括:

 

原始几何形状:圆、胶囊体、线段和凸多边形

凸包及相关辅助函数

质量和包围盒计算

本地射线和形状投射

接触曲面

形状距离

通用形状投射

碰撞时间

动态包围体积树

碰撞接口设计可用于刚体模拟之外。例如,您可以在物理之外的游戏其他方面使用动态树。

 

然而,Box2D的主要目的是成为刚体物理引擎。因此,碰撞接口仅包含在物理模拟中同样有用的功能。

 

形状

 

形状基元描述碰撞几何,可以独立于物理模拟使用。至少,您应该了解如何创建它,稍后可以附加到刚体。

 

Box2D形状支持几种操作:

 

测试一个点与形状的重叠

 

针对形状执行射线投射

 

计算形状的AABB

即轴对齐的包围盒。它是一个矩形框,其边与坐标轴(通常是x、y和z轴)平行,因此框不会随形状的旋转而旋转。这使得AABB在碰撞检测和其他计算中非常有用,因为它提供了一种简单而高效的方法来表示对象的大致边界。

计算形状的质量属性

 

圆形

 

圆形具有中心和半径。圆形是实心的。含在物理模拟中有用的功能。

 

b2Circle circle;

circle.center = (b2Vec2){2.0f, 3.0f};

circle.radius = 0.5f;

 

或者

b2Circle circle = {{2.0f, 3.0f}, 0.5f};

 

 

胶囊

 

Capsules有两个中心点和一个半径。这两个中心点分别是连接的两个半圆的中心。

 

 

多边形

 

Box2D中的多边形是实心凸多边形。当连接多边形内部两点的所有线段不穿过多边形的任何边时,多边形就是凸多边形。多边形是实心的,永远不是中空的。多边形必须有3个或更多个顶点。

多边形的顶点是以逆时针顺序(CCW)存储的。我们必须小心,因为逆时针的概念是针对一个右手坐标系,其中z轴指向平面外。根据您的坐标系约定,这可能在屏幕上显示为顺时针。

 

多边形成员是公共的,但您应该使用初始化函数来创建多边形。初始化函数会创建法向量并执行验证。

 

在Box2D中,多边形最多可以有8个顶点,由b2_maxPolygonVertices控制。如果您有更复杂的形状,建议使用多个多边形。

 

创建多边形有几种方法。您可以尝试手动创建它们,但不建议这样做。相反,提供了几个函数来创建它们。

 

例如,如果您需要一个正方形或矩形,可以使用以下函数:

 

b2Polygon square = b2MakeSquare(0.5f);

b2Polygon box = b2MakeBox(0.5f, 1.0f);

提供给这些函数的值是半宽或半高,这与圆形和胶囊体使用半径而不是直径相对应。

 

Box2D还支持圆角多边形。这些是具有厚圆角边的凸多边形。

 

b2Polygon roundedBox = b2MakeRoundedBox(0.5f, 1.0f, 0.25f);

如果您想要一个不在身体原点上居中的矩形,您可以使用偏移框。

 

b2Vec2 center = {1.0f, 0.0f};

float angle = b2_pi / 4.0f;

b2Polygon offsetBox = b2MakeOffsetBox(0.5f, 1.0f, center, angle);

如果您想要一个更一般的凸多边形,可以使用b2ComputeHull()计算凸包。然后您可以从凸包创建一个多边形。您可以使其圆角或非圆角。

 

b2Vec2 points[] = {{-1.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 1.0f}};

b2Hull hull = b2ComputeHull(points, 3);

float radius = 0.1f;

b2Polygon roundedTriangle = b2MakePolygon(&hull, radius);

如果您有用于生成凸多边形的自动过程,可以向b2ComputeHull()提供一个退化点集。在创建多边形之前,您应该检查凸包是否成功创建,否则会触发断言。

 

b2Hull questionableHull = b2ComputeHull(randomPoints, 8);

if (questionableHull.count == 0)

{

// 处理失败

}

退化点可能重合和/或共线。为了使凸包可行,封闭区域必须足够正。

 

线段

 

线段是线段。段形状可以与圆、胶囊体和多边形发生碰撞,但不能与其他线段发生碰撞。Box2D使用的碰撞算法要求两个碰撞形状中至少有一个具有足够正面积。段形状没有面积,因此段-段碰撞是不可能的。

 

b2Segment segment1;

segment1.point1 = (b2Vec2){0.0f, 0.0f};

segment2.point2 = (b2Vec2){1.0f, 0.0f};

 

// 等效

b2Segment segment2 = {{0.0f, 0.0f}, {1.0f, 0.0f}};

 

幽灵碰撞

 

在许多情况下,游戏环境是通过将多个段形状端对端连接而构建的。当多边形沿着段链滑动时,可能会产生意外的现象。在下图中,有一个与内部顶点发生碰撞的方框。这些幽灵碰撞是由多边形与内部顶点发生碰撞生成内部碰撞法线时引起的。

 

如果edge1不存在,这种碰撞看起来是正常的。但是如果存在edge1,内部碰撞看起来像是一个错误。但通常当Box2D碰撞两个形状时,它们是单独看待的。

 

如何处理“平滑段”以避免幽灵碰撞。

 

### 什么是平滑段?

 

平滑段使用“幽灵顶点”来避免物体在段与段之间连接处发生错误的碰撞。

 

### 如何设置平滑段?

 

1. **定义平滑段**:

   - `b2SmoothSegment` 是一个结构体,包含两个幽灵顶点和一段线段。

   - 这些幽灵顶点帮助Box2D识别并避免幽灵碰撞。

 

2. **示例代码**:

   ```c

   b2SmoothSegment smoothSegment = {0};

   smoothSegment.ghost1 = (b2Vec2){1.7f, 0.0f};

   smoothSegment.segment = (b2Segment){{1.0f, 0.25f}, {0.0f, 0.0f}};

   smoothSegment.ghost2 = (b2Vec2){-1.7f, 0.4f};

   ```

 

3. **注意事项**:

   - 幽灵顶点需要与相邻平滑段的顶点对齐。这需要精确的设置,否则容易出错。

幽灵顶点的设计并不总是为了延长实际线段的方向,而是为了帮助引擎识别从特定方向进入的碰撞,并防止物体在两段之间“穿过”或发生非预期的碰撞。通过将幽灵顶点设置在与实际顶点不完全一致的地方,可以更好地处理复杂形状的碰撞检测,并为引擎提供足够的信息来计算物体如何滑过这些连接点。

“one-sided collision” 指的是 Box2D 对碰撞的处理是单向的。也就是说,碰撞只会在物体从某一特定方向接触几何体时被检测到,而从另一方向则不会。

 

### 如何创建平滑段链?

 

- **使用链定义**:通过 `b2ChainDef` 定义一系列连接的线段。

- **创建链**:使用 `b2CreateChain()` 函数来自动生成这些平滑段。

 

### 实际步骤

 

1. **准备链定义**:定义一组顶点来表示整个链。

2. **生成链**:调用 `b2CreateChain()`,Box2D会自动处理幽灵顶点的对齐。

 

通过这些步骤,你可以在游戏中有效避免幽灵碰撞,而无需手动逐个设置每个平滑段。这样可以大大减少错误并提高开发效率。

 

这段内容解释了 Box2D 物理引擎中几何查询的几个重要功能。下面我逐一解释:

 

### 1. **几何查询(Geometric Queries)**

你可以对一个形状进行几何查询,来检测某些特定的几何关系或计算相关信息。

 

#### **形状点测试(Shape Point Test)**

这个功能用来检查一个点是否位于某个形状内部。你需要为形状提供一个变换(transform)和一个世界坐标点。例如:

 

```cpp

b2Vec2 point = {5.0f, 2.0f};

bool hit = b2PointInCapsule(point, &myCapsule);

```

 

在这个例子中,`b2PointInCapsule` 函数用于检测点 `point` 是否在 `myCapsule` 这个形状内部。类似的函数还有 `b2PointInCircle()` 和 `b2PointInPolygon()`。

 

#### **光线投射(Ray Cast)**

光线投射允许你将一条射线(ray)投向一个形状,并获取射线与形状的第一个交点及该交点的法向量(normal vector)。

 

```cpp

b2RayCastInput input;

input.origin = (b2Vec2){0.0f, 0.0f};

input.translation = (b2Vec2){1.0f, 0.0f};

input.maxFraction = 1.0f;

 

b2CastOutput output = b2RayCastPolygon(&input, &myPolygon);

if (output.hit == true)

{

    // do something

}

```

 

这里需要注意的是,如果射线起点在凸形状(如圆或多边形)内部,则不会检测到任何交点,因为 Box2D 将凸形状视为实心的。

 

#### **形状投射(Shape Cast)**

形状投射类似于光线投射,不过这次你是用一个形状去投射另一个形状。这个过程使用了一种抽象的方式来描述移动的形状,它被表示为一个带有半径的点云,这意味着即使输入的数据不是凸形状,算法也只会使用凸部分。

 

```cpp

b2ShapeCastInput input;

input.points[0] = (b2Vec2){1.0f, 0.0f};

input.points[1] = (b2Vec2){2.0f, -3.0f};

input.radius = 0.2f;

input.translation = (b2Vec2){1.0f, 0.0f};

input.maxFraction = 1.0f;

 

b2CastOutput output = b2ShapeCastPolygon(&input, &myPolygon);

if (output.hit == true)

{

    // do something

}

```

 

你还可以使用 `b2ShapeCast()` 函数来将一个点云线性投射到另一个点云上。所有的形状投射函数内部都会使用这个通用的 `b2ShapeCast()`。

 

### 2. **距离计算(Distance)**

`b2ShapeDistance()` 函数可以用来计算两个形状之间的距离。这个距离函数需要将形状转换为 `b2DistanceProxy`(即带有半径的点云)。函数还使用了一些缓存技术,以提高反复调用时的性能,特别是在形状只移动了很小的距离时。

 

### 3. **撞击时间(Time of Impact, TOI)**

当两个形状移动得很快时,它们可能会在一个时间步内相互穿过,这就是“隧道效应”。`b2TimeOfImpact()` 函数用来确定两个移动形状发生碰撞的时间点(TOI),它的主要目的是防止隧道效应。Box2D 在内部使用这个函数来防止移动物体穿过静态形状。

 

`b2TimeOfImpact()` 会识别一个初始分离轴,并确保形状不会在这个轴上交叉。随着形状接近,它会反复调整,直到形状接触或相互错过。尽管这个函数可能会错过最终位置上的一些碰撞,但它非常快速,足以防止隧道效应。

 

这个函数要求输入两个形状(转换为 `b2DistanceProxy`)和两个 `b2Sweep` 结构。`b2Sweep` 结构定义了形状的初始和最终变换。

 

### 4. **接触流形(Contact Manifolds)**

Box2D 有一些函数可以计算重叠形状的接触点。如果是圆形对圆形或圆形对多边形的碰撞,我们通常只能得到一个接触点和一个法向量;而多边形对多边形的碰撞可能会得到两个接触点。这些点共享同一个法向量,因此 Box2D 将它们组合成一个“流形”结构。接触求解器利用这个流形结构来提高叠加物体的稳定性。

 

通常你不需要直接计算接触流形,但你可能会使用到它在模拟中产生的结果。

 

`b2Manifold` 结构包含一个法向量和最多两个接触点。接触点存储了在刚体模拟中计算的法向(冲击力)和切向(摩擦力)的冲量。

 

 

 

好的,让我用一个简单的例子来帮助你理解光线投射和形状投射在游戏或物理模拟中的实际用途。

 

### 1. **光线投射(Ray Cast)**

 

**光线投射的作用**:

光线投射可以用来检测某条直线(射线)是否会碰到物体,以及在哪里碰到。这在很多游戏中都非常有用,比如射击游戏中的子弹、角色视野检测等。

 

**例子:射击游戏中的子弹检测**

想象你在玩一个射击游戏,你控制的角色用枪射出一颗子弹。光线投射就可以帮助游戏确定这颗子弹是否击中了敌人。

 

- **步骤1**:光线投射会从枪口位置发射出一条“射线”,这条射线代表子弹的飞行路径。

- **步骤2**:光线投射会检查这条射线是否碰到敌人(敌人被表示为一个形状)。

- **步骤3**:如果射线碰到敌人,光线投射会告诉你碰到了哪里,比如在敌人的头部、身体或者手臂。

- **结果**:如果射线碰到了敌人,游戏会判定子弹命中敌人,并可能减少敌人的生命值或者让敌人倒下。

 

**光线投射示例代码:**

```cpp

b2RayCastInput input;

input.origin = (b2Vec2){0.0f, 0.0f};  // 子弹发射的位置

input.translation = (b2Vec2){1.0f, 0.0f};  // 子弹飞行的方向

input.maxFraction = 1.0f;  // 射线的最大长度

 

b2CastOutput output = b2RayCastPolygon(&input, &myPolygon);  // 检测是否碰到敌人

if (output.hit == true)

{

    // 这里执行子弹击中敌人后的逻辑,比如减少敌人生命值

}

```

 

### 2. **形状投射(Shape Cast)**

 

**形状投射的作用**:

形状投射可以用来检测一个移动的物体是否会与其他物体碰撞,并且碰撞在哪里。这在游戏中用来处理角色移动、物体推挤、车辆模拟等情况。

 

**例子:角色在迷宫中移动**

想象你在玩一个迷宫游戏,你的角色需要在迷宫中移动而不撞墙。形状投射可以帮助游戏确定角色的移动路径上是否有障碍物(比如墙),并且在碰撞前停止移动。

 

- **步骤1**:形状投射会从角色的当前位置开始,向角色移动的方向投射一个形状(角色的形状)。

- **步骤2**:形状投射会检查这个形状在移动过程中是否会碰到墙壁。

- **步骤3**:如果会碰到墙壁,形状投射会告诉你碰到了哪里,并且让角色停下来,以免穿过墙壁。

- **结果**:角色会在碰到墙壁前停下,不会穿过墙,这样迷宫游戏就可以正常进行。

 

**形状投射示例代码:**

```cpp

b2ShapeCastInput input;

input.points[0] = (b2Vec2){1.0f, 0.0f};  // 角色的起始位置

input.points[1] = (b2Vec2){2.0f, -3.0f};  // 角色的另一个顶点,表示角色的大小

input.radius = 0.2f;  // 角色的半径或大小

input.translation = (b2Vec2){1.0f, 0.0f};  // 角色移动的方向

input.maxFraction = 1.0f;  // 移动的最大距离

 

b2CastOutput output = b2ShapeCastPolygon(&input, &myPolygon);  // 检测是否会碰到墙壁

if (output.hit == true)

{

    // 这里执行角色移动受阻后的逻辑,比如停下或转向

}

```

 

### **总结:**

- **光线投射**用于检测直线(射线)是否会与物体碰撞,常见于射击游戏中的子弹命中检测。

- **形状投射**用于检测一个移动的物体(形状)是否会与其他物体碰撞,常见于角色移动或物体推挤的情况。

 

这两个工具帮助游戏开发者在虚拟世界中处理物体的运动和碰撞,使得游戏体验更加真实和有趣。

 

 

b2DynamicTree被Box2D用来高效地组织大量形状。该对象并不直接了解形状,而是通过带有用户数据整数的轴对齐边界框(b2AABB)进行操作。

 

动态树是一种层次结构的AABB树。树中的每个内部节点都有两个子节点。叶子节点是单个用户AABB。树使用旋转来保持树的平衡,即使在出现退化输入的情况下也是如此。

 

该树结构允许进行高效的射线投射和区域查询。例如,您的场景中可能有数百个形状。您可以通过对场景中的每个形状进行射线投射的蛮力方式来执行一次射线投射。这种方法效率低下,因为它没有利用形状的分散。相反,您可以维护一个动态树,并对树进行射线投射。这会通过树沿着射线遍历,跳过大量形状。

 

区域查询使用树来查找所有与查询AABB重叠的叶AABB。这比蛮力方法更快,因为许多形状可以被跳过。

 

通常,您不会直接使用动态树。相反,您将通过b2World函数进行射线投射和区域查询。如果您打算实例化自己的动态树,可以通过查看Box2D如何使用它来学习如何使用它。还可以参考DynamicTree示例。