OpenGL学习笔记(三):向量、矩阵和变换

150 阅读24分钟

OpenGL学习笔记(一):在Mac上编译GLFW并配置到Xcode项目

向量

向量最基本的定义就是一个方向。或者更正式的说,向量有一个方向(Direction)和大小(Magnitude,也叫做强度或长度)。如果一个向量有2个维度,它表示一个平面的方向,当它有3个维度的时候它可以表达一个3D世界的方向。

下面你会看到3个向量,每个向量在2D图像中都用一个箭头 ( x , y )" role="presentation" style="position: relative;">(x, y)表示。我们在2D图片中展示这些向量,因为这样子会更直观一点。你可以把这些2D向量当做z坐标为0的3D向量。由于向量表示的是方向,起始于何处并不会改变它的值。下图我们可以看到向量 v ¯ " role="presentation" style="position: relative;">\bar{v}和 w ¯ " role="presentation" style="position: relative;">\bar{w}是相等的,尽管他们的起始点不同:

用公式表示就是这样: v ¯ = ( x y z ) " role="presentation" style="position: relative;">\bar{v} = \begin{pmatrix} \color{red}x \\ \color{green}y \\ \color{blue}z \end{pmatrix}

由于向量是一个方向,所以有些时候会很难形象地将它们用位置(Position)表示出来。为了让其更为直观,我们通常设定这个方向的原点为 ( 0 , 0 , 0 ) ​" role="presentation" style="position: relative;">(0, 0, 0),然后指向一个方向,对应一个点,使其变为位置向量(Position Vector)(你也可以把起点设置为其他的点,然后说:这个向量从这个点起始指向另一个点)。比如说位置向量 ( 3 , 5 ) ​" role="presentation" style="position: relative;">(3, 5)在图像中的起点会是 ( 0 , 0 ) ​" role="presentation" style="position: relative;">(0, 0),并会指向 ( 3 , 5 ) ​" role="presentation" style="position: relative;">(3, 5)。我们可以使用向量在2D或3D空间中表示方向与位置.

向量与标量的运算

标量(Scalar)只是一个数字(或者说是仅有一个分量的向量)。当把一个向量加/减/乘/除一个标量,我们可以简单的把向量的每个分量分别进行该运算。对于加法来说会像这样:

( 1 2 3 ) + x = ( 1 + x 2 + x 3 + x ) ​" role="presentation" style="text-align: center; position: relative;">⎛⎝⎜123⎞⎠⎟+x=⎛⎝⎜1+x2+x3+x⎞⎠⎟​ ( 1 2 3 ) + x = ( 1 + x 2 + x 3 + x ) ​

向量加减

向量的加法可以被定义为是分量的(Component-wise)相加,即将一个向量中的每一个分量加上另一个向量的对应分量:

v ¯ = ( 1 2 3 ) , k ¯ = ( 4 5 6 ) → v ¯ + k ¯ = ( 1 + 4 2 + 5 3 + 6 ) = ( 5 7 9 ) ​" role="presentation" style="text-align: center; position: relative;">v¯=⎛⎝⎜123⎞⎠⎟,k¯=⎛⎝⎜456⎞⎠⎟→v¯+k¯=⎛⎝⎜1+42+53+6⎞⎠⎟=⎛⎝⎜579⎞⎠⎟​ v ¯ = ( 1 2 3 ) , k ¯ = ( 4 5 6 ) → v ¯ + k ¯ = ( 1 + 4 2 + 5 3 + 6 ) = ( 5 7 9 ) ​

向量长度

我们使用勾股定理(Pythagoras Theorem)来获取向量的长度(Length)/大小(Magnitude)。如果你把向量的x与y分量画出来,该向量会和x与y分量为边形成一个三角形:

因为两条边(x和y)是已知的,如果希望知道斜边 v ¯ ​" role="presentation" style="position: relative;">\color{red}{\bar{v}}的长度,我们可以直接通过勾股定理来计算:

| | v ¯ | | = x 2 + y 2 ​" role="presentation" style="text-align: center; position: relative;">||v¯||=x2+y2−−−−−−√​ | | v ¯ | | = x 2 + y 2 ​

有一个特殊类型的向量叫做单位向量(Unit Vector)。单位向量有一个特别的性质——它的长度是1。我们可以用任意向量的每个分量除以向量的长度得到它的单位向量 n ^ " role="presentation" style="position: relative;">\hat{n}

n ^ = v ¯ | | v ¯ | | ​" role="presentation" style="text-align: center; position: relative;">n^=v¯||v¯||​ n ^ = v ¯ | | v ¯ | | ​

我们把这种方法叫做一个向量的标准化(Normalizing)。单位向量头上有一个^样子的记号。通常单位向量会变得很有用,特别是在我们只关心方向不关心长度的时候。

向量相乘

两个向量相乘是一种很奇怪的情况。普通的乘法在向量上是没有定义的,因为它在视觉上是没有意义的。但是在相乘的时候我们有两种特定情况可以选择:一个是点乘(Dot Product),记作 v ¯ ⋅ k ¯ " role="presentation" style="position: relative;">\bar{v} \cdot \bar{k},另一个是叉乘(Cross Product),记作 v ¯ × k ¯ " role="presentation" style="position: relative;">\bar{v} \times \bar{k}

