个性化阅读
专注于IT技术分析

电子游戏物理教程-第一部分:刚体动力学简介

本文概述

这是我们关于视频游戏物理的三部分系列的第一部分。对于本系列的其余部分, 请参见:

第二部分:实体物体的碰撞检测

第三部分:约束刚体模拟


物理模拟是计算机科学中的一个领域, 旨在使用计算机重现物理现象。通常, 这些模拟将数值方法应用于现有理论, 以获得与我们在现实世界中观察到的结果尽可能接近的结果。作为高级游戏开发人员, 这使我们能够在实际构建某个东西之前预测并仔细分析它们的行为, 这几乎总是更简单, 更便宜。

电子游戏物理就是模拟真实世界的物理。

物理模拟的应用范围非常广泛。最早的计算机已经用于执行物理模拟, 例如, 用于预测军事中弹丸的弹道运动。它也是土木和汽车工程学中必不可少的工具, 阐明了某些结构在地震或车祸等事件中的行为。而且不止于此。我们可以模拟诸如天体物理学, 相对论以及在自然奇观中可以观察到的许多其他疯狂的东西。

在电子游戏中模拟物理现象非常普遍, 因为大多数游戏的灵感都来自现实世界中的事物。许多游戏完全依靠物理模拟来娱乐。这意味着这些游戏需要稳定的模拟, 且不会崩溃或放慢速度, 通常这并非易事。

在任何游戏中, 只有某些物理效果才有意义。刚性的身体动力学–固体, 刚性物体的运动和相互作用–是迄今为止在游戏中模拟的最流行的一种效果。那是因为我们在现实生活中与之交互的大多数对象都是相当僵化的, 并且模拟刚体相对简单(尽管正如我们所看到的, 这并不意味着步履蹒跚)。其他一些游戏则需要模拟更复杂的实体, 例如可变形的物体, 流体, 磁性物体等。

在这个视频游戏物理教程系列中, 将探讨刚体模拟, 从本文中的简单刚体运动开始, 然后在接下来的部分中介绍通过碰撞和约束进行的刚体之间的交互。将介绍和解释在现代游戏物理引擎中最常用的方程式, 例如Box2D, Bullet Physics和Chipmunk Physics。

刚体动力学

在视频游戏物理学中, 我们希望对屏幕上的对象进行动画处理, 并赋予它们逼真的物理行为。这是通过基于物理的过程动画实现的, 该过程动画是通过将数值计算应用于物理理论定律而生成的动画。

动画是通过连续显示一系列图像而产生的, 对象从一个图像稍微移动到另一个图像。当快速连续显示图像时, 效果是对象的明显平滑连续运动。因此, 要在物理模拟中为对象设置动画, 我们需要根据物理定律每秒更新对象的物理状态(例如位置和方向), 并在每次更新后重绘屏幕。

物理引擎是执行物理模拟的软件组件。它接收将要模拟的实体的规范以及一些配置参数, 然后可以逐步进行模拟。每一步都将模拟向前移动几分之一秒, 然后结果可以在屏幕上显示。请注意, 物理引擎仅执行数值模拟。结果的结果可能取决于游戏的要求。并非总是希望将每个步骤的结果绘制到屏幕上。

可以使用牛顿力学对刚体的运动进行建模, 该力学基于艾萨克·牛顿著名的三运动定律:

  1. 惯性:如果没有力施加在物体上, 则其速度(速度和运动方向)不得改变。

  2. 力, 质量和加速度:作用在物体上的力等于物体的质量乘以物体的加速度(速度变化率)。这由公式F = ma给出。

  3. 动作和反应:”每一个动作都有相等和相反的反应。”换句话说, 只要一个物体在另一个物体上施加力, 第二个物体就在第一个物体上施加相同大小和相反方向的力。

根据这三个定律, 我们可以制作一个物理引擎, 以重现我们所熟悉的动态行为, 从而为玩家带来身临其境的体验。

