65.9K
CodeProject 正在变化。 阅读更多。
Home

制作 2D 物理引擎:形状、世界和积分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2017 年 11 月 13 日

CPOL

6分钟阅读

viewsIcon

17628

表示形状、世界,并在 2D 物理引擎中积分力与速度

制作 2D 物理引擎:系列

这是制作 2D 物理引擎系列中的第三篇文章。如果您还没有阅读本系列中本篇文章之前的所有文章,我强烈建议您绕道快速浏览一下。

  1. 制作 2D 物理引擎:数学部分
  2. 制作 2D 物理引擎:空间和物体
  3. 制作 2D 物理引擎:形状、世界和积分
  4. 制作 2D 物理引擎:质量、惯性和力

引言

本文重点介绍物理引擎中几个最重要的方面:形状,它表示实体占据的空间;世界,它管理着刚体(以及更多)并且本质上是物理引擎的接口;以及力与速度的积分,这是任何物理模拟的基本构建块。到本文结束时,您的屏幕上应该会有一个在重力作用下真实下落的物体!这可能看起来微不足道,但这是完成我们物理引擎的重要一步。

为了了解本文结束时应该实现的效果,请查看我正在使用 Rust 编写的、与本系列文章配套的2D 物理引擎这个提交

Shapes

形状(或碰撞体,或您想称呼的任何名称)代表我们世界中物体占据的物理空间或范围。在上一篇文章中,我们讨论了 Body,以及它如何通过表示物体来构成物理引擎的基础。然而,Shape 构成了 Body 的基础。没有 ShapeBody 就什么都不是——一个没有真正物理存在的点对象。虽然点对象在物理学的某些领域非常有用,但我们用不到它们。

Shape 应该提供其他感兴趣的功能,例如生成边界体积或执行射线交叉,但为了降低复杂性,将在需要时再进行介绍。

圆形

我敢肯定您一定听说过普通的圆形。对于初学者来说,维基百科的圆形文章提供了一个很好的介绍。圆形形状非常受欢迎;它几乎被用于所有 2D 游戏或物理模拟中。您可以用它来模拟车轮、球、冰球——可能性无穷无尽。

表示圆形

由于所有形状都将与一个刚体相关联,因此我们 Circle 实现所需的一切就是半径。

struct Circle : Shape {
    float radius;
}

凸多边形

凸多边形是另一种常用的 2D 形状。凸多边形比圆形更通用:可以用它来表示盒子、角色、墙壁等。凹多边形也在物理引擎中使用,但我们稍后会讨论它们。

表示凸多边形

ConvexPolygon 有两个主要组成部分:顶点和边法线。需要记住的一个重要事项是顶点的顺序。从边法线的计算到碰撞检测和射线交叉,一切都依赖于凸多边形顶点的顺序。为了达到所有目的,我们将按照惯例,将顶点视为按逆时针顺序排列。虽然边法线可以(并且确实)从顶点本身计算得出,但由于它们使用非常频繁,因此在创建时预先计算它们会更简单、更高效。

将严格遵循的另一个约定是,索引 i 处的法线对应于顶点 i(i + 1) % vertices.count 之间的边。

struct ConvexPolygon : Shape {
    Vec2[] vertices;
    Vec2[] normals;
}

世界

世界是引擎的核心——它在每个时间步管理和更新刚体、查询、关节、碰撞检测等。它可以被视为一组可以进行物理相互作用的对象的集合。从某种意义上说,宇宙就是一个 World;它里面的所有物体都通过物理力相互作用。

目前,我们的 World 仅包含一个刚体列表,但所有额外的引擎功能将或多或少地由它控制。

struct World {
    List<Body> bodies;
}

世界更新

World 最重要的功能可能是每时间步更新属于它的所有物理实体。世界更新期间会发生很多事情,包括碰撞检测和解析,以及更新刚体和关节。

我们当前的世界更新模型很简单,但会随着 World 的定义而扩展。

void Update(deltaTime) {
     for body in this.bodies {
         body.Update(deltaTime);
     }
}

在这个世界更新的定义中,我们遇到了两件新事物:body.UpdatedeltaTime,这两者将在下一节中介绍。

集成

现在让我们来讨论一个比前两篇文章都更与物理相关的课题:力与速度的积分。

运动方程

正如您在高中时学到的,三个基本的运动方程(包括平移和旋转)如下:

$\begin{aligned}
\vec{v} = \int {\vec{a}dt} &\hbox{ 或 } \vec{v} = \vec{a} \cdot t \\
\vec{x} = \int {\vec{v}dt} &\hbox{ 或 } \vec{x} = \vec{v} \cdot t \\
\vec{F} = m\vec{a} &\hbox{ 或 } \vec{a} = \frac{\vec{F}}{m} \hbox{(牛顿第二定律)} \\
\end{aligned}$

我们将使用这些方程来计算每个时间步的刚体的位置和旋转,但我们不会使用符号积分,而是使用数值积分。数值积分最适合物理引擎,因为我们将以非常小的时间步长(约 0.01 秒)处理所有内容,而且我们无法预测加速度和速度的变化。

半隐式(辛)欧拉

市面上有不少数值积分方法,但我们将使用半隐式(辛)欧拉积分。它直观且高效,同时保持高精度。在我们的辛欧拉积分实现中,我们只需将变量的变化(上述方程中的被积函数)添加到变量本身即可。

回顾上面定义的运动方程。位置 (x) 取决于速度 (v),速度取决于加速度 (a)。因此,我们在每个时间步以相反的顺序进行评估:首先是加速度,然后是速度,最后是位置。由于此步骤必须为每个 Body 执行,因此它将放在 Body.Update 内部。

Vec2 acceleration = force / mass + gravity;        // Gravity will always act on the body
velocity += acceleration * deltaTime;
position += velocity * deltaTime;

现在我们来讨论 deltaTime 的重要性。它作为我们运动方程中数值积分系统的微分((dt))。

类似地,我们可以对旋转进行积分

Vec2 angular_acc = torque / inertia;
angular_vel += angular_acc * deltaTime;
rotation += angular_vel * deltaTime;

力与力矩管理

外部代理(包括物理引擎)与刚体的主要交互机制将是通过力(以及间接的力矩)。我们确保没有力或力矩会“泄露”到下一个时间步,因此在当前时间步结束时(Body.Update),我们会清除所有力与力矩。

force = Vec2.Zero;
torque = 0;

与刚体进行的任何交互都应在每个帧或每个时间步的基础上进行。因此,施加到刚体上的所有力与力矩仅对当前帧有效。力在现实生活中也是这样工作的,但由于我们的世界是连续的而不是离散的,并且没有任何“时间步”,因此不明显。在一个时间步中,刚体总是在最后进行更新,因此此时所有力与力矩都应该已经被施加。

后续步骤

到目前为止,您应该能够真实地模拟刚体的运动(和重力)了!这可能看起来微不足道,但力是任何物理模拟中最重要的部分。既然我们已经掌握了基础知识,我们就可以继续进行一些更高级但仍然非常基础的内容:碰撞。

历史

  • 2017 年 11 月 13 日:初始发布
  • 2017 年 11 月 18 日:添加 GitHub 项目链接
© . All rights reserved.