构建2D物理引擎:形状、世界与力的集成

本文将深入探讨物理引擎中一些最重要的概念:形状、世界以及力和速度的集成。形状代表了实体在世界中所占的空间;世界管理着物体(以及更多),本质上是物理引擎的接口;而力和速度的集成是任何物理模拟的基础构建块。通过本文的学习,将能够在屏幕上实现一个在重力作用下真实下落的对象!虽然这看起来可能并不起眼,但这是完成物理引擎的一个重要里程碑。

形状

形状(或者碰撞体,或者愿意称之为任何其他名称)代表了对象在世界中所占据的物理空间或范围。在之前的文章中,讨论了Body,以及它是如何通过代表对象来形成物理引擎的基础的。然而,Shape构成了Body的基础。没有形状,Body就什么都不是——一个没有实际物理存在的点对象。虽然在物理学的某些领域中点对象非常有用,但这里不需要它们。

Shape还应该提供其他有趣的功能,比如生成边界体积或执行射线相交,但为了简化复杂性,这些将在需要时覆盖。

相信一定听说过谦逊的圆形。对于初学者来说,Circle Wikipedia article提供了一个很好的介绍。圆形形状非常流行;它几乎被用于所有的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 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{ or } \vec{v} = \vec{a} \cdot t \\ \vec{x} = \int {\vec{v}dt} &\hbox{ or } \vec{x} = \vec{v} \cdot t \\ \vec{F} = m\vec{a} &\hbox{ or } \vec{a} = \frac{\vec{F}}{m} \hbox{(Newton's Second Law)} \\ \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;

与物体的任何和所有交互都应该是每帧或每个时间步的。因此,应用于物体的所有力和扭矩应该仅对当前帧有效。这也是真实生活中力的工作方式,但这并不明显,因为世界是连续的,不是离散的,没有“时间步”。在时间步中,物体总是在最后更新,因此所有力和扭矩应该已经应用。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485