下图从总体上概述了物理引擎的一般过程:

运行良好的物理引擎是视频游戏物理和编程的关键。

向量

为了了解物理模拟的工作原理, 对向量及其操作有基本的了解至关重要。如果你已经熟悉矢量数学, 请继续阅读。但是, 如果你不是, 或者想复习一下, 请花一点时间阅读本文结尾处的附录。

粒子模拟

理解刚体模拟的一个很好的垫脚石是从粒子开始。模拟粒子比模拟刚体更简单, 我们可以使用相同的原理来模拟后者, 但是会增加粒子的体积和形状。

粒子只是空间中的一个点, 具有位置矢量, 速度矢量和质量。根据牛顿第一定律, 仅当对其施加力时, 其速度才会改变。当其速度矢量的长度不为零时, 其位置将随时间变化。

为了模拟粒子系统, 我们需要首先创建一个具有初始状态的粒子数组。每个粒子必须具有固定的质量, 空间的初始位置和初始速度。然后, 我们必须开始仿真的主循环, 在该循环中, 对于每个粒子, 我们必须计算当前作用于其上的力, 从该力产生的加速度中更新其速度, 然后根据该速度更新其位置。我们刚刚计算过。

取决于模拟的种类, 力可能来自不同的来源。它可以是重力, 风或磁力, 也可以是这些的组合。它可以是整体力, 例如恒定重力, 也可以是粒子之间的力, 例如吸引或排斥力。

为了使仿真以现实的速度运行, 我们”仿真”的时间步应与自上一个仿真步骤以来经过的实际时间相同。但是, 可以按比例放大此时间步长以使仿真运行更快, 或按比例缩小该步长以使其以慢动作运行。

假设我们在时刻ti处有一个质点m, 位置p(ti)和速度v(ti)的粒子。此时, 力f(ti)施加在该粒子上。可以使用以下公式分别计算此粒子在将来的时间ti + 1处的位置和速度:p(ti + 1)和v(ti + 1):

半隐式欧拉

用技术术语来说, 我们在这里所做的是使用半隐式Euler方法在数值上积分一个粒子的运动微分方程, 这是大多数游戏物理学引擎使用的方法, 因为它的简单性和可接受的精度(对于小数值)时间跨度, dt。 Drs。的”基于物理的建模:原理与实践”课程的”微分方程基础知识”讲座笔记。安迪·维特金(Andy Witkin)和大卫·巴拉夫(David Baraff)是一篇不错的文章, 可以更深入地研究这种方法的数值方法。

以下是使用C语言进行粒子模拟的示例。

#define NUM_PARTICLES 1

// Two dimensional vector.
typedef struct {
    float x;
    float y;
} Vector2;

// Two dimensional particle.
typedef struct {
    Vector2 position;
    Vector2 velocity;
    float mass;
} Particle;

// Global array of particles.
Particle particles[NUM_PARTICLES];

// Prints all particles' position to the output. We could instead draw them on screen
// in a more interesting application.
void  PrintParticles() {
    for (int i = 0; i < NUM_PARTICLES; ++i) {
        Particle *particle = &particles[i];
        printf("particle[%i] (%.2f, %.2f)\n", i, particle->position.x, particle->position.y);
    }
}

// Initializes all particles with random positions, zero velocities and 1kg mass.
void InitializeParticles() {
    for (int i = 0; i < NUM_PARTICLES; ++i) {
        particles[i].position = (Vector2){arc4random_uniform(50), arc4random_uniform(50)};
        particles[i].velocity = (Vector2){0, 0};
        particles[i].mass = 1;
    }
}

// Just applies Earth's gravity force (mass times gravity acceleration 9.81 m/s^2) to each particle.
Vector2 ComputeForce(Particle *particle) {
    return (Vector2){0, particle->mass * -9.81};
}

