游戏引擎中的物理学 - 线性运动


前言

现代视频游戏中,可能会有数百个物体在屏幕上移动和互动,看起来非常逼真。这背后是一个物理引擎,它由一系列函数和类组成,用于模拟物体之间的复杂互动,使其在实时环境中表现得更为真实。物理引擎的核心有两个功能:首先,它必须让物体在受力的情况下以符合物理规律的方式移动;其次,它必须检测到物体之间的碰撞,并根据碰撞结果让物体做出相应的反应。在本文中,笔者将带你们学习如何实现真实的物体运动,并且了解如何在游戏模拟中加入加速度、速度和质量等概念。

1. 物理实体

在视频游戏中构建的场景中,可能包含数百个物体,开发者需要考虑这些物体在“真实世界”中模拟的精确度。总体来说,有两个因素会影响物体的物理属性:它们是否能够引发物理互动,以及它们是否会对物理互动做出反应。

在典型的第一人称射击游戏(FPS)中,墙壁可以与玩家发生碰撞,阻止玩家像幽灵一样穿过它们——在某处的游戏代码中,这些墙壁触发了碰撞检测,随后会通过推回玩家或让玩家沿着墙壁滑动的方式来解决碰撞。然而,这些墙壁通常不会对互动做出反应,除了在图形上可能会有一些变化——撞到墙壁并不会使墙壁稍微移动,甚至如果你向墙壁射击,子弹留下的痕迹也只是图形效果,墙壁的物理属性并未因此改变。

在游戏引擎的术语中,上述例子中的墙壁可能具有一个碰撞体积(例如我们在射线投射时使用的轴对齐边界框或球体),用来检测玩家是否与它相交,但缺乏一个物理实体,该实体用于模拟力如何施加到物体上,以及在发生碰撞时物体应该如何反应。这些物理实体可以大致分为三类——粒子、刚体和软体,每一种都可以在游戏中用于模拟不同类型的物体。

粒子

最简单的物理表示形式是粒子。粒子在空间中有一个位置,并且可以赋予其速度和加速度。虽然粒子可能有质量,但它没有方向性;它只是空间中的一个点,表示“某种东西”的存在。这可以看作是视频游戏中常用的“粒子效果”的扩展,用于视觉上表示火焰、烟雾等现象——每个“粒子”都是世界空间中的一个小纹理,它可以移动,但其方向对它所参与的整体模拟并不重要。有时,粒子系统会使用物理计算来提高其逼真度——例如,从燃烧的火焰中飞溅的火花可能会在表面上弹跳,而从喷泉中喷出的水粒子最终可能会在重力作用下被拉向地面。
粒子示意图

刚体

刚体比粒子更进一步——与其说是一组构成形状的粒子,不如说刚体是一个具有体积和方向的单一物体(稍后我们会看到,它还可以通过施加力来改变其方向)。我们可以将刚体看作世界中大多数物体的坚固形状——我们可以轻松确定一个杯子或飞船的体积和方向。虽然杯子(或飞船!)实际上由多个部分组成(如杯盖、杯身和纸板套),更不用说由多个原子构成,但我们可以假设这些部分之间的位置保持固定,因此可以将它们视为一个整体。当力作用于刚体时,刚体不会改变形状或改变其质量在整个体积中的分布,它们始终保持为一个完全“固定”的形状。
刚体示意图

软体

在我们的游戏中,大多数物体可以被假设为刚体——它们永远不会改变形状,除非通过动画来实现。然而,有些物体需要更加精确的物理动态来完整地模拟它们在世界中的运动——比如超级英雄的斗篷在风中飘动,或者一个由软泥制成的玩家角色,它可以根据玩家的输入而拉伸或压扁:
软体示意图

为了实现这样的效果,我们需要使用软体,而不是刚体。与其用一个单一的坚固形状来表示物体的物理属性,不如将物体视为由一组点组成的,这些点通过弹簧相互连接:
软体拆解图
当每个点受到力的作用时(无论是通过与世界的某种碰撞,还是通过手柄控制器直接施加的力),弹簧会拉伸和收缩,将这些力扩散开来,使整个形状移动或“挤压”。如果我们想象这些“点”是一个网格的顶点,那么很容易理解如何通过物理上一致的方式移动这些点,从而产生旗帜飘动效果,或是拉伸/挤压效果。然而,让这些点与周围的世界正确互动要比刚体复杂得多——与简单的立方体或球体(这两者都有简单的方法来判断是否有物体接触它们,如我们将在后续教程中看到的)不同,软体需要进行许多检查,以确定哪些点被世界中的物体推动。

