4.2 点和向量

本文是Game Engine Architecture的一部分
免责声明

大多数的现代3D游戏是由虚拟世界中的三维物体组成的。游戏引擎需要跟踪每一个对象的位置、朝向和缩放,让它们在游戏世界中运作,然后将它们的坐标转换到屏幕空间,使它们能渲染在屏幕上。游戏中的物体几乎总是由三角形组成的,三角形的顶点则以点来表示。因此,在我们学习如何表示整个三维物体前,让我们先看一看点,以及它的近亲向量。

4.2.1 点和笛卡尔坐标系

从技术上讲,一个代表着一个n维空间中的位置。(在游戏里,n通常等于2或者3。)笛卡尔坐标系是目前为止最常为游戏程序员所使用的坐标系统。它使用二到三条相互垂直的轴来指定一个二维或三维空间中的位置。因此,一个点P是用一个二元组(Px, Py)或者一个三元组(Px, Py, Pz)来表示的。

当然了,笛卡尔坐标系并不是我们唯一的选择。一些其它常见的坐标系统包括:

  • 柱坐标系。该系统采用了一个垂直的“高度”轴h,一个由垂直方向发射出来的径向轴r,以及一个夹角theta(θ)。在柱坐标系中,一个点P由一个三元组(Ph, Pr, Pθ)表示。见图4.2。
  • 球坐标系。该系统采用了两个夹角phi(φ)和theta(θ),以及一个径向测量量r。一个点因此可以由一个三元组(Pr, Pφ, Pθ)表示。见图4.3.

笛卡尔坐标系是目前为止在游戏编程中最广泛使用的坐标系统。然而要记住,我们总应该选择最能反映手头上问题的坐标系。例如,在Midway Home Entertainment开发的游戏Crank the Weasel中,主角Crank在一座充满艺术感的城市中一边游荡一遍收集战利品。我希望让这些战利品绕着Crank的身体旋转飞舞,并越来越靠近直至消失。我将战利品的坐标表示在以Crank当前位置为中心的柱坐标系中。为了实现这样一个螺旋动画,我只需要简单地为战利品的θ赋予一个恒角速度,为其r轴赋予一个非常小的向内的恒线速度,再为其h轴赋予一个非常小的向上的恒线速度好让物品能飞进Crank的口袋。这个简单的动画看起来效果非常棒,并且使用柱坐标系来为其建模远比使用笛卡尔坐标系建模简单。

4.2.2 左手笛卡尔坐标系 vs 右手笛卡尔坐标系

在三维笛卡尔坐标系中,我们可使用两种方式来安排这三条相互垂直的轴:右手方式(RH)和左手方式(LH)。在右手笛卡尔坐标系中,当你的右手绕z轴弯曲,你的拇指将指向z轴正方向,你的其余手指从x轴绕向y轴。在左手笛卡尔坐标系中,情况完全相同,只是你要把右手换成左手。左手和右手坐标系唯一的区别在于其中一条轴的朝向。例如,如果y轴正方向朝上且x轴正方向朝右,那么在右手坐标系中z轴正方向朝向我们,而在左手坐标系中z轴正方向则背向我们。左手及右手笛卡尔坐标系如图4.4所示。