void RunSimulation() {
    float totalSimulationTime = 10; // The simulation will run for 10 seconds.
    float currentTime = 0; // This accumulates the time that has passed.
    float dt = 1; // Each step will take one second.
    
    InitializeParticles();
    PrintParticles();
    
    while (currentTime < totalSimulationTime) {
        // We're sleeping here to keep things simple. In real applications you'd use some
        // timing API to get the current time in milliseconds and compute dt in the beginning 
        // of every iteration like this:
        // currentTime = GetTime()
        // dt = currentTime - previousTime
        // previousTime = currentTime
        sleep(dt);

        for (int i = 0; i < NUM_PARTICLES; ++i) {
            Particle *particle = &particles[i];
            Vector2 force = ComputeForce(particle);
            Vector2 acceleration = (Vector2){force.x / particle->mass, force.y / particle->mass};
            particle->velocity.x += acceleration.x * dt;
            particle->velocity.y += acceleration.y * dt;
            particle->position.x += particle->velocity.x * dt;
            particle->position.y += particle->velocity.y * dt;
        }
        
        PrintParticles();
        currentTime += dt;
    }
}

如果你调用RunSimulation函数(对于单个粒子), 它将打印如下内容:

particle[0] (-8.00, 57.00)
particle[0] (-8.00, 47.19)
particle[0] (-8.00, 27.57)
particle[0] (-8.00, -1.86)
particle[0] (-8.00, -41.10)
particle[0] (-8.00, -90.15)
particle[0] (-8.00, -149.01)
particle[0] (-8.00, -217.68)
particle[0] (-8.00, -296.16)
particle[0] (-8.00, -384.45)
particle[0] (-8.00, -482.55)

如你所见, 粒子从(-8, 57)位置开始, 然后其y坐标开始下降得越来越快, 因为它在重力的作用下向下加速。

以下动画直观地表示了单个粒子的三个步骤的序列:

粒子动画

最初, 在t = 0时, 粒子处于p0。步进之后, 它沿其速度矢量v0指向的方向移动。在下一步中, 将力f1施加到其上, 并且速度矢量开始改变, 就好像它是在力矢量的方向上被拉动一样。在接下来的两个步骤中, 力矢量会改变方向, 但会继续向上拉动粒子。

刚体模拟

刚体是不会变形的实体。这样的固体在现实世界中不存在-即使最坚硬的材料在施加一定的力时也至少会变形很小-但是刚体对于游戏开发者而言是一种有用的物理模型, 可简化对动力学的研究。可以忽略变形的实体。

刚体就像粒子的延伸, 因为它也具有质量, 位置和速度。此外, 它具有体积和形状, 因此可以旋转。这增加了听起来的复杂性, 尤其是在三个方面。

刚体自然绕其质心旋转, 并且刚体的位置被视为其质心的位置。我们定义刚体的初始状态, 其质心在原点, 旋转角度为零。它在任何时刻t的位置和旋转都将是初始状态的偏移。

了解视频游戏的物理原理对于创建漏洞少,粉丝多的游戏至关重要。

重心是物体质量分布的中点。如果你想象一个质量为M的刚体是由N个微小的粒子组成的, 每个微粒的质量为mi, 并且其位置ri在体内, 则质心可以计算为:

重心

该公式表明, 质心是按其质量加权的粒子位置的平均值。如果整个身体的密度均匀, 则质心与身体形状的几何中心相同, 也称为质心。游戏物理引擎通常仅支持均匀密度, 因此可以将几何中心用作质心。

刚体不是由有限数量的离散粒子组成, 而是连续的。因此, 我们应该使用积分而不是有限的总和来计算它, 如下所示:

质心中心

其中r是每个点的位置向量, 而r(rho)是一个函数, 用于给出体内每个点的密度。本质上, 此积分与有限和具有相同的作用, 但是在连续体积V中具有相同的作用。

由于刚体可以旋转, 因此我们必须引入其角度特性, 该特性类似于粒子的线性特性。在二维中, 刚体只能绕着指向屏幕外的轴旋转, 因此我们只需要一个标量来表示其方向。我们通常在这里使用弧度(一个完整的圆从0到2π)作为单位, 而不是角度(一个完整的圆从0到360的角度)作为单位, 因为这简化了计算。