点乘

两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。可能听起来有点费解,我们来看一下公式:

v ¯ ⋅ k ¯ = | | v ¯ | | ⋅ | | k ¯ | | ⋅ cos ⁡ θ" role="presentation" style="text-align: center; position: relative;">v¯⋅k¯=||v¯||⋅||k¯||⋅cosθ v ¯ ⋅ k ¯ = | | v ¯ | | ⋅ | | k ¯ | | ⋅ cos ⁡ θ

它们之间的夹角记作 θ ​" role="presentation" style="position: relative;">\theta。为什么这很有用?想象如果 v ¯ ​" role="presentation" style="position: relative;">\bar{v}和 k ¯ ​" role="presentation" style="position: relative;">\bar{k}都是单位向量,它们的长度会等于1。这样公式会有效简化成:

v ¯ ⋅ k ¯ = 1 ⋅ 1 ⋅ c o s θ = c o s θ " role="presentation" style="text-align: center; position: relative;">v¯⋅k¯=1⋅1⋅cosθ=cosθ v ¯ ⋅ k ¯ = 1 ⋅ 1 ⋅ c o s θ = c o s θ

现在点积只定义了两个向量的夹角。你也许记得90度的余弦值是0,0度的余弦值是1。使用点乘可以很容易测试两个向量是否正交(Orthogonal)或平行(正交意味着两个向量互为直角)。

也可以通过点乘的结果计算两个非单位向量的夹角,点乘的结果除以两个向量的长度之积,得到的结果就是夹角的余弦值,即 c o s θ ​" role="presentation" style="position: relative;">cos\theta

cos ⁡ θ = v ¯ ⋅ k ¯ | | v ¯ | | ⋅ | | k ¯ | | ​" role="presentation" style="text-align: center; position: relative;">cosθ=v¯⋅k¯||v¯||⋅||k¯||​ cos ⁡ θ = v ¯ ⋅ k ¯ | | v ¯ | | ⋅ | | k ¯ | | ​

点乘是通过将对应分量逐个相乘,然后再把所得积相加来计算的。

( 0.6 − 0.8 0 ) ⋅ ( 0 1 0 ) = ( 0.6 ∗ 0 ) + ( − 0.8 ∗ 1 ) + ( 0 ∗ 0 ) = − 0.8" role="presentation" style="text-align: center; position: relative;">⎛⎝⎜0.6−0.80⎞⎠⎟⋅⎛⎝⎜010⎞⎠⎟=(0.6∗0)+(−0.8∗1)+(0∗0)=−0.8 ( 0.6 − 0.8 0 ) ⋅ ( 0 1 0 ) = ( 0.6 ∗ 0 ) + ( − 0.8 ∗ 1 ) + ( 0 ∗ 0 ) = − 0.8

叉乘

叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量。如果输入的两个向量也是正交的,那么叉乘之后将会产生3个互相正交的向量。

两个正交向量A和B叉积:

( A x A y A z ) × ( B x B y B z ) = ( A y ⋅ B z − A z ⋅ B y A z ⋅ B x − A x ⋅ B z A x ⋅ B y − A y ⋅ B x ) " role="presentation" style="text-align: center; position: relative;">⎛⎝⎜AxAyAz⎞⎠⎟×⎛⎝⎜BxByBz⎞⎠⎟=⎛⎝⎜Ay⋅Bz−Az⋅ByAz⋅Bx−Ax⋅BzAx⋅By−Ay⋅Bx⎞⎠⎟ ( A x A y A z ) × ( B x B y B z ) = ( A y ⋅ B z − A z ⋅ B y A z ⋅ B x − A x ⋅ B z A x ⋅ B y − A y ⋅ B x )

矩阵

简单来说矩阵就是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。下面是一个2×3矩阵的例子:

[ 1 2 3 4 5 6 ] ​" role="presentation" style="text-align: center; position: relative;">[142536]​ [ 1 2 3 4 5 6 ] ​

矩阵可以通过 ( i , j )" role="presentation" style="position: relative;">(i, j)进行索引,i是行,j是列,这就是上面的矩阵叫做2×3矩阵的原因。

矩阵的加减

矩阵与标量之间的加减定义如下:

[ 1 2 3 4 ] + 3 = [ 1 + 3 2 + 3 3 + 3 4 + 3 ] = [ 4 5 6 7 ] ​" role="presentation" style="text-align: center; position: relative;">[1324]+3=[1+33+32+34+3]=[4657]​ [ 1 2 3 4 ] + 3 = [ 1 + 3 2 + 3 3 + 3 4 + 3 ] = [ 4 5 6 7 ] ​

矩阵与矩阵之间的加减就是两个矩阵对应元素的加减运算,所以总体的规则和与标量运算是差不多的,只不过在相同索引下的元素才能进行运算。