弹簧的效果也很棘手,虽然施加在一个点上的力应该以一致的方式与相连的点相互作用,但微小的浮点误差可能导致软体即使在应该静止时也会“抖动”,如果这些抖动能量积累得足够多,软体可能会变形,甚至完全破裂。

2. 物理术语

在这个教程系列中,我们将接触到许多你可能熟悉或不熟悉的术语。为避免任何歧义,我们在这里对其中的一些术语进行解释。

位移(Displacement)

在图形学中,我们已经熟悉了如何更改物体在世界空间中的位置。在一些与物理计算相关的文献中,你会看到位置有时被称为位移,这个术语特指物体相对于其初始位置随时间移动的距离。位移通常表示为一个向量,其中每个轴上的位移使用米作为标准测量单位。

在任何与求导相关的计算中,我们会使用 p 来表示位置;在经典力学中,实际的位移向量(或我们在前一个模块中所习惯的物体位置向量)还会被用 s 来表示,源自拉丁语的“spatium”,意为空间。

速度(Velocity)

物体位移相对于时间的一阶导数(即物体的位置随时间的变化量)称为速度。与位移类似,速度也是一个三维向量——我们的物体会以一定的速度(速度向量的大小)沿着特定的方向(速度向量的方向)移动。当物理实体的速度向量发生变化时,意味着它要么在减速(向量的大小接近于零),要么在加速(向量的大小增大)。

我们通常用米每秒作为速度的度量单位来描述物体速度的大小——如果物体的速度为 x,那么在1秒后,它将在世界中沿着 x 方向移动 |x| 米;有时,这也会表示为 x m/s 或 ms⁻¹。在物理计算中,当讨论物体位置的变化时,速度通常用 ˙p 表示,有时也直接用 v 表示。

加速度(Acceleration)

一个物体位置相对于时间的二阶导数被称为它的加速度(或者换句话说,加速度衡量的是速度的变化率)。在物理计算中,当专门考虑变化率时,它通常用 ¨p 表示,不太常用的是 ˙v,而在其他计算中出现时通常就用 “a” 表示。在汽车中,我们踩下油门踏板来加速(当然是在合法限速范围内!),通过让发动机做更多的功;这个功改变了汽车的速度,进而随着时间改变了它的位置。加速度通常用米每秒平方来测量,常表示为 m/s²,或者也可以写成 ms⁻²。

最常见的加速度的例子可能就是地球的重力加速度,你可能知道它是 9.8 m/s²。这意味着物理物体以每秒增加 9.8 米的速度向下运动 —— 也就是说,每一秒,速度的变化率会增加 9.8 米每秒,所以物体加速,从而位置变化得越来越快(直到达到终端速度 —— 当撞击空气产生的 “阻力” 减速与加速度的力相等时,就相互抵消了)。

加速度和速度之间的区别很重要 —— 以 9.8 米 / 秒的恒定速度向前移动是相当快的,你可以在 10 秒多一点的时间内完成 100 米短跑;以 9.8 m/s² 的恒定加速度运动意味着 10 秒后你将以 98 米每秒的速度移动,这会让你在奥运会上获得金牌。

力(Force)

要让一个物体移动,必须有一个力作用在它上面。这个力有方向和大小,所以在代码中可以用一个向量来表示。力的单位是牛顿,1 牛顿是使 1 千克的重物以每秒 1 米的加速度运动所需的力。因此,力是对刚体加速度的一种调整。即 F = m a F = ma F=ma(其中 F F F表示力, m m m表示质量, a a a表示加速度)。

质量(Mass)

质量是对一个物体由多少物质组成的度量,用千克来衡量。物质会抵抗力,所以一个物体的质量越大,移动它就越困难。这和那个物体的重量不一样,尽管两者是相关的。重量是一个物体在重力作用下施加的力——在地球上,1 千克的质量由于地球的重力会施加 9.8 牛顿的力,但同样的物体在月球上施加的力较小,因为那里的重力较小,把它往下拉的力就小——所以它“重量较轻”……即使它仍然是 1 千克的质量。这就是为什么尽管在太空中物体没有重量,但它们仍然需要大型火箭发动机——来抵消物体的质量。
在物理引擎中,我们希望在模拟中移动和相互作用的每个物体都将有一个质量值,以克或千克(或从这衍生出的某个值)来衡量。一般来说,我们存储质量的倒数,即 1 m a s s \frac{1}{mass} mass1。这个“逆”质量值有助于将一些除法运算转换为乘法运算,并且还提供了一些我们稍后会看到的有用的副作用。