为了旋转, 刚体需要一定的角速度, 该角速度是具有单位弧度每秒的标量, 通常由希腊字母ω(Ω)表示。但是, 为了获得角速度, 人体需要接收一些旋转力, 我们称其为扭矩, 以希腊字母τ(tau)表示。因此, 适用于旋转的牛顿第二定律是:

旋转第二定律

其中α(α)是角加速度, I是惯性矩。

对于旋转, 惯性矩类似于线性运动的质量。它定义了更改刚体角速度的难易程度。在二维中, 它是一个标量, 定义为:

转动惯量

其中V表示应对整个人体体积的所有点执行该积分, r是相对于旋转轴的每个点的位置矢量, r2实际上是r与自身的点积, 而????是给出体内每个点的密度。

例如, 质量为m, 宽度为w, 高度为h且质心为2的二维盒子的惯性矩为:

箱惯性矩

在这里, 你可以找到公式列表, 以计算围绕不同轴的一堆形状的惯性矩。

在刚体上的一点上施加力时, 可能会产生扭矩。在二维上, 扭矩是一个标量, 由作用在距质量中心的偏移矢量为r的点上的力f生成的扭矩τ为:

扭矩2D

其中θ(θ)是f和r之间的最小角度。

该图可以帮助你了解本视频游戏物理教程的内容。

先前的公式恰好是r和f之间的叉积长度的公式。因此, 我们可以在三个维度上做到这一点:

扭力
游戏开发人员对物理的敏锐理解使最终产品的良好和不良用户体验有所不同。

二维模拟可以看作是三维模拟, 其中所有刚体都薄而平坦, 并且都发生在xy平面上, 这意味着z轴上没有移动。这意味着f和r始终在xy平面中, 因此τ将始终具有零个x和y分量, 因为叉积将始终垂直于xy平面。这又意味着它将始终平行于z轴。因此, 仅叉积的z分量很重要。这导致二维转矩的计算可以简化为:

扭矩2D
电子游戏的物理原理可能很复杂。使用图表和标准物理方程式可以阐明这一点。

难以置信的是, 如何在模拟中仅增加一个维度就使事情变得相当复杂。在三个维度上, 必须用四元数表示刚体的方向, 四元数是一种四元素矢量。惯性矩由称为惯性张量的3×3矩阵表示, 该矩阵不是恒定的, 因为它取决于刚体的方向, 因此随着时间的推移会随身体旋转而变化。要了解有关3D刚体仿真的所有详细信息, 你可以查看出色的”刚体仿真I-无约束刚体动力学”, 这也是Witkin和Baraff的”基于物理的建模:原理与实践”课程的一部分。

模拟算法与粒子模拟非常相似。我们只需要添加刚体的形状和旋转特性:

#define NUM_RIGID_BODIES 1

// 2D box shape. Physics engines usually have a couple different classes of shapes
// such as circles, spheres (3D), cylinders, capsules, polygons, polyhedrons (3D)...
typedef struct {
    float width;
    float height;
    float mass;
    float momentOfInertia;
} BoxShape;

// Calculates the inertia of a box shape and stores it in the momentOfInertia variable.
void CalculateBoxInertia(BoxShape *boxShape) {
    float m = boxShape->mass;
    float w = boxShape->width;
    float h = boxShape->height;
    boxShape->momentOfInertia = m * (w * w + h * h) / 12;
}

// Two dimensional rigid body
typedef struct {
    Vector2 position;
    Vector2 linearVelocity;
    float angle;
    float angularVelocity;
    Vector2 force;
    float torque;
    BoxShape shape;
} RigidBody;

// Global array of rigid bodies.
RigidBody rigidBodies[NUM_RIGID_BODIES];