[ 4 2 1 6 ] − [ 2 4 0 1 ] = [ 4 − 2 2 − 4 1 − 0 6 − 1 ] = [ 2 − 2 1 5 ] ​" role="presentation" style="text-align: center; position: relative;">[4126]−[2041]=[4−21−02−46−1]=[21−25]​ [ 4 2 1 6 ] − [ 2 4 0 1 ] = [ 4 − 2 2 − 4 1 − 0 6 − 1 ] = [ 2 − 2 1 5 ] ​

矩阵的数乘

和矩阵与标量的加减一样,矩阵与标量之间的乘法也是矩阵的每一个元素分别乘以该标量。

2 ⋅ [ 1 2 3 4 ] = [ 2 ⋅ 1 2 ⋅ 2 2 ⋅ 3 2 ⋅ 4 ] = [ 2 4 6 8 ] ​" role="presentation" style="text-align: center; position: relative;">2⋅[1324]=[2⋅12⋅32⋅22⋅4]=[2648]​ 2 ⋅ [ 1 2 3 4 ] = [ 2 ⋅ 1 2 ⋅ 2 2 ⋅ 3 2 ⋅ 4 ] = [ 2 4 6 8 ] ​

矩阵相乘

矩阵相乘有一些限制:

  1. 只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘。

  2. 矩阵相乘不遵守交换律(Commutative),也就是说 A ⋅ B ≠ B ⋅ A ​" role="presentation" style="position: relative;">A \cdot B \neq B \cdot A

[ 1 2 3 4 ] ⋅ [ 5 6 7 8 ] = [ 1 ⋅ 5 + 2 ⋅ 7 1 ⋅ 6 + 2 ⋅ 8 3 ⋅ 5 + 4 ⋅ 7 3 ⋅ 6 + 4 ⋅ 8 ] = [ 19 22 43 50 ] " role="presentation" style="text-align: center; position: relative;">[1324]⋅[5768]=[1⋅5+2⋅73⋅5+4⋅71⋅6+2⋅83⋅6+4⋅8]=[19432250] [ 1 2 3 4 ] ⋅ [ 5 6 7 8 ] = [ 1 ⋅ 5 + 2 ⋅ 7 1 ⋅ 6 + 2 ⋅ 8 3 ⋅ 5 + 4 ⋅ 7 3 ⋅ 6 + 4 ⋅ 8 ] = [ 19 22 43 50 ]

矩阵的乘法是一系列乘法和加法组合的结果,它使用到了左侧矩阵的行和右侧矩阵的列。

结果矩阵的维度是(n, m),n等于左侧矩阵的行数,m等于右侧矩阵的列数。

[ 4 2 0 0 8 1 0 1 0 ] ⋅ [ 4 2 1 2 0 4 9 4 2 ] = [ 4 ⋅ 4 + 2 ⋅ 2 + 0 ⋅ 9 4 ⋅ 2 + 2 ⋅ 0 + 0 ⋅ 4 4 ⋅ 1 + 2 ⋅ 4 + 0 ⋅ 2 0 ⋅ 4 + 8 ⋅ 2 + 1 ⋅ 9 0 ⋅ 2 + 8 ⋅ 0 + 1 ⋅ 4 0 ⋅ 1 + 8 ⋅ 4 + 1 ⋅ 2 0 ⋅ 4 + 1 ⋅ 2 + 0 ⋅ 9 0 ⋅ 2 + 1 ⋅ 0 + 0 ⋅ 4 0 ⋅ 1 + 1 ⋅ 4 + 0 ⋅ 2 ] = [ 20 8 12 25 4 34 2 0 4 ] " role="presentation" style="position: relative;">⎡⎣⎢400281010⎤⎦⎥⋅⎡⎣⎢429204142⎤⎦⎥=⎡⎣⎢4⋅4+2⋅2+0⋅90⋅4+8⋅2+1⋅90⋅4+1⋅2+0⋅94⋅2+2⋅0+0⋅40⋅2+8⋅0+1⋅40⋅2+1⋅0+0⋅44⋅1+2⋅4+0⋅20⋅1+8⋅4+1⋅20⋅1+1⋅4+0⋅2⎤⎦⎥=⎡⎣⎢2025284012344⎤⎦⎥ [ 4 2 0 0 8 1 0 1 0 ] ⋅ [ 4 2 1 2 0 4 9 4 2 ] = [ 4 ⋅ 4 + 2 ⋅ 2 + 0 ⋅ 9 4 ⋅ 2 + 2 ⋅ 0 + 0 ⋅ 4 4 ⋅ 1 + 2 ⋅ 4 + 0 ⋅ 2 0 ⋅ 4 + 8 ⋅ 2 + 1 ⋅ 9 0 ⋅ 2 + 8 ⋅ 0 + 1 ⋅ 4 0 ⋅ 1 + 8 ⋅ 4 + 1 ⋅ 2 0 ⋅ 4 + 1 ⋅ 2 + 0 ⋅ 9 0 ⋅ 2 + 1 ⋅ 0 + 0 ⋅ 4 0 ⋅ 1 + 1 ⋅ 4 + 0 ⋅ 2 ] = [ 20 8 12 25 4 34 2 0 4 ]

向量与矩阵相乘

