制作 2D 物理引擎:数学部分






4.90/5 (13投票s)
开始制作自己的 2D 物理引擎所需的所有数学知识介绍
制作 2D 物理引擎:系列
这是制作 2D 物理引擎系列的第一篇文章。
- 制作 2D 物理引擎:数学部分
- 制作 2D 物理引擎:空间和物体
- 制作 2D 物理引擎:形状、世界和积分
- 制作 2D 物理引擎:质量、惯性和力
引言
为什么游戏需要物理引擎?
游戏中的物理引擎有助于我们模拟一个半真实的、玩家容易产生共鸣的世界。从第一款《大金刚》游戏到最新的《最后生还者》,物理引擎都帮助实现了世界的物质化。物理引擎可以真实也可以不真实,具体取决于您开发的游戏类型。本系列文章希望能为您提供关于物理引擎如何工作的算法以及足够的知识,以便您能从头开始实现自己的 Box2D 版本!
要模拟游戏中的物理效果,我需要了解什么?
您总是可以选择编写自己的物理引擎(这是本系列文章的主要重点),也可以使用市面上或免费的引擎,例如 NVIDIA 的 PhysX 引擎和 Havok Physics,供您在项目中集成。所有游戏引擎都附带了物理引擎,但您仍然需要实现游戏特定的物理实体/模拟,例如车辆引擎、船只、浮力、空气阻力等等。所有这些都需要在 2D 和 3D 中掌握向量和矩阵的知识。本文将介绍实现 2D 游戏物理效果所需的一些更重要的向量和矩阵概念。
向量
让我们从“n 维空间中的点和方向”这一最基本概念开始:向量。
什么是向量?
向量是用于“携带”点 A 到点 B 的几何对象。它具有大小和方向。它通常用于表示高中物理中提到的“矢量”量,如力、速度。
向量表示法
“n 维”空间中的向量有“n”个分量。通常使用 2D、3D 和 4D 向量。向量可以表示为列矩阵,即 n 维向量表示为 n*1 矩阵。它也可以表示为有序集合,例如:(\(a_1, a_2, \ldots a_{n-1}, a_n)\)
任何 2D 或 3D 向量的分量通常用字母 x、y 和 z 来表示,它们也代表该向量的相应笛卡尔坐标。
下面的内容主要针对 2D 向量,但可以轻松扩展到 3D 向量。
向量运算
向量长度
向量的长度(或模)等于它与原点(零向量)之间的勾股距离。它可以表示如下:\(\mathbf{||A||} = \sqrt{a_1^2 + a_2^2 + \ldots + a_n^2}\)
在代码中,它可以定义如下
float length(Vec2 vec)
return sqrt(vec.x * vec.x + vec.y * vec.y)
在许多需要比较距离的情况下,会避免耗时的 sqrt 运算,而是使用向量长度的平方。
float sqrLength(Vec2 vec)
return vec.x * vec.x + vec.y * vec.y
向量归一化(单位向量)
单位向量是指长度为 1 的向量。它通常用于表示法线和切线等方向。要获得特定向量的单位向量(方向),则用该向量除以其长度。此过程称为归一化。
Vec2 normalized(Vec2 vec)
return vec * (1 / length(vec))
向量乘法
向量可以与标量相乘,也可以与同维度的另一个向量相乘。
向量与标量相乘
向量可以被标量缩放,即其每个分量都将乘以该标量。
向量与另一个向量相乘
两个向量可以使用点积(标量积)或叉积(向量积)进行乘法运算。
点积
点积是两个向量的分量乘积之和。它返回一个标量。
点积是最常用的向量运算之一,因为它与两个向量之间夹角的余弦密切相关。
cos theta = A dot B / (length(A) * length(B))
或
cos theta = normalized(a) dot normalized(b)
有一点需要牢记:如果两个向量相互垂直,它们的点积将等于零(因为 cos theta = 0)。
叉乘
叉积(向量积)是 3D 中一个常用的运算。叉积,记为 a × b,是一个垂直于 a 和 b 的向量,定义为
其中 n 是垂直于 a 和 b 的向量。
叉积仅对 3D 向量有明确定义。但由于 2D 向量可以被视为位于 XY 平面上的 3D 向量,因此任何两个 2D 向量的叉积都可以定义为它们的 3D 平面表示的叉积,结果是一个沿 Z 轴的向量,可以表示为标量(表示 Z 轴向量的模)。
类似地,一个 2D 向量与一个标量叉乘会得到另一个垂直于原始 2D 向量的 2D 向量。
2D 向量的叉积如下所示
// Two crossed vectors return a scalar pointing in the z direction float cross(Vector2 a, Vec2 b) return a.x * b.y - a.y * b.x; //A vector crossed with a scalar (z-axis) will return a vector on the 2D Cartesian plane Vector2 cross(Vector2 a, float s) return Vec2(s * a.y, -s * a.x); Vector2 cross(float s, Vector2 a) return Vec2(-s * a.y, s * a.x);
Vector2 结构伪代码
用您选择的编程语言,Vector2
结构应该如下所示。您可以根据自己的喜好更改名称。
struct Vector2 {
float x, y;
float length() {
return sqrt(x * x + y * y);
}
float sqrLength() {
return x * x + y * y;
}
Vector2 operator *(Vector2 v, float s) {
return Vector2(v.x * s, v.y * s);
}
void normalize() {
float inv_len = 1 / length();
x *= inv_len; y *= inv_len;
}
float dot(Vector2 a, Vector2 b) {
return a.x * b.x + a.y * b.y;
}
// Two crossed vectors return a scalar pointing in the z direction
float cross(Vector2 a, Vec2 b) {
return a.x * b.y - a.y * b.x;
}
//A vector crossed with a scalar (z-axis) will return a vector on the 2D Cartesian plane
Vector2 cross(Vector2 a, float s) {
return Vec2(s * a.y, -s * a.x);
}
Vector2 cross(float s, Vector2 a) {
return Vec2(-s * a.y, s * a.x);
}
}
请注意,Vector2
结构的所有实例,就像大多数语言中的所有基本类型一样,应该按值复制,而不是按引用复制,除非明确要求。向量的引用复制会导致不必要的麻烦和隐藏的 bug。
矩阵
矩阵是矩形数组——由数字、符号或表达式组成,按行和列排列——并以特定方式处理。矩阵通常用于计算机图形学和物理学,用于将一个基点变换到另一个基点,包括旋转、平移和缩放。
本文只介绍用于旋转 2D 向量的 2x2 矩阵。
为什么要使用矩阵?
如果您还记得高中数学,一个 \(l \times m\) 矩阵乘以一个 \(m \times n\) 矩阵会得到一个 \(l \times n\) 矩阵。在这种情况下,一个 2x2 矩阵乘以一个表示为 2x1 矩阵的向量会得到另一个 2x1 矩阵(2D 向量)。这使得在数学上更简单,在计算上更有效地变换向量。一个重要的变换,即旋转,将在接下来的几个小节中介绍。
2D 旋转
每个物体都有一个方向。从旋转的角度来看,方向与位置(即物体在某一时刻的旋转角度)同义,角速度(方向的变化率)与速度同义,而力矩与力同义。由于 2D 中的物体只能绕假想的 z 轴旋转,因此 2D 物体的方向是一个表示绕 z 轴旋转的弧度的标量。由于点到原点的距离必须保持不变(根据角运动学中旋转的定义),旋转的点将始终位于以原点为中心、半径等于其到原点距离的圆周上。
按一定角度旋转向量
在 2D 笛卡尔平面中,对于某个向量 P(x, y),如果 P 需要旋转的角度为“theta”,那么
这直接来自三角函数复合角公式,在将向量转换为极坐标形式之后。
使用矩阵旋转向量
再次查看上面的方程。我已经将其以矩阵形式呈现,以便在创建矩阵方程后更容易获得旋转矩阵。在继续之前,请尝试自己找出旋转矩阵。
2x2 矩阵与 2x1 矩阵的矩阵乘法公式将大致如下所示
现在将此结果与前一个方程的结果进行比较。
从上面的关系,我们可以得出
Matrix2 结构伪代码
一个 2*2 的矩阵结构如下所示
struct Matrix2
{
float m00, m01
float m10, m11;
//Create from angle in radians
void set(real radians) {
real c = cos(radians);
real s = sin(radians);
m00 = c; m01 = -s;
m10 = s; m11 = c;
}
Matrix2 transpose() {
return Matrix2(m00, m10,
m01, m11);
}
Vector2 operator*(Vector2 rhs) {
return Vec2(m00 * rhs.x + m01 * rhs.y, m10 * rhs.x + m11 * rhs.y);
}
Matrix2 operator*(Matrix2 rhs ) {
return Mat2(
m00 * rhs.m00 + m01 * rhs.m10, m00 * rhs.m01 + m01 * rhs.m11,
m10 * rhs.m00 + m11 * rhs.m10, m10 * rhs.m01 + m11 * rhs.m11);
}
}
与 Vector2
结构一样,Matrix2
结构实例也必须按值复制。
What Next?
下一篇文章将开始介绍如何制作一个非常基础的物理引擎,其中包含代码。它将包括形状、刚体以及速度和力的积分等概念。我希望在未来的文章中涵盖碰撞检测、碰撞解析、凹形形状、广谱阶段、射线投射、扩展到 3D 以及其他概念。我会尽量将其分成尽可能多的文章,因为它内容很多!
如果您有任何评论/建议,请告诉我!
历史
2015 年 9 月 13 日:首次发布
2017 年 11 月 5 日:语言和内容改进