动量(Momentum)

在物理引擎的文献中,你可能会遇到另一个术语——动量。它只是一个物体的质量和速度的乘积:
p = m v p = mv p=mv
动量的单位是千克·米/秒——这只是动量计算公式中两部分单位的组合。你可能听说过动量守恒。它指的是一个物理系统中总动量应该始终是恒定的——当物体碰撞时,它们会把一部分动量传递给另一个物体,从而减少自己的动量。这就是为什么在斯诺克中,母球撞击另一个球时,母球可能会停下来,但另一个球会加速;系统中的总动量是守恒的,即使速度之和不守恒(母球可能比其他球重或轻,但它仍然会传递动量,使得整个系统保持恒定)。

3. 牛顿运动定律

物体运动的基本原理可以用艾萨克・牛顿的三大运动定律简洁地描述 —— 我们之后对移动的物体所做的一切以及对物体之间碰撞的反应都与这些定律相关,所以在我们继续学习本教程系列的过程中,对这些定律有一些了解会很有用。

牛顿第一定律

第一定律指出,除非受到其他力的作用,否则物体要么保持静止,要么以恒定速度运动。对于我们的物理引擎来说,这意味着一个物体在默认情况下不应该有太多动作!它不应该移动,除非有东西撞击它,或者物体自身施加某个力使其朝一个方向移动。一旦物体开始移动,它应该继续移动,直到有其他力使它减速或改变其方向。想想一艘漂浮在深空中的宇宙飞船——没有东西推它或对它施加其他力(除了来自附近天体的极小的引力,我们现在先忽略它!),所以它可以保持静止。如果宇宙飞船启动火箭发动机一段时间,它就会开始移动。然后它会继续沿那个方向移动,即使发动机再次关闭,因为没有东西作用在它上面使它减速。即使旋转飞船也不会改变运动的方向或速度——除非发动机再次启动。

惯性

牛顿第一定律有时被称为惯性定律。惯性是物体对其速度变化的抵抗,它与物体的质量有关——就像移动一个更重的物体更困难一样,一旦一个物体开始移动,改变它的方向也更困难。

摩擦力与阻尼

在地球上,你可能在骑自行车或开车时注意到,如果你停止蹬踏板或关闭发动机,你不会一直前进,而是会停下来——除非你在下坡!重力对所有有质量的物体施加一个力,因此会把物体拉下坡,但在水平面上,我们会停下来是因为摩擦力——自行车或汽车的轮胎在路面上摩擦,会产生一些力,而且汽车/自行车的车架也会因为空气阻力而减速——这是在空气中移动时产生的摩擦力。计算一个物体所受的摩擦力(来自轮胎或空气)是一个计算上很棘手的问题,所以在游戏的实时物理引擎中我们通常不这么做。相反,我们通过在每次更新时稍微阻尼速度来模拟摩擦力,将速度乘以一个标量值——只要这个值略小于 1.0,它就会使物体慢慢失去速度,就好像被摩擦力减慢一样。

牛顿第二定律

第二定律指出,作用在一个物体上的力的总和等于该物体的质量乘以物体的加速度,即:
F = m a F = ma F=ma

在物理引擎中,我们通常在代码中对物体施加力,而不是直接施加加速度。例如,我们可能会说角色跳跃时施加了 1000 牛顿的力(力的单位就来自这个定律!)。然后,这个力会随着时间被整合到我们的玩家对象的速度和位置中。因此,最好把这个方程重新排列成这样:
a = F m a=\frac{F}{m} a=mF

这样,我们可以把计算一个物体的新加速度看作是所有施加的力的总和除以质量。还记得前面提到在物理引擎中我们通常处理质量的倒数吗?这个方程很好地解释了为什么。我们可以不用除以质量 m m m,而是乘以质量的倒数:
a = F m − 1 a = Fm^{-1} a=Fm1