前面我们把向量表示成这种形式 ( x y z ) " role="presentation" style="position: relative;">\begin{pmatrix} \color{red}x \\ \color{green}y \\ \color{blue}z \end{pmatrix},这种情况下它其实就是一个N×1矩阵,N表示向量分量的个数。我们也可以把它表示成一个1×N的矩阵,也就是这样​ ( x y z ) " role="presentation" style="position: relative;">\begin{pmatrix} \color{red}x & \color{green}y & \color{blue}z \end{pmatrix}

如果我们有一个N×N矩阵,我们可以用我们的1×N向量乘以这个矩阵,因为这个矩阵的行数等于向量的列数,所以最终得到的结果也是一个1×N的矩阵,这就相当于是给向量做了一个变换。

单位矩阵

单位矩阵是一个除了对角线以外都是0的N×N矩阵。在下式中可以看到,这种变换矩使一个向量完全不变:

( a b c d ) ⋅ [ 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 ] = [ 1 ⋅ a 1 ⋅ b 1 ⋅ c 1 ⋅ d ] = ( a b c d ) " role="presentation" style="text-align: center; position: relative;">(abcd)⋅⎡⎣⎢⎢⎢1000010000100001⎤⎦⎥⎥⎥=[1⋅a1⋅b1⋅c1⋅d]=(abcd) ( a b c d ) ⋅ [ 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 ] = [ 1 ⋅ a 1 ⋅ b 1 ⋅ c 1 ⋅ d ] = ( a b c d )

缩放

对一个向量进行缩放(Scaling)就是对向量的长度进行缩放,而保持它的方向不变。由于我们进行的是2维或3维操作,我们可以分别定义一个有2或3个缩放变量的向量,每个变量缩放一个轴(x、y或z)。

我们先来尝试缩放向量 v ¯ = ( 3 , 2 )" role="presentation" style="position: relative;">\color{red}{\bar{v}} = (3,2)。我们可以把向量沿着x轴缩放0.5,使它的宽度缩小为原来的二分之一;我们将沿着y轴把向量的高度缩放为原来的两倍。我们看看把向量缩放(0.5, 2)倍所获得的 s ¯ " role="presentation" style="position: relative;">\color{blue}{\bar{s}}是什么样的:

OpenGL通常是在3D空间进行操作的,对于2D的情况我们可以把z轴缩放1倍,这样z轴的值就不变了。我们刚刚的缩放操作是不均匀(Non-uniform)缩放,因为每个轴的缩放因子(Scaling Factor)都不一样。如果每个轴的缩放因子都一样那么就叫均匀缩放(Uniform Scale)。

我们下面会构造一个变换矩阵来为我们提供缩放功能。我们从单位矩阵了解到,每个对角线元素会分别与向量的对应元素相乘。如果我们把1变为3会怎样?这样子的话,我们就把向量的每个元素乘以3了,这事实上就把向量缩放3倍。如果我们把缩放变量表示为 ( S 1 , S 2 , S 3 )" role="presentation" style="position: relative;">(\color{red}{S_1}, \color{green}{S_2}, \color{blue}{S_3})可以为任意向量 ( x , y , z )" role="presentation" style="position: relative;">(x,y,z)定义一个缩放矩阵:

( x y z ) ⋅ [ S 1 0 0 0 S 2 0 0 0 S 3 ] = ( S 1 ⋅ x S 2 ⋅ y S 3 ⋅ z ) ​" role="presentation" style="text-align: center; position: relative;">(xyz)⋅⎡⎣⎢S1000S2000S3⎤⎦⎥=(S1⋅xS2⋅yS3⋅z)​ ( x y z ) ⋅ [ S 1 0 0 0 S 2 0 0 0 S 3 ] = ( S 1 ⋅ x S 2 ⋅ y S 3 ⋅ z ) ​

位移

位移(Translation)是在原始向量的基础上加上另一个向量从而获得一个在不同位置的新向量的过程,从而在位移向量基础上移动了原始向量。

和缩放矩阵一样,在4×4矩阵上有几个特别的位置用来执行特定的操作,对于位移来说它们是第四列最上面的3个值。如果我们把位移向量表示为 ( T x , T y , T z ) ​" role="presentation" style="position: relative;">(\color{red}{T_x},\color{green}{T_y},\color{blue}{T_z}),我们就能把位移矩阵定义为:

( x y z w ) ⋅ [ 1 0 0 0 0 1 0 0 0 0 1 0 T x T y T z 1 ] = ( x + T x y + T y z + T z w ) " role="presentation" style="text-align: center; position: relative;">(xyzw)⋅⎡⎣⎢⎢⎢⎢100Tx010Ty001Tz0001⎤⎦⎥⎥⎥⎥=(x+Txy+Tyz+Tzw) ( x y z w ) ⋅ [ 1 0 0 0 0 1 0 0 0 0 1 0 T x T y T z 1 ] = ( x + T x y + T y z + T z w )

这样是能工作的,因为所有的位移值都要乘以向量的w行,所以位移值会加到向量的原始值上(想想矩阵乘法法则)。而如果你用3x3矩阵我们的位移值就没地方放也没地方乘了,所以是不行的。

