位移
位移(Translation)是在原始向量的基础上加上另一个向量从而获得一个在不同位置的新向量的过程,从而在位移向量基础上移动了原始向量。
和缩放矩阵一样,在4×4矩阵上有几个特别的位置用来执行特定的操作,对于位移来说它们是第四列最上面的3个值。如果我们把位移向量表示为(Tx,Ty,Tz),我们就能把位移矩阵定义为:
[
1
0
0
T
x
0
1
0
T
y
0
0
1
T
z
0
0
0
1
]
⋅
(
x
y
z
1
)
=
(
x
+
T
x
y
+
T
y
z
+
T
z
1
)
\begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}{T_x} \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}{T_y} \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}{T_z} \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} x + \color{red}{T_x} \\ y + \color{green}{T_y} \\ z + \color{blue}{T_z} \\ 1 \end{pmatrix}
⎣⎢⎢⎡100001000010TxTyTz1⎦⎥⎥⎤⋅⎝⎜⎜⎛xyz1⎠⎟⎟⎞=⎝⎜⎜⎛x+Txy+Tyz+Tz1⎠⎟⎟⎞
这样是能工作的,因为所有的位移值都要乘以向量的w行,所以位移值会加到向量的原始值上(想想矩阵乘法法则)。而如果你用3x3矩阵我们的位移值就没地方放也没地方乘了,所以是不行的。
齐次坐标(Homogeneous Coordinates)
向量的w分量也叫齐次坐标。想要从齐次向量得到3D向量,我们可以把x、y和z坐标分别除以w坐标。我们通常不会注意这个问题,因为w分量通常是1.0。使用齐次坐标有几点好处:它允许我们在3D向量上进行位移(如果没有w分量我们是不能位移向量的),而且下一章我们会用w值创建3D视觉效果。
如果一个向量的齐次坐标是0,这个坐标就是方向向量(Direction Vector),因为w坐标是0,这个向量就不能位移(译注:这也就是我们说的不能位移一个方向)。
旋转
2D空间的旋转可以通过向量表示,比如(x, y)旋转角度A旋转到(x1,y1)。
那么x1 = xcosA - ysinA,y1 = xsinA + ycosA(或者是x1 = xcosA + ysinA,y1 = -xsinA + y * cosA)
也就是说它的二维旋转变换矩阵就是:
c
o
s
A
s
i
n
A
−
s
i
n
A
c
o
s
A
\begin{matrix} cosA & sinA\\ -sinA & cosA \end{matrix}
cosA−sinAsinAcosA
或者是:
c
o
s
A
−
s
i
n
A
s
i
n
A
c
o
s
A
\begin{matrix} cosA & -sinA\\ sinA & cosA \end{matrix}
cosAsinA−sinAcosA
在3D空间中旋转需要定义一个角和一个旋转轴(Rotation Axis)。物体会沿着给定的旋转轴旋转特定角度。
旋转矩阵在3D空间中每个单位轴都有不同定义,旋转角度用θ表示:
沿x轴旋转:
[
1
0
0
0
0
cos
θ
−
sin
θ
0
0
sin
θ
cos
θ
0
0
0
0
1
]
⋅
(
x
y
z
1
)
=
(
x
cos
θ
⋅
y
−
sin
θ
⋅
z
sin
θ
⋅
y
+
cos
θ
⋅
z
1
)
\begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}0 \\ \color{green}0 & \color{green}{\cos \theta} & - \color{green}{\sin \theta} & \color{green}0 \\ \color{blue}0 & \color{blue}{\sin \theta} & \color{blue}{\cos \theta} & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} x \\ \color{green}{\cos \theta} \cdot y - \color{green}{\sin \theta} \cdot z \\ \color{blue}{\sin \theta} \cdot y + \color{blue}{\cos \theta} \cdot z \\ 1 \end{pmatrix}
⎣⎢⎢⎡10000cosθsinθ00−sinθcosθ00001⎦⎥⎥⎤⋅⎝⎜⎜⎛xyz1⎠⎟⎟⎞=⎝⎜⎜⎛xcosθ⋅y−sinθ⋅zsinθ⋅y+cosθ⋅z1⎠⎟⎟⎞
沿y轴旋转:
[
cos
θ
0
sin
θ
0
0
1
0
0
−
sin
θ
0
cos
θ
0
0
0
0
1
]
⋅
(
x
y
z
1
)
=
(
cos
θ
⋅
x
+
sin
θ
⋅
z
y
−
sin
θ
⋅
x
+
cos
θ
⋅
z
1
)
\begin{bmatrix} \color{red}{\cos \theta} & \color{red}0 & \color{red}{\sin \theta} & \color{red}0 \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}0 \\ - \color{blue}{\sin \theta} & \color{blue}0 & \color{blue}{\cos \theta} & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} \color{red}{\cos \theta} \cdot x + \color{red}{\sin \theta} \cdot z \\ y \\ - \color{blue}{\sin \theta} \cdot x + \color{blue}{\cos \theta} \cdot z \\ 1 \end{pmatrix}
⎣⎢⎢⎡cosθ0−sinθ00100sinθ0cosθ00001⎦⎥⎥⎤⋅⎝⎜⎜⎛xyz1⎠⎟⎟⎞=⎝⎜⎜⎛cosθ⋅x+sinθ⋅zy−sinθ⋅x+cosθ⋅z1⎠⎟⎟⎞
沿z轴旋转:
[
cos
θ
−
sin
θ
0
0
sin
θ
cos
θ
0
0
0
0
1
0
0
0
0
1
]
⋅
(
x
y
z
1
)
=
(
cos
θ
⋅
x
−
sin
θ
⋅
y
sin
θ
⋅
x
+
cos
θ
⋅
y
z
1
)
\begin{bmatrix} \color{red}{\cos \theta} & - \color{red}{\sin \theta} & \color{red}0 & \color{red}0 \\ \color{green}{\sin \theta} & \color{green}{\cos \theta} & \color{green}0 & \color{green}0 \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} \color{red}{\cos \theta} \cdot x - \color{red}{\sin \theta} \cdot y \\ \color{green}{\sin \theta} \cdot x + \color{green}{\cos \theta} \cdot y \\ z \\ 1 \end{pmatrix}
⎣⎢⎢⎡cosθsinθ00−sinθcosθ0000100001⎦⎥⎥⎤⋅⎝⎜⎜⎛xyz1⎠⎟⎟⎞=⎝⎜⎜⎛cosθ⋅x−sinθ⋅ysinθ⋅x+cosθ⋅yz1⎠⎟⎟⎞
沿着任意轴(Rx, Ry, Rz)旋转:
[
cos
θ
+
R
x
2
(
1
−
cos
θ
)
R
x
R
y
(
1
−
cos
θ
)
−
R
z
sin
θ
R
x
R
z
(
1
−
cos
θ
)
+
R
y
sin
θ
0
R
y
R
x
(
1
−
cos
θ
)
+
R
z
sin
θ
cos
θ
+
R
y
2
(
1
−
cos
θ
)
R
y
R
z
(
1
−
cos
θ
)
−
R
x
sin
θ
0
R
z
R
x
(
1
−
cos
θ
)
−
R
y
sin
θ
R
z
R
y
(
1
−
cos
θ
)
+
R
x
sin
θ
cos
θ
+
R
z
2
(
1
−
cos
θ
)
0
0
0
0
1
]
\begin{bmatrix} \cos \theta + \color{red}{R_x}^2(1 - \cos \theta) & \color{red}{R_x}\color{green}{R_y}(1 - \cos \theta) - \color{blue}{R_z} \sin \theta & \color{red}{R_x}\color{blue}{R_z}(1 - \cos \theta) + \color{green}{R_y} \sin \theta & 0 \\ \color{green}{R_y}\color{red}{R_x} (1 - \cos \theta) + \color{blue}{R_z} \sin \theta & \cos \theta + \color{green}{R_y}^2(1 - \cos \theta) & \color{green}{R_y}\color{blue}{R_z}(1 - \cos \theta) - \color{red}{R_x} \sin \theta & 0 \\ \color{blue}{R_z}\color{red}{R_x}(1 - \cos \theta) - \color{green}{R_y} \sin \theta & \color{blue}{R_z}\color{green}{R_y}(1 - \cos \theta) + \color{red}{R_x} \sin \theta & \cos \theta + \color{blue}{R_z}^2(1 - \cos \theta) & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}
⎣⎢⎢⎡cosθ+Rx2(1−cosθ)RyRx(1−cosθ)+RzsinθRzRx(1−cosθ)−Rysinθ0RxRy(1−cosθ)−Rzsinθcosθ+Ry2(1−cosθ)RzRy(1−cosθ)+Rxsinθ0RxRz(1−cosθ)+RysinθRyRz(1−cosθ)−Rxsinθcosθ+Rz2(1−cosθ)00001⎦⎥⎥⎤
矩阵的组合
使用矩阵进行变换的真正力量在于,根据矩阵之间的乘法,我们可以把多个变换组合到一个矩阵中。让我们看看我们是否能生成一个变换矩阵,让它组合多个变换。假设我们有一个顶点(x, y, z),我们希望将其缩放2倍,然后位移(1, 2, 3)个单位。我们需要一个位移和缩放矩阵来完成这些变换。结果的变换矩阵看起来像这样:
T
r
a
n
s
.
S
c
a
l
e
=
[
1
0
0
1
0
1
0
2
0
0
1
3
0
0
0
1
]
.
[
2
0
0
0
0
2
0
0
0
0
2
0
0
0
0
1
]
=
[
2
0
0
1
0
2
0
2
0
0
2
3
0
0
0
1
]
Trans . Scale = \begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}1 \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}2 \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}3 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} . \begin{bmatrix} \color{red}2 & \color{red}0 & \color{red}0 & \color{red}0 \\ \color{green}0 & \color{green}2 & \color{green}0 & \color{green}0 \\ \color{blue}0 & \color{blue}0 & \color{blue}2 & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} = \begin{bmatrix} \color{red}2 & \color{red}0 & \color{red}0 & \color{red}1 \\ \color{green}0 & \color{green}2 & \color{green}0 & \color{green}2 \\ \color{blue}0 & \color{blue}0 & \color{blue}2 & \color{blue}3 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix}
Trans.Scale=⎣⎢⎢⎡1000010000101231⎦⎥⎥⎤.⎣⎢⎢⎡2000020000200001⎦⎥⎥⎤=⎣⎢⎢⎡2000020000201231⎦⎥⎥⎤
注意,当矩阵相乘时我们先写位移再写缩放变换的。矩阵乘法是不遵守交换律的,这意味着它们的顺序很重要。当矩阵相乘时,在最右边的矩阵是第一个与向量相乘的,所以你应该从右向左读这个乘法。建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会(消极地)互相影响。比如,如果你先位移再缩放,位移的向量也会同样被缩放(译注:比如向某方向移动2米,2米也许会被缩放成1米)!
用最终的变换矩阵左乘我们的向量会得到以下结果:
[ 2 0 0 1 0 2 0 2 0 0 2 3 0 0 0 1 ] . [ x y z 1 ] = [ 2 x + 1 2 y + 2 2 z + 3 1 ] \begin{bmatrix} \color{red}2 & \color{red}0 & \color{red}0 & \color{red}1 \\ \color{green}0 & \color{green}2 & \color{green}0 & \color{green}2 \\ \color{blue}0 & \color{blue}0 & \color{blue}2 & \color{blue}3 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} . \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} \color{red}2x + \color{red}1 \\ \color{green}2y + \color{green}2 \\ \color{blue}2z + \color{blue}3 \\ 1 \end{bmatrix} ⎣⎢⎢⎡2000020000201231⎦⎥⎥⎤.⎣⎢⎢⎡xyz1⎦⎥⎥⎤=⎣⎢⎢⎡2x+12y+22z+31⎦⎥⎥⎤
实践
openGL配合GLM库使用,这是一个专门为openGL定制的数学库。
GLM库从0.9.9版本起,默认会将矩阵类型初始化为一个零矩阵(所有元素均为0),而不是单位矩阵(对角元素为1,其它元素为0)。如果你使用的是0.9.9或0.9.9以上的版本,你需要将所有的矩阵初始化改为
glm::mat4 mat = glm::mat4(1.0f)
。
头文件
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
例子:
- 位移
例如将一个向量(1, 0, 0)位移(1, 1, 0)个单位
glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
glm::mat4 trans = glm::mat4(1.0f);
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));
vec = trans * vec;
std::cout << vec.x << vec.y << vec.z << std::endl;
- 旋转+缩放
例如将物体先在每个轴都缩放到0.5倍,然后沿z轴旋转90度。
glm::mat4 trans = glm::mat4(1.0f);
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
将矩阵传递给着色器:
GLSL里也有一个mat4类型,修改顶点着色器让其接收一个mat4的uniform变量,然后再用矩阵uniform乘以位置向量:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 transform;
void main()
{
gl_Position = transform * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}
在把位置向量传给gl_Position之前,先添加一个uniform,并且将其与变换矩阵相乘。物体现在应该是原来的二分之一大小并(向左)旋转了90度。当然,我们仍需要把变换矩阵传递给着色器:
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
我们首先查询uniform变量的地址,然后用有Matrix4fv后缀的glUniform函数把矩阵数据发送给着色器。第一个参数你现在应该很熟悉了,它是uniform的位置值。第二个参数告诉OpenGL我们将要发送多少个矩阵,这里是1。第三个参数询问我们我们是否希望对我们的矩阵进行置换(Transpose),也就是说交换我们矩阵的行和列。OpenGL开发者通常使用一种内部矩阵布局,叫做列主序(Column-major Ordering)布局。GLM的默认布局就是列主序,所以并不需要置换矩阵,我们填GL_FALSE。最后一个参数是真正的矩阵数据,但是GLM并不是把它们的矩阵储存为OpenGL所希望接受的那种,因此我们要先用GLM的自带的函数value_ptr来变换这些数据。