在使用 OpenGL 的应用程序中,当我们指定了模型的顶点后,顶点依次会变换到不同的 OpenGL 空间中,最后才会被显示到屏幕上。在变换的过程中,通过使用矩阵,我们更高效地来完成这些变换工作。
本篇博客主要介绍的是矩阵以及矩阵在空间几何中的应用。关于 OpenGL 空间,我把它们安排在了另一篇博客OpenGL 的空间变换(下):空间变换中来介绍。
本篇博客主要分为两部分:矩阵基础和矩阵在空间几何中的应用。对熟悉矩阵的读者来说,可以跳过矩阵基础直接阅读第二部分。
矩阵基础
数学上,一个 mxn 的矩阵是一个由 m 行 n 列元素排列成的矩形阵列。矩阵里的元素可以是数字、符号或者数学式。例如下面是一个由 6 个数字构成的 2 行 3 列矩阵:
对于行(列)数为 1 的矩阵,我们称为行(列)向量。注意,这里的向量与空间几何中的向量并不是同一个概念。为了更好地区分两者,接下来只要描述的是矩阵的向量,本文都会以行(列)向量来表示。否则,描述的就是空间几何中的向量。
矩阵的基本运算
矩阵最基本的运算包括加(减)法、数乘和转置运算。
加(减)法:mxn 矩阵 A 和 B 的和(差):A±B 为一个 mxn 矩阵,其中每个元素是 A 和 B 相应元素的和(差)。
根据加(减)法我们还可以推导出,矩阵的取负操作实则是对每个元素进行取负操作。
数乘:标量 c 与矩阵 A 的数乘:c·A 的每个元素是 A 的相应元素与 c 的乘积。
转置:mxn 矩阵 A 的转置是一个 nxm 的矩阵,记为 A‘,其中的第 i 行第 j 列元素是原矩阵 A 的第 j 行第 i 列元素。
矩阵乘法
与矩阵的数乘不同,矩阵的乘法是指两个矩阵相乘,并且当且仅当在第一个矩阵 A 的列数与另一个矩阵 B 的行数相等时才有定义。假设 A 为 mxn 矩阵,而 B 是 nxp 矩阵,那么 A 乘以 B 的乘积 A·B 则是个 m x p 的矩阵。它的任意元素为:
其中, 其中1 ≤ i ≤ m, 1 ≤ j ≤ p。
通过下面这张示意图,我们可以更好地理解 A·B 的过程:
矩阵的乘法满足结合律和对矩阵加法的分配律(左分配律和右分配律):
结合律:(A·B)·C = A·(B·C)
左分配律:(A+B)·C = A·C+B·C
右分配律:C·(A+B) = C·A+C·B
矩阵的乘法与数乘运算之间也满足类似结合律的规律:c·(A·B) = (c·A)·B = A·(c·B);与转置之间则满足倒置的分配律:(A·B)’ = B’·A’。
值得注意的是,矩阵乘法不满足交换律。一般来说,矩阵 A 及 B 的乘积 A·B 存在,但 B·A 不一定存在。即使存在,大多数时候 A·B 也不等于 B·A。
特殊方阵
我们把行数与列数相等的矩阵称为方块矩阵,简称方阵或 n 维矩阵。对于一个方阵来说,其主对角线是一条由左上角至右下角的对角线。而另一条对角线则称作反对角线或次对角线。下面来介绍两种特殊的方阵(也叫特殊矩阵):
单位矩阵
如果一个 n 维矩阵除了主对角线的一组元素为 1.0 之外,其他元素均为 0.0。那么称该矩阵为单位矩阵。例如,下面是一个 4 维单位矩阵:
将一个矩阵乘以(无论是左乘还是右乘)一个单位矩阵,就相当于该矩阵乘以 1(这里指的是数乘),不会发生任何变化。
逆矩阵
设有两个 n 维矩阵 A、B,如果 A 乘以(无论左乘还是右乘)B 得到的是一个单位矩阵,那么 A 与 B 就互称为逆矩阵。一个矩阵的逆矩阵如果存在的话,那么就是唯一的。用公式来表示:A·B = B·A = 单位矩阵。
当然,除了上述的两种特殊矩阵之外,还有许多其他的特殊矩阵。但对于本文接下来的内容,我们只需要知道单位矩阵和逆矩阵就足够了。
矩阵在空间几何中的应用
矩阵是高等代数中的常见工具,也常见于统计分析等应用数学学科中。在物理学中,矩阵于电路学、力学、光学和量子物理都有应用。不仅如此,矩阵在空间几何中也有很重要的应用,它可以用来描述空间几何中的对象(坐标系、点位置、向量)和变换,这是接下来将详细介绍的内容。
在学习矩阵在空间几何中的应用之前,我们先来了解一个空间几何的重要概念——齐次坐标。
齐次坐标
一般来说,我们都是用笛卡尔坐标来描述几何空间的。在三维空间中,笛卡尔坐标系是由三条两两垂直的坐标轴构成的,三条坐标轴相交于原点:
我们可以使用 (x, y, z) 的方式来表示三维空间中一个点的位置。其中 x、y、z 分别对应点在 x-轴、y-轴、z-轴上的坐标。除此之外,这种方式也可以用来表示三维空间中的一个向量。如果用来表示向量的话,那么 x、y、z 则分别表示向量在 x-轴、y-轴、z-轴方向上的分量。
笛卡尔坐标通过上述方式用 N 个分量来描述 N 维空间。这种方式本身存在一个缺陷,那就是它无法用来表示一个无穷远的点,比如 (∞, ∞, ∞)。针对这个问题,数学家们提出了齐次坐标。
齐次坐标在笛卡尔坐标的基础上增加了一个新的分量 w,用 N+1 个分量来描述 N 维空间。对于笛卡尔坐标系中的任意一个点 (x, y, z),都可表示为一族齐次坐标 (w·x, w·y, w·z, w) ,其中 w 不等于 0。
所以,对于笛卡尔坐标系中的点的位置 (1, 2, 1), 我们可以用齐次坐标表示为 (1, 2, 1, 1) 或 (2, 4, 2, 2) 等等。如果想要将一个齐次坐标转换为笛卡尔坐标,只需将齐次坐标的每一分量都除以 w,使得齐次坐标的 w 分量将变为 1。我们把这个过程称为齐次坐标标准化。
如果点 (1, 2, 1) 沿着它与坐标系原点所在的直线移动到无限远处,那么它的位置用齐次坐标来表示则应该是 (1, 2, 1, 0)。(本质上是一个向量)
对于齐次坐标来说,点位置和向量采用的表示方法是不同的:
点位置:(x, y, z, w),其中 w 不为 0
向量:(x, y, z, 0)
相比笛卡尔坐标(点位置和向量都采用同样的表示方法),使用齐次坐标来表示可以更好地区分点位置和向量。
齐次坐标的好处其实还不止这些,这个我们后面会再讲到。
用矩阵来表示三维空间中的对象
在三维空间中,一个点的位置或一个向量可以用一个四分量的齐次坐标来表示。对应地,我们可以用一个 4x1 的矩阵(列向量)来表示该齐次坐标。而对于坐标系,它是由一个原点和三条坐标轴的方向来定义。即它可以由一个点位置和三个向量来确定。我们可以用下面的 4 维矩阵来表示一个三维空间的坐标系:
其中,前三个列向量分别表示坐标系 x-轴、y-轴、z-轴;第四个列向量表示坐标系的原点。
用矩阵来表示三维空间中的变换
在对一个几何对象进行平移、旋转或缩放操作时,对象的几何状态(位置、方向)会发生相应的变化。变化前后的状态我们可以用矩阵来表示。不仅如此,对于这些操作(平移、旋转或缩放)本身,我们也可以用矩阵来表示,这种矩阵称为变换矩阵。变换矩阵描述的是操作本身,与被操作的几何对象无关。
将表示几何对象的矩阵乘以变换矩阵,所得的结果就是进行相应操作后的几何对象的矩阵表示。
平移变换
平移矩阵仅仅是将几何对象沿着其父坐标系的 3 个坐标轴中的一个或多个进行平移:
其中 t.x、t.y 和 t.z 分别表示在 x-轴、y-轴和 z-轴上的平移。
点位置的平移变换:
向量的平移变换:
子坐标系的平移变换:
从变换后的结果矩阵可以看出,点位置会随着平移而发生相应的变化;而向量则始终保持不变。同理,子坐标系的原点也会随着平移发生相应的变化,而其坐标轴则始终保持不变。
旋转变换
对于三维空间中的旋转,我们可以有三种方式来描述,分别是:旋转矩阵、欧拉角,还有四元数。
旋转矩阵
旋转矩阵将一个几何对象围绕其父坐标系的 3 个坐标轴中的一个或多个进行旋转。不同坐标轴上的旋转矩阵是不一样的:
其中,s 表示 sin,c 表示 cos。
按照顺序将以上三个基本旋转矩阵乘在一起,可以得出一个复合旋转矩阵。该矩阵可以一次性完成 x、y、z 三条轴上的所有旋转变换:
该矩阵表示先围绕 x-轴旋转 φ ,紧接着围绕 y-轴旋转 θ,最后再围绕 z-轴旋转 ψ,这里的坐标轴指的是父坐标系的坐标轴。
欧拉角
欧拉角是一个包含三个角度的的集合,我们可以将其表示为 (α, β, γ)。每个角度分别表示围绕对应坐标轴的旋转角度。这里的坐标轴指的是被旋转模型自身的坐标系,而不是其父坐标系。对于一个给定的欧拉角 (α, β, γ),我们会将模型先围绕 α 所对应的坐标轴旋转 α,然后基于旋转后模型自身的坐标系,再围绕 β 所对应的坐标轴旋转 β,最后基于旋转后模型自身的坐标系,再围绕 γ 所对应的坐标轴旋转 γ。
值得注意的是,欧拉角所包含的三个角度 α、β、γ 与坐标轴的对应关系不一定是 XYZ。只要连续的两次旋转不是基于(围绕)同一条坐标轴即可,如 ZYX 甚至是 ZXZ 都可以,但是像 ZZX 则不行。欧拉角总共有 12 种对应方式。
按照对应方式的顺序将基本旋转矩阵乘在一起,就可以得出欧拉角的矩阵表示。假设一个欧拉角的对应方式为 ZXZ,则其矩阵表示为 Rz(α)·Ry(β)·Rz(γ)。
细心的同学可能已经发现,在计算复合旋转矩阵时,最先进行的旋转其矩阵在最右侧,说明该矩阵最先与点的齐次坐标相乘,旋转矩阵按照旋转的次序从右向左排列。而在欧拉角中,最先进行的旋转其矩阵在最左侧。这是因为对于旋转矩阵来说,我们始终是以模型的父坐标系为参照来的,父坐标系不会因为模型的旋转而发生变化。而对于欧拉角来说,每一次的旋转都是基于模型自身的坐标系,而不是其父坐标系。当模型被旋转时,其自身的坐标系也会跟着一起旋转。
欧拉角有一个弊端——万向节死锁。因为万向节死锁的存在,所以在使用欧拉角时无法实现球面平滑插值。有兴趣了解的读者可以观看此视频 欧拉旋转—万向节锁—在线播放—优酷网,视频高清在线观看。
比起旋转矩阵,欧拉角具有节省存储空间(因为它只需用三个值)和直观的优点,很多游戏引擎都是使用欧拉角来表示旋转的。虽然欧拉角存在万向节死锁的弊端,但是我们可以通过添加一些制约来规避它。除了这两种表示方式,我们还可以另外一种更好的方式来表示三维空间中的旋转,那就是四元数。
四元数
我们还可以通过指定一条任意方向的旋转轴 A(用标准化的向量表示)以及围绕旋转轴 A 旋转的角度 θ,来描述三维空间中的任意旋转。这种表示方法称为轴-角。我们可以通过标准旋转矩阵的复合运用来表示轴-角:
Rx(-p)·Ry(-q)·Rz(θ)·Ry(q)·Rx(p)
原理是通过两次旋转使得旋转轴 A 与 z-轴重合,然后围绕 z-轴旋转 θ。最后再通过两次旋转将旋转轴 A 摆回到原来的角度。和计算复合旋转矩阵时一样,最先进行的旋转,其矩阵在最右侧:
首先,围绕 x-轴旋转角度 p,使得旋转轴 A 在 x-轴 与 z-轴所在的平面上
然后,围绕 y-轴旋转角度 q,使得旋转轴 A 与 z-轴重合
接着,围绕 z-轴旋转角度 θ(实际上也是围绕旋转轴 A 旋转 θ)
之后,围绕 y-轴转回角度 q(即 -q)
最后,围绕 x-轴转回角度 p(即 -p)
其中,x-轴、y-轴和 z-轴指的是父坐标系的坐标轴,p 和 q 的值需要通过旋转轴 A 计算出来。
不过,上述的表示方式还是比较复杂。我们可以使用一种更加简洁的方式来表示轴-角——四元数。它可以表示为 (a, b, c, d),我们定义其与旋转轴 A、旋转角度 θ 的关系如下:
a = sin(θ/2) * A.x
b = sin(θ/2) * A.y
c = sin(θ/2) * A.z
d = cos(θ/2)
其中,A.x、A.y、A.z 分别对应旋转轴 A 的三个分量。
对于四元数所表示的旋转,其对应的旋转矩阵如下(这里省去推导过程直接给出结论):
用四元数来表示旋转需要的存储空间很小(只需用四个值)。而且相比起欧拉角来说,四元数不存在万向节死锁的问题。所以,我们可以使用被称为球面线性插值的方法对四元数进行插值运算,从而解决了平滑旋转的插值问题。
假设几何对象(点、向量或子坐标系)围绕其父坐标系的 y-轴旋转了 θ,则对应的矩阵变换如下:
点位置的旋转变换
向量的旋转变换
坐标系的旋转变换
从变换后的结果矩阵可以看出,无论点位置、还是向量都会随着旋转而发生相应的变化;同样,子坐标系的原点和坐标轴也会随着旋转而发生相应的变化。
缩放变换
缩放矩阵将几何对象沿着其父坐标系的 3 个坐标轴方向,按照指定比例进行放大或缩小:
这里 s.x、s.y 和 s.z 分别代表在 x、y 和 z 轴方向上的缩放比。
点位置的缩放变换
向量的缩放变换
坐标系的缩放变换
从变换后的结果矩阵可以看出,无论点位置、还是向量都会随着缩放而发生相应的变化;同理,子坐标系的原点和坐标轴也会随着缩放而发生相应的变化。
综合变换
实际上,我们很少会只进行这三种变换中的一种。反而,总会想要同时进行这些变换。我们可以将这三种类型的变换矩阵乘在一起来得到一个复合的几何变换矩阵。将一个几何对象乘以该复合矩阵,可以对该对象同时进行所有相应的变换。
另外,对于点位置(或坐标系的原点)而言,如果其 w 分量不为 1,则应先将点位置(或坐标系的原点)进行齐次坐标标准化。然后,再使用其标准化的结果来进行对应的几何变换。
本篇博客介绍了矩阵的基本知识,以及如何用矩阵来表示几何对象和三种最基本的几何变换。除此之外,矩阵还可以用来表示一些其他的变换。这部分的内容会在OpenGL 的空间变换(下):空间变换中进行详细的介绍。