齐次坐标(Homogeneous Coordinates)

向量的w分量也叫齐次坐标。想要从齐次向量得到3D向量,我们可以把x、y和z坐标分别除以w坐标。我们通常不会注意这个问题,因为w分量通常是1.0。使用齐次坐标有几点好处:它允许我们在3D向量上进行位移(如果没有w分量我们是不能位移向量的)。

旋转

首先我们来定义一个向量的旋转到底是什么。下图中展示的2D向量 v ¯ ​" role="presentation" style="position: relative;">\bar{v}是由 k ¯ ​" role="presentation" style="position: relative;">\bar{k}向右旋转72度所得的:

在3D空间中旋转需要定义一个角和一个旋转轴(Rotation Axis)。物体会沿着给定的旋转轴旋转特定角度。如果你想要更形象化的感受,可以试试向下看着一个特定的旋转轴,同时将你的头部旋转一定角度。当2D向量在3D空间中旋转时,我们把旋转轴设为z轴(尝试想象这种情况)。

沿x轴旋转:

( x y z 1 ) ⋅ [ 1 0 0 0 0 cos ⁡ θ sin ⁡ θ 0 0 − sin ⁡ θ cos ⁡ θ 0 0 0 0 1 ] = ( x cos ⁡ θ ⋅ y − sin ⁡ θ ⋅ z sin ⁡ θ ⋅ y + cos ⁡ θ ⋅ z 1 ) ​" role="presentation" style="text-align: center; position: relative;">(xyz1)⋅⎡⎣⎢⎢⎢10000cosθ−sinθ00sinθcosθ00001⎤⎦⎥⎥⎥=(xcosθ⋅y−sinθ⋅zsinθ⋅y+cosθ⋅z1)​ ( x y z 1 ) ⋅ [ 1 0 0 0 0 cos ⁡ θ sin ⁡ θ 0 0 − sin ⁡ θ cos ⁡ θ 0 0 0 0 1 ] = ( x cos ⁡ θ ⋅ y − sin ⁡ θ ⋅ z sin ⁡ θ ⋅ y + cos ⁡ θ ⋅ z 1 ) ​

沿y轴旋转:

( x y z 1 ) ⋅ [ cos ⁡ θ 0 − sin ⁡ θ 0 0 1 0 0 sin ⁡ θ 0 cos ⁡ θ 0 0 0 0 1 ] = ( cos ⁡ θ ⋅ x + sin ⁡ θ ⋅ z y − sin ⁡ θ ⋅ x + cos ⁡ θ ⋅ z 1 ) ​" role="presentation" style="text-align: center; position: relative;">(xyz1)⋅⎡⎣⎢⎢⎢cosθ0sinθ00100−sinθ0cosθ00001⎤⎦⎥⎥⎥=(cosθ⋅x+sinθ⋅zy−sinθ⋅x+cosθ⋅z1)​ ( x y z 1 ) ⋅ [ cos ⁡ θ 0 − sin ⁡ θ 0 0 1 0 0 sin ⁡ θ 0 cos ⁡ θ 0 0 0 0 1 ] = ( cos ⁡ θ ⋅ x + sin ⁡ θ ⋅ z y − sin ⁡ θ ⋅ x + cos ⁡ θ ⋅ z 1 ) ​

沿z轴旋转:

( x y z 1 ) ⋅ [ cos ⁡ θ sin ⁡ θ 0 0 − sin ⁡ θ cos ⁡ θ 0 0 0 0 1 0 0 0 0 1 ] = ( cos ⁡ θ ⋅ x − sin ⁡ θ ⋅ y sin ⁡ θ ⋅ x + cos ⁡ θ ⋅ y z 1 ) ​" role="presentation" style="text-align: center; position: relative;">(xyz1)⋅⎡⎣⎢⎢⎢cosθ−sinθ00sinθcosθ0000100001⎤⎦⎥⎥⎥=(cosθ⋅x−sinθ⋅ysinθ⋅x+cosθ⋅yz1)​ ( x y z 1 ) ⋅ [ cos ⁡ θ sin ⁡ θ 0 0 − sin ⁡ θ cos ⁡ θ 0 0 0 0 1 0 0 0 0 1 ] = ( cos ⁡ θ ⋅ x − sin ⁡ θ ⋅ y sin ⁡ θ ⋅ x + cos ⁡ θ ⋅ y z 1 ) ​

对于3D空间中的旋转,一个更好的模型是沿着任意的一个轴,比如单位向量 ( 0.662 , 0.2 , 0.7222 ) ​" role="presentation" style="position: relative;">(0.662, 0.2, 0.7222)旋转,而不是对一系列旋转矩阵进行复合。这样的一个(超级麻烦的)矩阵是存在的,见下面这个公式,其中 ( R x , R y , R z ) ​" role="presentation" style="position: relative;">(\color{red}{R_x}, \color{green}{R_y}, \color{blue}{R_z})代表任意旋转轴:

[ cos ⁡ θ + R x 2 ( 1 − cos ⁡ θ ) R y R x ( 1 − cos ⁡ θ ) + R z sin ⁡ θ R z R x ( 1 − cos ⁡ θ ) − R y sin ⁡ θ 0 R x R y ( 1 − cos ⁡ θ ) − R z sin ⁡ θ cos ⁡ θ + R y 2 ( 1 − cos ⁡ θ ) R z R y ( 1 − cos ⁡ θ ) + R x sin ⁡ θ 0 R x R z ( 1 − cos ⁡ θ ) + R y sin ⁡ θ R y R z ( 1 − cos ⁡ θ ) − R x sin ⁡ θ cos ⁡ θ + R z 2 ( 1 − cos ⁡ θ ) 0 0 0 0 1 ] ​" role="presentation" style="text-align: center; position: relative;">⎡⎣⎢⎢⎢⎢cosθ+Rx2(1−cosθ)RxRy(1−cosθ)−RzsinθRxRz(1−cosθ)+Rysinθ0RyRx(1−cosθ)+Rzsinθcosθ+Ry2(1−cosθ)RyRz(1−cosθ)−Rxsinθ0RzRx(1−cosθ)−RysinθRzRy(1−cosθ)+Rxsinθcosθ+Rz2(1−cosθ)00001⎤⎦⎥⎥⎥⎥​ [ cos ⁡ θ + R x 2 ( 1 − cos ⁡ θ ) R y R x ( 1 − cos ⁡ θ ) + R z sin ⁡ θ R z R x ( 1 − cos ⁡ θ ) − R y sin ⁡ θ 0 R x R y ( 1 − cos ⁡ θ ) − R z sin ⁡ θ cos ⁡ θ + R y 2 ( 1 − cos ⁡ θ ) R z R y ( 1 − cos ⁡ θ ) + R x sin ⁡ θ 0 R x R z ( 1 − cos ⁡ θ ) + R y sin ⁡ θ R y R z ( 1 − cos ⁡ θ ) − R x sin ⁡ θ cos ⁡ θ + R z 2 ( 1 − cos ⁡ θ ) 0 0 0 0 1 ] ​

矩阵的组合

使用矩阵进行变换的真正力量在于,根据矩阵之间的乘法,我们可以把多个变换组合到一个矩阵中。让我们看看我们是否能生成一个变换矩阵,让它组合多个变换。假设我们有一个顶点(x, y, z),我们希望将其缩放2倍,然后位移 ( 1 , 2 , 3 )" role="presentation" style="position: relative;">(1, 2, 3)个单位。我们需要一个位移和缩放矩阵来完成这些变换。结果的变换矩阵看起来像这样:

T r a n s = [ 2 0 0 0 0 2 0 0 0 0 2 0 0 0 0 1 ] ⋅ [ 1 0 0 0 0 1 0 0 0 0 1 0 1 2 3 1 ] = [ 2 0 0 0 0 2 0 0 0 0 2 0 1 2 3 1 ] ​" role="presentation" style="text-align: center; position: relative;">Trans=⎡⎣⎢⎢⎢2000020000200001⎤⎦⎥⎥⎥⋅⎡⎣⎢⎢⎢1001010200130001⎤⎦⎥⎥⎥=⎡⎣⎢⎢⎢2001020200230001⎤⎦⎥⎥⎥​ T r a n s = [ 2 0 0 0 0 2 0 0 0 0 2 0 0 0 0 1 ] ⋅ [ 1 0 0 0 0 1 0 0 0 0 1 0 1 2 3 1 ] = [ 2 0 0 0 0 2 0 0 0 0 2 0 1 2 3 1 ] ​

T r a n s ′ = [ 1 0 0 0 0 1 0 0 0 0 1 0 1 2 3 1 ] ⋅ [ 2 0 0 0 0 2 0 0 0 0 2 0 0 0 0 1 ] = [ 2 0 0 0 0 2 0 0 0 0 2 0 2 4 6 1 ] ​" role="presentation" style="text-align: center; position: relative;">Trans′=⎡⎣⎢⎢⎢1001010200130001⎤⎦⎥⎥⎥⋅⎡⎣⎢⎢⎢2000020000200001⎤⎦⎥⎥⎥=⎡⎣⎢⎢⎢2002020400260001⎤⎦⎥⎥⎥​ T r a n s ′ = [ 1 0 0 0 0 1 0 0 0 0 1 0 1 2 3 1 ] ⋅ [ 2 0 0 0 0 2 0 0 0 0 2 0 0 0 0 1 ] = [ 2 0 0 0 0 2 0 0 0 0 2 0 2 4 6 1 ] ​

注意,当矩阵相乘时我们先写位移再写缩放变换的。矩阵乘法是不遵守交换律的,这意味着它们的顺序很重要。建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会互相影响。比如,如果你先位移再缩放,位移的向量也会同样被缩放。

用最终的变换矩阵乘以我们的向量会得到以下结果:

( x y z 1 ) ⋅ [ 2 0 0 0 0 2 0 0 0 0 2 0 1 2 3 1 ] = ( 2 x + 1 2 y + 2 2 z + 3 1 ) " role="presentation" style="text-align: center; position: relative;">(xyz1)⋅⎡⎣⎢⎢⎢2001020200230001⎤⎦⎥⎥⎥=(2x+12y+22z+31) ( x y z 1 ) ⋅ [ 2 0 0 0 0 2 0 0 0 0 2 0 1 2 3 1 ] = ( 2 x + 1 2 y + 2 2 z + 3 1 )

CATransform3D

我们可以在<QuartzCore/CATransform3D.h>中看到CATransform3D的定义:

struct CATransform3D
{
  CGFloat m11, m12, m13, m14;
  CGFloat m21, m22, m23, m24;
  CGFloat m31, m32, m33, m34;
  CGFloat m41, m42, m43, m44;
};

这个结构体对应的是这样一个4x4的变换矩阵:

[ m 11 m 12 m 13 m 14 m 21 m 22 m 23 m 24 m 31 m 32 m 33 m 34 m 41 m 42 m 43 m 44 ] ​" role="presentation" style="text-align: center; position: relative;">⎡⎣⎢⎢⎢m11m21m31m41m12m22m32m42m13m23m33m43m14m24m34m44⎤⎦⎥⎥⎥​ [ m 11 m 12 m 13 m 14 m 21 m 22 m 23 m 24 m 31 m 32 m 33 m 34 m 41 m 42 m 43 m 44 ] ​

坐标向量和变换矩阵的乘法为:

( x y z 1 ) ⋅ [ m 11 m 12 m 13 m 14 m 21 m 22 m 23 m 24 m 31 m 32 m 33 m 34 m 41 m 42 m 43 m 44 ] = ​" role="presentation" style="text-align: center; position: relative;">(xyz1)⋅⎡⎣⎢⎢⎢m11m21m31m41m12m22m32m42m13m23m33m43m14m24m34m44⎤⎦⎥⎥⎥=​ ( x y z 1 ) ⋅ [ m 11 m 12 m 13 m 14 m 21 m 22 m 23 m 24 m 31 m 32 m 33 m 34 m 41 m 42 m 43 m 44 ] = ​

( m 11 x + m 21 y + m 31 z + m 41 m 12 x + m 22 y + m 32 z + m 42 m 13 x + m 23 y + m 33 z + m 43 m 14 x + m 24 y + m 34 z + m 44 ) " role="presentation" style="text-align: center; position: relative;">(m11x+m21y+m31z+m41m12x+m22y+m32z+m42m13x+m23y+m33z+m43m14x+m24y+m34z+m44) ( m 11 x + m 21 y + m 31 z + m 41 m 12 x + m 22 y + m 32 z + m 42 m 13 x + m 23 y + m 33 z + m 43 m 14 x + m 24 y + m 34 z + m 44 )

CGAffineTransform

CGAffineTransform是用在2D中的变换矩阵,我们可以在<CoreGraphic/CGAffineTransform.h>中看到CGAffineTransform的定义:

struct CGAffineTransform {
  CGFloat a, b, c, d;
  CGFloat tx, ty;
};

这个结构体对应的是这样一个3x3的变换矩阵:

[ a b 0 c d 0 t x t y 1 ] ​" role="presentation" style="text-align: center; position: relative;">⎡⎣⎢actxbdty001⎤⎦⎥​ [ a b 0 c d 0 t x t y 1 ] ​

坐标向量和变换矩阵的乘法为:

( x y 1 ) ⋅ [ a b 0 c d 0 t x t y 1 ] = [ x ⋅ a + y ⋅ c + t x x ⋅ b + y ⋅ d + t y 1 ] ​" role="presentation" style="text-align: center; position: relative;">(xy1)⋅⎡⎣⎢actxbdty001⎤⎦⎥=[x⋅a+y⋅c+txx⋅b+y⋅d+ty1]​ ( x y 1 ) ⋅ [ a b 0 c d 0 t x t y 1 ] = [ x ⋅ a + y ⋅ c + t x x ⋅ b + y ⋅ d + t y 1 ] ​

缩放:

( x y 1 ) ⋅ [ s x 0 0 0 s y 0 0 0 1 ] = [ s x ⋅ x s y ⋅ y 1 ] ​" role="presentation" style="text-align: center; position: relative;">(xy1)⋅⎡⎣⎢sx000sy0001⎤⎦⎥=[sx⋅xsy⋅y1]​ ( x y 1 ) ⋅ [ s x 0 0 0 s y 0 0 0 1 ] = [ s x ⋅ x s y ⋅ y 1 ] ​

位移:

( x y 1 ) ⋅ [ 1 0 0 0 1 0 t x t y 1 ] = [ x + t x y + t y 1 ] ​" role="presentation" style="text-align: center; position: relative;">(xy1)⋅⎡⎣⎢10tx01ty001⎤⎦⎥=[x+txy+ty1]​ ( x y 1 ) ⋅ [ 1 0 0 0 1 0 t x t y 1 ] = [ x + t x y + t y 1 ] ​

旋转:

( x y 1 ) ⋅ [ c o s θ s i n θ 0 − s i n θ c o s θ 0 0 0 1 ] = [ c o s θ ⋅ x − s i n θ ⋅ y s i n θ ⋅ x + c o s θ ⋅ y 1 ] " role="presentation" style="text-align: center; position: relative;">(xy1)⋅⎡⎣⎢cosθ−sinθ0sinθcosθ0001⎤⎦⎥=[cosθ⋅x−sinθ⋅ysinθ⋅x+cosθ⋅y1] ( x y 1 ) ⋅ [ c o s θ s i n θ 0 − s i n θ c o s θ 0 0 0 1 ] = [ c o s θ ⋅ x − s i n θ ⋅ y s i n θ ⋅ x + c o s θ ⋅ y 1 ]

透视投影

实际中你如果直接使用旋转,会注意到旋转前后,结果看起来竟然和普通的缩放一模一样,这是为什么呢?原因其实很简单,假如绕y轴旋转,空间中的图层虽然旋转了,但是显示到XoY平面(也就是iPhone的屏幕上)的时候,会把3D的物体进行正投影,这样子看上去就像是左右压缩一样

而学过绘画的都知道人的视野并不是平行的,而是有一个透视图的概念,眼睛前有实际平行的两条线段发出(相当于z轴方向的向量),人眼看起来会相交于一点上(焦点,Focal point),这才产生了3D感。

如何用变换矩阵实现透视投影呢?只需要修改一个值 m 34 " role="presentation" style="position: relative;">m_{34}。为什么单单修改一个 m 34 " role="presentation" style="position: relative;">m_{34}的值,就能达到这种透视3D的效果呢?

Core Animation已经定义了焦点的x,y坐标,就是这个图层的anchorPoint(锚点),同时取z=0的XoY平面作为图像平面(也就是iPhone的屏幕平面),那么假如我希望焦点到图像平面的距离是d,可以假设焦点坐标为(0,0,d),现在对m34的值进行赋值为w,初始向量坐标为 ( x , y , z )" role="presentation" style="position: relative;">(x,y,z),开始推导:

矩阵乘法:

( x y z 1 ) ⋅ [ 1 0 0 0 0 1 0 0 0 0 1 w 0 0 0 1 ] = ( x y z z w + 1 ) ​" role="presentation" style="text-align: center; position: relative;">(xyz1)⋅⎡⎣⎢⎢⎢10000100001000w1⎤⎦⎥⎥⎥=(xyzzw+1)​ ( x y z 1 ) ⋅ [ 1 0 0 0 0 1 0 0 0 0 1 w 0 0 0 1 ] = ( x y z z w + 1 ) ​

此时得到的向量不为齐次,需要进行齐次化,得到真正的坐标:

( x ′ y ′ z ′ 1 ) = ( x z w + 1 y z w + 1 z z w + 1 1 ) ​" role="presentation" style="text-align: center; position: relative;">(x′y′z′1)=(xzw+1yzw+1zzw+11)​ ( x ′ y ′ z ′ 1 ) = ( x z w + 1 y z w + 1 z z w + 1 1 ) ​

最后对XoY平面进行投影,则最终看到的二维向量应该为:

( x z w + 1 y z w + 1 ) " role="presentation" style="text-align: center; position: relative;">(xzw+1yzw+1) ( x z w + 1 y z w + 1 )

现在考虑x轴的情况(y轴同理),我们知道真实三维空间的x坐标是x,现在得到透视投影下的x坐标是 x z w + 1 ​" role="presentation" style="position: relative;">\frac{x}{zw+1}

为了得到d和w的关系,这里引用一幅图,绿色的点为原始点,红色的点为投影到XoY平面上的点,我们这里推导不需要管具体的值,只是为了更清晰地发现规律:

根据相似三角形原理我们可以得到

| x z w + 1 : x | = d : ( | z | + d ) ​" role="presentation" style="text-align: center; position: relative;">|xzw+1:x|=d:(|z|+d)​ | x z w + 1 : x | = d : ( | z | + d ) ​

去绝对值号,且x!=0,z!=0,由图可得此处的z为负数,所以:

1 z w + 1 = d d − z ​" role="presentation" style="text-align: center; position: relative;">1zw+1=dd−z​ 1 z w + 1 = d d − z ​

z w + 1 = 1 − z d " role="presentation" style="text-align: center; position: relative;">zw+1=1−zd z w + 1 = 1 − z d

w = − 1 d " role="presentation" style="text-align: center; position: relative;">w=−1d w = − 1 d

因此 m 34 ​" role="presentation" style="position: relative;">m_{34}的值就为 − 1 d ​" role="presentation" style="position: relative;">- \frac{1}d,这里的 d ​" role="presentation" style="position: relative;">d就是焦点距离,也就是人眼到手机屏幕的距离,一般取值在500~1000之间。默认初始变换矩阵的 m 34 ​" role="presentation" style="position: relative;">m_{34}是0,也就是说认为焦点无限远,因此看起来没有任何3D感。假如我们取d越大,则看起来越没有投射和3D感;取d越小,则3D感和失真感越强烈。

参考文章:

LearnOpenGL CN:变换

Core Animation 3D 仿射变换知识