除法通常比乘法运算慢(也就是说,它需要更多的 CPU 周期),所以通过这样做,我们加快了物理计算的速度。但还有另一个好处!如前所述,质量的倒数使得在我们的游戏中加入我们不想让其移动的物体更容易,只需将质量的倒数设为 0.0:
a = 0 = F ⋅ 0 a = 0 = F·0 a=0=F0

这样,无论有多少力作用在物体上(来自与它碰撞的东西等),它都不会移动,因为它产生的加速度最终也会是零。这对于像关卡的“地面”这样的东西很有用,我们希望玩家能够站在上面并在这个物体上跳跃(需要一些物理交互来解决任何碰撞检测),但我们真的不希望地面移动——想象一下,如果马里奥每次跳跃时马里奥的关卡地面都稍微向下移动一点!

牛顿第三定律

在上面的马里奥例子中,如果不是在我们的计算中使用质量倒数所带来的“无限”质量,你可能会想知道为什么马里奥每次跳跃时地面都会移动。这是由于牛顿第三运动定律,该定律指出,当一个物体对第二个物体施加力时,第二个物体同时在相反方向施加一个大小相等的力;这通常被表述为——对于每一个作用力,都有一个大小相等、方向相反的反作用力。

一般来说,这意味着在我们的物理引擎中,每当发生碰撞时,两个物体最终都会受到力——在斯诺克游戏中,如果母球撞击另一个球,第二个球会受到母球施加的一些力,但母球也会因为通过第二个球施加给它的大小相等、方向相反的力而减速。

在我们的马里奥例子中,这意味着当他跳跃时,实际上应该有一个相反的力作用于整个世界。实际上,跳跃交互可能会被编码为直接将向上的力施加到马里奥身上并跳过这个交互——但现在想想当他再次落地时;那是一个必须被检测和解决的碰撞,质量倒数确保我们在落地时不会推动整个世界。

数值积分

我们知道,物体位置随时间的变化由速度属性描述,而速度的变化率是加速度。要实际执行这些变化,我们需要进行更多的微积分运算,并进行一些积分。正如你可能已经知道的那样,要对一个函数(被积函数)进行积分,我们在越来越小的时间间隔上评估函数的结果,以便逐步更准确地了解函数图像曲线下的面积:
数值积分示意图
在描述速度变化率和加速度时,我们经常看到以下方程:
v = d p d t v=\frac{dp}{dt} v=dtdp a = d v d t a=\frac{dv}{dt} a=dtdv

在描述速度和加速度的积分时,我们会使用这些方程:
v = ∫ a d t v=\int adt v=adt p = ∫ v d t p=\int vdt p=vdt

在每种情况下, d t dt dt是时间的变化量。由于我们正在处理由一系列依次渲染的离散帧组成的实时视频游戏模拟,这个时间变化量是相当明显的——1 秒除以帧率将为我们提供每一帧的 d t dt dt

在物理引擎中,我们通常通过将可能影响一个物体的所有单独的力相加来确定那一帧作用在该物体上的总力——一艘既启动主引擎向前移动,又启动侧推进器向右移动的宇宙飞船,将受到一个总力,这个总力应该使它沿对角线移动。
飞船受力示意图
在这里,我们可以使用前面提到的方程 a = F m − 1 a = Fm^{-1} a=Fm1来确定宇宙飞船正在承受的加速度大小。为了实际确定宇宙飞船的新位置,应该对加速度值进行积分,以确定宇宙飞船速度的变化量,然后从那里对速度进行积分以确定飞船在当前帧中的位置变化。

然而,有一个问题。如果飞船在 1 秒后以 10 米/秒²加速,在 2 秒后以 25 米/秒²加速,那么在 1.5 秒时物体的加速度是多少?仅从这些时间点我们无法真正知道加速度是否是线性增加的,所以将这些视为离散变化与“现实生活”条件相比可能会导致不准确。这使得我们每秒更新物理系统的次数非常重要——我们将每一秒分割成更多的离散帧(然后通过该帧的 d t dt dt进行积分),物理系统就能够更准确地模拟加速度和速度的变化,并且我们就能更接近“真实”答案。

在物理引擎中,有许多常用的数值积分方法来确定模拟中物理对象在每一帧的新位置,每一种方法都有不同的特点和缺点。

显式欧拉法

显式欧拉积分(有时简称为“欧拉积分”)是所讨论的积分方法中最简单的一种。在每次模拟更新中,我们确定以下新值:
v n + 1 = v n + a n d t v_{n+1}=v_n + a_ndt vn+1=vn+andt
p n + 1 = p n + v n d t p_{n+1}=p_n + v_ndt pn+1=pn+vndt