// Prints the position and angle of each body on the output.
// We could instead draw them on screen.
void PrintRigidBodies() {
    for (int i = 0; i < NUM_RIGID_BODIES; ++i) {
        RigidBody *rigidBody = &rigidBodies[i];
        printf("body[%i] p = (%.2f, %.2f), a = %.2f\n", i, rigidBody->position.x, rigidBody->position.y, rigidBody->angle);
    }
}

// Initializes rigid bodies with random positions and angles and zero linear and angular velocities.
// They're all initialized with a box shape of random dimensions.
void InitializeRigidBodies() {
    for (int i = 0; i < NUM_RIGID_BODIES; ++i) {
        RigidBody *rigidBody = &rigidBodies[i];
        rigidBody->position = (Vector2){arc4random_uniform(50), arc4random_uniform(50)};
        rigidBody->angle = arc4random_uniform(360)/360.f * M_PI * 2;
        rigidBody->linearVelocity = (Vector2){0, 0};
        rigidBody->angularVelocity = 0;
        
        BoxShape shape;
        shape.mass = 10;
        shape.width = 1 + arc4random_uniform(2);
        shape.height = 1 + arc4random_uniform(2);
        CalculateBoxInertia(&shape);
        rigidBody->shape = shape;
    }
}

// Applies a force at a point in the body, inducing some torque.
void ComputeForceAndTorque(RigidBody *rigidBody) {
    Vector2 f = (Vector2){0, 100};
    rigidBody->force = f;
    // r is the 'arm vector' that goes from the center of mass to the point of force application
    Vector2 r = (Vector2){rigidBody->shape.width / 2, rigidBody->shape.height / 2};
    rigidBody->torque = r.x * f.y - r.y * f.x;
}

void RunRigidBodySimulation() {
    float totalSimulationTime = 10; // The simulation will run for 10 seconds.
    float currentTime = 0; // This accumulates the time that has passed.
    float dt = 1; // Each step will take one second.
    
    InitializeRigidBodies();
    PrintRigidBodies();
    
    while (currentTime < totalSimulationTime) {
        sleep(dt);
        
        for (int i = 0; i < NUM_RIGID_BODIES; ++i) {
            RigidBody *rigidBody = &rigidBodies[i];
            ComputeForceAndTorque(rigidBody);
            Vector2 linearAcceleration = (Vector2){rigidBody->force.x / rigidBody->shape.mass, rigidBody->force.y / rigidBody->shape.mass};
            rigidBody->linearVelocity.x += linearAcceleration.x * dt;
            rigidBody->linearVelocity.y += linearAcceleration.y * dt;
            rigidBody->position.x += rigidBody->linearVelocity.x * dt;
            rigidBody->position.y += rigidBody->linearVelocity.y * dt;
            float angularAcceleration = rigidBody->torque / rigidBody->shape.momentOfInertia;
            rigidBody->angularVelocity += angularAcceleration * dt;
            rigidBody->angle += rigidBody->angularVelocity * dt;
        }
        
        PrintRigidBodies();
        currentTime += dt;
    }
}

为单个刚体调用RunRigidBodySimulation会输出其位置和角度, 如下所示:

body[0] p = (36.00, 12.00), a = 0.28
body[0] p = (36.00, 22.00), a = 15.28
body[0] p = (36.00, 42.00), a = 45.28
body[0] p = (36.00, 72.00), a = 90.28
body[0] p = (36.00, 112.00), a = 150.28
body[0] p = (36.00, 162.00), a = 225.28
body[0] p = (36.00, 222.00), a = 315.28
body[0] p = (36.00, 292.00), a = 420.28
body[0] p = (36.00, 372.00), a = 540.28
body[0] p = (36.00, 462.00), a = 675.28
body[0] p = (36.00, 562.00), a = 825.28

由于施加了100牛顿的向上力, 因此只有y坐标发生变化。该力未直接施加在质心上, 因此会产生扭矩, 从而导致旋转角度增加(逆时针旋转)。

