本文将深入探讨物理引擎中一些最重要的概念:形状、世界以及力和速度的集成。形状代表了实体在世界中所占的空间;世界管理着物体(以及更多),本质上是物理引擎的接口;而力和速度的集成是任何物理模拟的基础构建块。通过本文的学习,将能够在屏幕上实现一个在重力作用下真实下落的对象!虽然这看起来可能并不起眼,但这是完成物理引擎的一个重要里程碑。
形状(或者碰撞体,或者愿意称之为任何其他名称)代表了对象在世界中所占据的物理空间或范围。在之前的文章中,讨论了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.Update
和deltaTime
,这两个都将在下一节中介绍。
现在来讨论一个比前两篇文章的整个内容更与物理学相关的话题:力和速度的集成。
正如在高中所学的,三个基本的运动方程(包括平移和旋转)如下:
$\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;
与物体的任何和所有交互都应该是每帧或每个时间步的。因此,应用于物体的所有力和扭矩应该仅对当前帧有效。这也是真实生活中力的工作方式,但这并不明显,因为世界是连续的,不是离散的,没有“时间步”。在时间步中,物体总是在最后更新,因此所有力和扭矩应该已经应用。