这表明对于我们下一帧的值(表示为 n + 1 n + 1 n+1)是通过取当前帧的值(表示为 n n n),并加上导数乘以时间步长而形成的。我们隐含地知道当前帧的加速度是多少,因为如我们之前所见,在该帧中应用的任何力都可以相加,并乘以质量的倒数来获得它。

隐式欧拉法

显式欧拉积分并不完美——由于加速度的积分,速度在整个时间步长内应该是变化的,但我们只是将其简单地视为一个恒定的增量来改变位置。对于任何随时间变化速度量的物体,这种近似随着时间的推移会导致不准确。一个更完整的积分方法应该是在下一步计算导数,如下所示:
v n + 1 = v n + a n + 1 d t v_{n+1}=v_n + a_{n + 1}dt vn+1=vn+an+1dt
p n + 1 = p n + v n + 1 d t p_{n+1}=p_n + v_{n + 1}dt pn+1=pn+vn+1dt
这被称为隐式欧拉法,有时也称为“向后欧拉法”积分。对于从完整数据集进行积分来说,这是可行的,因为“下一个”速度和加速度将可用于积分。但对于实时模拟来说,这并不是很好,除非我们确切地知道下一帧将应用什么加速度(也许它以固定速率不断增加,或者是其他一些简单的函数),因为我们的游戏交互可能会导致每一帧都有独特的碰撞和力的组合。我们可以尝试通过将导数拟合到曲线来预测导数,但预测可能是错误的,从而导致不准确,而这正是我们要避免的!

半隐式欧拉法

在隐式欧拉积分和显式欧拉积分之间有一个中间地带,称为半隐式欧拉(有时也称为辛欧拉)积分。在这种情况下,我们使用当前状态积分二阶导数(加速度),然后使用更更新的状态积分一阶导数(速度):
v n + 1 = v n + a n d t v_{n + 1}=v_n + a_ndt vn+1=vn+andt
p n + 1 = p n + v n + 1 d t p_{n + 1}=p_n + v_{n + 1}dt pn+1=pn+vn+1dt

在实践中,这就像简单地交换我们计算物体新速度和位置的顺序一样,使其计算速度与显式欧拉法一样快,但更准确,因此随着时间的推移不太可能导致问题。

维莱特(Verlet)积分法

如果我们知道一个物体的当前位置、前一个位置以及这两个测量值之间的时间步长,我们就可以直接确定速度,而不必单独存储它。这是维莱特积分法的基础:
p n + 1 = p n + ( p n − p n − 1 ) + a n d t 2 p_{n + 1}=p_n+(p_n - p_{n - 1})+a_ndt^2 pn+1=pn+(pnpn1)+andt2

这种方法有时用于在 GPU 上计算的粒子系统,因为重建速度的成本可能小于从另一个存储速度数据的缓冲区读取的影响,以及从该缓冲区读取将导致的缓存未命中。维莱特积分的缺点是,如果我们希望一个物体在模拟开始时已经在移动,我们就必须进行一些额外的计算——如果没有前一个位置(或者更糟的是,前一个位置变量默认为原点),那么我们得到的物体预测速度将完全不准确。

龙格-库塔(Runge-Kutta )方法

我们可以通过分割时间步长并进行多次积分来进一步进行积分,以尝试更好地了解速度和位置在时间步长过程中的变化。一种这样的预测方法是龙格-库塔方法,它对一帧时间步长的多个分割取平均值。可以采取不同的步数,导致略有不同的结果——采取两步被称为“RK2”,采取四步被称为“RK4”。

使用这种方法可以随着时间的推移在位置上获得更高的准确性,但代价是现在每个物体的积分需要四倍的计算量。在不需要完全准确的情况下,通常最好将计算时间用于推进模拟,并以这种方式计算新的碰撞和运动——对于游戏体验来说,更高的帧率通常比更“准确”的单个帧更好。


总结

在本教程中,我们看到了如何在我们的游戏世界中实现一个基本刚体的基础知识,它可以通过将加速度和速度积分到其世界位置中而移动。我们已经看到了帧时间对于我们物理模拟的正确积分和稳定性是多么重要,以及质量和阻尼如何随着时间与我们物体的运动相互作用。

  • 16
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱写代码的辰洋

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值