图形结果类似于粒子动画, 但是现在我们可以移动和旋转形状。

刚体动画

这是不受限制的模拟, 因为这些物体可以自由移动并且彼此之间不相互作用–它们不会碰撞。没有碰撞的模拟非常无聊, 并且不是很有用。在本系列的下一部分中, 我们将讨论碰撞检测, 以及一旦检测到碰撞就如何解决。

附录:向量

向量是具有长度(或更正式地说是幅度)和方向的实体。可以在笛卡尔坐标系中直观地表示它, 在此我们将其分解为二维空间中的两个分量x和y或三维空间中的三个分量x, y和z。

尽管游戏编程教程为开发人员提供了一定程度的清晰度,但它们通常并不包含物理图表。
了解视频游戏的物理原理会使开发人员的工作变得更加复杂,但最终产品会更加成功。

我们用一个粗体的小写字母表示一个向量, 它的组成部分用与下标组成的规则的相同字符表示。例如:

Vec2D

代表二维向量。每个分量都是在相应坐标轴上距原点的距离。

物理引擎通常具有自己的轻量级, 高度优化的数学库。例如, Bullet Physics引擎具有一个称为Linear Math的解耦数学库, 该库可以单独使用, 而无需Bullet的任何物理模拟功能。它具有代表诸如矢量和矩阵之类的实体的类, 并且如果可用, 它会利用SIMD指令。

对向量进行运算的一些基本代数运算在下面进行了概述。

长度

长度或大小运算符由||表示||。使用直角三角形著名的毕达哥拉斯定理, 可以从其分量计算出向量的长度。例如, 在两个维度中:

Vec2Dength

否定

取反向量时, 长度保持不变, 但方向更改为完全相反。例如, 给定:

Vec2D

否定是:

否定
用于游戏开发的物理学是模拟在现实世界中发生运动时刚体会发生什么情况的更简单方法。

加减

向量可以彼此加或减。将两个向量相减就等于将一个向量与另一向量相加。在这些操作中, 我们只需添加或减去每个组件:

加减

可以将所得向量可视化为指向两个原始向量端对端连接时指向的同一点:

此视频游戏物理教程提供了说明和图表,以提高游戏开发人员的技能。

标量乘法

当矢量乘以标量时(就本教程而言, 标量就是任何实数), 矢量的长度将改变该数量。如果标量为负, 则向量也将指向相反的方向。

标量Vec2D
标量乘法

点积

点积运算符采用两个向量并输出标量数。它定义为:

点积

其中ø是两个向量之间的角度。在计算上, 使用向量的分量来进行计算要简单得多, 即:

点产品组件

点积的值等于向量a在向量b上的投影长度乘以b的长度。也可以翻转此动作, 取b在a上的投影长度, 再乘以a的长度, 即可得到相同的结果。这意味着点积是可交换的-a与b的点积与b与a的点积相同。点积的一个有用特性是, 如果向量正交(它们之间的角度为90度), 则其值为零, 因为cos 90 = 0。

叉积

在三个维度上, 我们还可以使用叉积运算符将两个向量相乘以输出另一个向量。它定义为:

交叉产品

叉积的长度为:

交叉长度

ø是a和b之间的最小角度。结果是向量c与a和b正交(垂直)。如果a和b平行, 即它们之间的角度为零或180度, 则c将成为零矢量, 因为sin 0 = sin 180 = 0。

与点积不同, 叉积不是可交换的。叉积中元素的顺序很重要, 因为:

交叉通勤

结果的方向可以通过右手定则确定。张开右手, 将食指指向a的相同方向, 并以以下方式定向手:握紧拳头时, 手指沿这些矢量之间的最小角度朝b笔直移动。现在你的拇指指向a×b的方向。

右手规则适用于视频游戏物理以及传统物理。
赞(0)
未经允许不得转载:srcmini » 电子游戏物理教程-第一部分:刚体动力学简介

评论 抢沙发

评论前必须登录!