将左手坐标系转换成右手坐标系是非常容易的,反之亦然。我们只需要反转任意一条坐标轴的朝向,并使另外两条坐标轴的朝向保持不变。要记住的是,在左手和右手坐标系中数学规则并没有改变。只有我们对数字的解释 —— 在我们脑海中对数字如何映射到三维空间中的映像 —— 改变了。左手和右手公约仅适用于视图层,而对于底层的数学没什么关系。(事实上,当处理物理模拟中的叉乘时这是很有关系的。但对于绝大多数游戏编程任务来说,我们可以很安全地忽略这些微妙之处。有关更多信息,请参见http://en.wikipedia.org/wiki/Pseudovector。)

数值表示以及视图表示之间的映射完全取决于作为程序员或者是数学家的我们。我们可以选择让y轴指向上,z轴指向前,x轴指向左(RH)或指向右(LH)。或者我们也可以选择让z轴指向上。或者我们还可以让x轴指向上。重要的是,我们在做出了决定以后,应该始终坚持它。

话虽然这么说,但有时候一些约定确实在某些应用中工作得更好。例如,3D图形程序员通常使用一个左手坐标系,其中y轴向上,x轴向右且z轴指向远离观察者的方向(也就是说,沿着摄像机的朝向)。当3D图形使用该坐标系统渲染到2D屏幕上时,增加的y轴坐标对应着增加的景深(也就是距离摄像机的距离)。正如我们将要在接下来的几节中见到的,这就是为什么需要使用z缓冲方案来实现深度遮挡的原因。

4.2.3 向量

向量是一个在空间中既有大小又有方向的量。向量可以看做是一个从尾端点伸展向头端点的有向线段。相比之下,常用于表示大小的标量(也就是普通的实数值)是没有方向的。

通常标量以斜体表示(如v),而向量以黑体表示(如v)。

正如点一样,向量可以由一个由标量组成的三元组(x, y, z)表示。点和向量之间的区别实际上是非常微妙的。技术上来说,一个向量通常只是相对于一个已知点的偏移量。向量可以被移动到三维空间中的任意地方 —— 只要它的大小和方向不变,它还是同一个向量。

向量通常可以用来表示点,只要我们把它的尾部固定在坐标系的原点。这样的向量有时被称之为位置向量半径向量。根据我们目的的不同,我们可以将任意标量三元组解释为点或向量,只要我们记住位置向量的约束是把尾部定位在坐标系的原点就可以了。这意味着点和向量在数学中会以非常微妙的形式区分对待。有人把点称作绝对坐标,而把向量称作相对坐标。

在严格的线性代数场景下,大部分程序员使用名词“向量”来同时表示点(位置向量)和向量。大部分的3D数学库也是这样使用名词“向量”的。在本书中,当差别很显著时,我们会使用名词“方向向量”或只是“方向”。注意,你总是应该在脑海中保持对于点和方向的差异的清晰认识(即使你的数学库不作区分)。正如我们将要在章节4.3.6.1看到的,当需要把它们转换进齐次坐标系以执行4×4矩阵操作时,点和方向需要被区分对待。因此,将两种向量混合有可能会导致你的代码出现bug。

4.2.3.1 笛卡尔基本向量

为笛卡尔坐标系的三条轴分别定义三个正交的单位向量(也就是说,三个向量之间相互垂直,且长度均为一)总是很有用的。沿x轴的单位向量常被称作i,沿y轴单位向量常被称作j,沿z轴单位向量常被称作k。向量ijk有时候被称为笛卡尔基本向量。任意点或向量都可以通过标量(实数)乘以单位向量的和来表示。例如,
(5, 3, –2) = 5i + 3j – 2 k

4.2.4 向量操作

大多数你可以在标量上执行的数学操作也可以应用到向量上。也有一些新的操作只适用于向量。

4.2.4.1 向量乘以标量

将一个向量a和一个标量s相乘是通过将向量a中的每一部分分别与标量s相乘来完成的:

向量乘以标量将使向量的大小缩放,而保留其方向不变,如图4.5所示。将向量乘以-1将会反转向量的方向(头变尾尾变头)。

沿每个轴的缩放因子可以各不相同。这种缩放我们称之为非均匀缩放,它可以被表示为一个向量中的每一部分逐个乘以另一个缩放向量。这种运算我们使用⊗运算符表示。技术上说,我们把两个向量间的这种特殊乘法称之为Hadamard乘法。它很少在游戏产业中被使用 —— 实际上,非均匀缩放是它唯一在游戏中使用的地方:

我们将会在章节4.3.7.3中看到,缩放向量实际上只是3×3对角缩放矩阵S的一个压缩表示形式。因此方程(4.1)的另一种形式如下:

4.2.4.2 加法和减法

两个向量ab之间的加法被定义为ab中的每个部分各自相加。这可以被看做是将向量a和向量b头尾相接 —— 和将是从向量a的尾部指向向量b的头部的向量:

向量相减a – b只不过是a和-b(也就是将b乘以-1,使其反转)的加法操作。这使得结果向量的每一部分等于向量ab的每一部分分别做加法所得的差:

向量加法和减法如图4.6所示。

将点和方向相加或相减

你可以自由地对两个方向向量做加减法。然而技术上说,点却不可以相互加在一起 —— 你只可以把一个方向加到一个点上,该操作的最终结果是另一个点。同样地,你可以将两个点相减,从而得到一个方向向量。这些操作可概括如下:

  • 方向 + 方向 = 方向
  • 方向 – 方向 = 方向
  • 点 + 方向 = 点
  • 点 – 点 = 方向
  • 点 + 点 = 毫无意义(别这样做!)

4.2.4.3 大小

向量的大小是一个标量,它代表着向量在二维或三维空间中的长度。向量的大小以在向量的黑体符号旁边放置一条垂直的直线来表示。我们可以使用毕达哥拉斯定理来计算向量的大小,如图4.7所示:


4.2.4.4 在实际中运用向量操作

信不信由你,通过到迄今为止我们刚学过的向量运算知识,我们已经可以解决游戏中各种各样的问题了。当试图解决一个问题时,我们可以对已知的数据使用如加、减、乘和求长度等操作来生成新的数据。例如,假设我们拥有一个AI角色的当前位置向量P1,以及它当前的速度向量v,我们就可以通过将速度向量v与时间间隔Δt相乘,再将结果加上P1,以求得AI在下一帧的位置P2。如图4.8所示,结果方程式是P2 = P1 + (Δt)v。(这就是所谓的显式欧拉积分 —— 实际上它只在速度为常量时有效,但你懂的。)
再举个例子,假设我们有两个球体,并且我们想要知道它们是否相交。给出两个球的圆心坐标C1和C2,我们可以简单地让两点相减以求出两球之间的方向向量,d = C1 – C2。向量的大小d = |d|决定了两球中心相距多远。如果这个距离小于两球的半径之和,则它们是相交的,否则就是不想交。这如图4.9所示。

对于大多数电脑来说平方根运算的代价是昂贵的,因此游戏程序员总应该使用平方大小的方法来替代:

对于比较两个向量长度(“向量a是否长于向量b?”),以及比较其他一些(平方过的)标量时,使用平方大小是有效的。因此在我们的球体碰撞检测中,我们应该计算d^2 = |d|^2,并将之与两球半径的平方和(r1 + r2)^2进行比较,以获得最佳速度。当编写高性能软件时,永远别在不需要使用平方根的地方使用平方根!

4.2.4.5 规格化及单位向量

单位向量是指大小(长度)为一的向量。我们将会在接下来看到,单位向量在3D数学和游戏编程中是非常有用的。给定具有任意长度v = |v|的向量v,我们可以将其转换为单位向量u,且u具有和v相同的方向,但其具有单位长度。要做到这一点,我们可以简单地把v乘以它大小的倒数。我们把这种操作称之为规格化


4.2.4.6 法向量

如果一个向量垂直于某平面,我们则认为向量是这个平面的法向量。法向量在游戏和计算机图形学中是非常有用的。例如,一个平面可以由一个点和一个法向量来定义。在三维图形学的光照计算中大量使用了法向量,来定义光线和表面碰撞的方向。

法向量通常具有单位长度,但他们并不需要如此。小心不要混淆了术语“规格化(normalization)”和术语“法向量(normal vector)”。规格化的向量是任何具有单位长度的向量。法向量则是任何垂直于平面的向量,但它不必具有单位长度。

4.2.4.7 点乘和投影

向量之间可以相互作乘法,但与标量乘法不一样,向量乘法有许多种不同类型。在游戏编程中,我们最常使用以下两种乘法:

  • 点乘(也被称作标量乘或内积),和
  • 叉乘(也被称作向量乘或外积)。

两个向量之间的点乘将产生一个标量,它被定义为向量各部分乘积的和:

点乘还可以被写作是两向量的大小乘积再乘以两向量夹角的余弦值:
点乘符合交换率(也就是说做乘法的两个向量的顺序可以倒转)和加法分配率

点乘和标量乘法的结合如下所示:

向量投影

如果u是一个单位向量(|u| = 1),那么点乘(a · u)代表了向量a投影在由向量u的方向所定义的无限长直线上的投影长度,如图4.10所示。这个概念同等地适用于二维、三维以及更高的维度,以解决各种各样的三维问题。

作为点乘的大小

一个向量的平方大小可以由向量与自身的点乘得出。因此向量的大小可以简单地做平方根运算后得出:

这之所以可行是因为角度0的余弦值为1,因此剩下来的就是|a||a| = |a|^2。

点乘测试

点乘非常适合用来测试两个向量是否共线或垂直,以及他们是否大致上指向同一方向或相反方向。对于两个任意的向量ab,游戏程序员通常使用以下测试,如图4.11所示:

  • 共线。  (a ⋅ b) = |a||b|= ab(也就是说,两个向量之间的夹角正好为0度 —— 当两个向量均为单位向量时,结果为+1)。
  • 共线但反向。 (a ⋅ b) = –a(也就是说,两个向量之间的夹角为180度 —— 当两个向量均为单位向量时,结果为-1)。
  • 垂直。 (a ⋅ b) = 0(也就是说,两个向量之间的夹角为90度)。
  • 同向。(a ⋅ b) > 0(也就是说,他们之间的夹角小于90度)。
  • 反向。(a ⋅ b) < 0(也就是说,他们之间的夹角大于90度)。

点乘的一些其他应用

点乘可运用于游戏编程中的方方面面。例如,假设我们想要知道一个敌人是处于玩家的身前还是处于玩家的身后。我们可以通过将玩家的位置P与敌人的位置E相减(v = E – P)。假设我们拥有一个向量f指向玩家面对的方向。(我们将会在4.3.10.3中看到,向量f可以直接从玩家的模型世界矩阵中取得。)点乘d = v ⋅ f可以被用来测试敌人是否在玩家身前 —— 如果结果为正数则敌人在玩家身前,负数则是在身后。

点乘还可以用于判断一个点是处于平面的上方还是下方(这在编写一个月球登陆游戏中可能会很有用)。我们可以使用两个向量来定义一个平面:平面上的任意一点Q,以及一个垂直于平面的单位向量n(法向量)。为了找出点P相对于平面的高度h,我们首先要计算出从平面上任意点(点Q就很好)指向点P的向量。因此我们有了v = P – Q。向量v与单位向量n的点乘结果就是向量v在由向量n定义的直线上的投影。这正是我们想要寻找的高度。因此,h = v=(– Q)⋅n。这如图4.12所示。

4.2.4.8 叉乘

两个向量的叉乘(也被称作外积和向量积)将产生另一个垂直于这两个向量的向量,如图4.13所示。以下仅给出三维的叉乘运算:
叉乘的大小

叉乘结果向量的大小是每个被乘向量大小的积再乘以两向量夹角的正弦值。(这与点乘大小的定义十分相似,不过要把余弦换成正弦)。

叉乘|b|的大小等于以ab为边的平行四边形的面积,如图4.14所示。因为一个三角形是一个平行四边形的一半,以位置向量V1、V2和V3围成的三角形面积可以由其任意两条边的外积的大小的一半求得:

叉乘的方向

当使用右手坐标系时,你可以使用右手定则来确定叉乘的方向。将你的手握成杯状,手指从向量a转向向量b,则拇指指向的方向即为叉乘(b)的方向。

注意当使用左手坐标系时,叉乘的方向由左手定则确定。这意味着叉乘的方向取决于所选择的坐标系统。这可能在最开始看上去有点奇怪,但请记住,坐标系统的选择并不会影响我们将要进行的数学计算 —— 它仅改变了数字在三维空间中的可视化效果。当将右手坐标系和左手坐标系相互转换时,所有点和向量的数值保持不变,只是一条轴发生了反转。因此如果叉乘正好发生在我们进行了反转的那条轴(例如z轴)上时,我们需要将那一条轴所对应的值反转。如果不这样做,那我们就需要对叉乘本身的数学定义进行修改,以使得叉乘后z轴的值会在心坐标系中取反。我不会让这些规则导致我失眠。只需要记住:当可视化叉乘时,在右手坐标系中使用右手定则,而在左手坐标系中使用左手定则。

叉乘的属性

叉乘不符合交换率(因此是顺序相关的):

× b ≠ × a

然而,它符合反交换率:
× b = × a

叉乘符合加法分配律
× (c) = (× b) + (× c)

它和标量的乘法结合如下:

(sa) × b = a × (sb) = s(a × b)

笛卡尔基本向量与叉乘的关系如下:
(i x j) = -(j x i) = k
(j x k) = -(k x j) = i
(k x i) = -(i x k) = j

这三个叉乘定义了绕轴的正方向旋转。从x到y的正方向旋转(绕z轴),从y到z的正方向旋转(绕x轴),以及从z到x的正方向旋转(绕y轴)。正如我们将会看到的,这给了我们一个提示,那就是为什么绕y轴旋转的矩阵看上去与绕x或绕z轴旋转的矩阵相反。

实际中运用的叉乘

叉乘在游戏中有许多应用。其中一个最常见的用法是用来寻找垂直于另两个向量的向量。正如我们将在章节4.3.10.2所看到的,如果我们已经知道一个对象的本地单位向量组(本地向量ijk),那么我们就可以很容易找到表示这个对象朝向的矩阵。假设我们只知道一个对象的本地向量k —— 也就是该物体面对的方向。如果我们假设物体没有绕k方向的旋转,我们就可以通过将本地向量k(我们已知的)与世界坐标系的上方向向量(等于[0 1 0])做叉乘,以求得本地向量i。然后我们就可以通过简单地对ik做如下叉乘j = k x i以求得本地向量j

非常相似的方法可以用来求出一个三角形或其它形状的表面的法向量。给出平面上的三点P1、P2和P3,法向量n = normalize[(P2 – P1) x (P3 – P1)]。

叉乘还用于物理模拟。当一个力施加到一个对象上时,它将使对象产生旋转运动(如果力并非施加在对象的重心)。这个旋转力被称作力矩,它是如下计算的。给定力F,以及从重心到受力点的向量r,力矩N = r x F

4.2.5 点和向量的线性插值

在游戏中,我们经常需要找到介于两个已知向量中间的向量。例如,我们希望在两秒内将物体从A点移动到B点,帧率为每秒30帧,我们就需要在AB之间计算出60个中间位置。

线性插值是一种用于寻找两个已知点之间的中间点的简单的数学运算。该运算通常被缩写为LERP(linear interpolation)。该运算定义如下,其中β的范围是[0, 1]:

从几何上来说,L = LERP(AB, β)是某点由A开始,沿着AB方向移动百分之β距离后的位置,如图4.15所示。从数学上说,LERP函数只是求两个输入向量的加权平均,两个向量的权重分别为(1 – β)和β。注意到权重总是从0加到1,这是任何加权平均操作的通用要求。

发表评论

电子邮件地址不会被公开。 必填项已用*标注