文章目录
LearnGL - 学习笔记目录
本人才疏学浅,如有什么错误,望不吝指出。
上一篇:LearnGL - 05.3 - 封装 Main.cpp 中重复 GLFW代码,让新建的项目更方便的开展。
这一篇:这篇我们在讲一丢丢关于矩阵的内容。
本来我不想写关于矩阵的内容。因为要排版很多内容(LaTex挺多),-_-!!!,但是想了一下,还是加上吧,因为这个是我自己以前看过的资料总结的,先暂时写这么多,后续有空再补上关于矩阵的其他内容。
这里推荐一下关于矩阵的完整系统教学,我也是参考了其他网站上关于 OpenGL 学习资料中的:变换,这篇文章里介绍的:
可汗学院
- 可汗学院-线性代数-矩阵-在线视频、习题、小考 - (什么时候国内的在线免费教育能做到这样,教育方面就进步了)
- 矩阵变换
最近需要系统学习一下这方面资料,发现他这在线免费的教程资料太太太好了,忍不住要分享一下。
相比我之前在:网易公开课,里看的熟肉的质量好多了,因为我上面推荐的可汗学院的网站是官方网,虽然的内容是英文版,但是还好的是陌生单词不会很多。
可汗的故事以前看过,反正后来是比尔盖茨投资他继续为科学教育助力,因为比尔盖茨也是他的粉丝,而我,是比尔盖茨的粉丝。
看看:“A world class education for anyone, anywhere 100% free。”,这就是初心,感动人的工匠,这是造福人类啊。
OK,那么开始正文:
变换 这篇文章里排版什么的我是非常喜欢,看起来很舒服。
但里面刚好有一丢丢我会的内容没提到,我也在此基础上添加一丢丢笔记吧。
注意,什么是 向量、标量向量相乘、向量加减法、向量点积、向量乘积、矩阵乘法 我都不再说明,因为 变换 说得足够的清晰了。
点在坐标系下的定位
我们先了解 点在坐标系下的定位
先看看坐标轴长什么样:
这些内容大家肯定了解,毕竟都是初中知识。
O是坐标系原点,X就是X轴,其他两个就是Y轴和Z轴。
它们看起来就像是 三个向量。
没错,就是三个向量,OK,那么我们将它们每个轴的向量都标记上向量分量:
我们先给轴都定义一个标识符(字母上面加一个指向右的箭头)吧:
X
→
\overrightarrow X
X 表 X轴,以此类推
Y
→
、
Z
→
\overrightarrow Y、\overrightarrow Z
Y、Z。
然后原点是:
O
O
O
X
→
=
(
1
,
0
,
0
)
\overrightarrow X=(1,0,0)
X=(1,0,0)
Y
→
=
(
0
,
1
,
0
)
\overrightarrow Y=(0,1,0)
Y=(0,1,0)
Z
→
=
(
0
,
0
,
1
)
\overrightarrow Z=(0,0,1)
Z=(0,0,1)
从坐标轴长度可以看出来,都是 1的长度(单位化向量长度都是1)。
所以三个向量都是 单位向量(基向量)。
而且三个向量都是相互 垂直的(正交的)。
所以我们也可以叫:正交基向量,也叫 自然基、标准化(归一化)基向量,叫法挺多,选一个方便我们理解的就好,暂时选用 正交基向量。
如果我有任意一个点像在此坐标系下显示位置,我们如果处理呢?
假设我有一个点P,它就在原点O(0,0,0)的位置上,它和原点重合了,我们想让它右边(X轴正方向)移动0.5的位移量,好让我们看清这个点:
那么下面P点的位置变成了: O + 0.5 X → O+0.5 \overrightarrow X O+0.5X
就是在:原点 O O O基础上往 X → \overrightarrow X X偏移0.5的量
O
=
(
0
,
0
,
0
)
,
X
→
=
(
1
,
0
,
0
)
O=(0,0,0), \overrightarrow X=(1,0,0)
O=(0,0,0),X=(1,0,0)
P
点
=
O
+
0.5
X
→
P点=O+0.5\overrightarrow X
P点=O+0.5X
=
(
0
,
0
,
0
)
+
0.5
⋅
(
1
,
0
,
0
)
=(0,0,0)+0.5\cdot(1,0,0)
=(0,0,0)+0.5⋅(1,0,0)
=
(
0.5
,
0
,
0
)
=(0.5,0,0)
=(0.5,0,0)
然后再向上(Y轴正方向)移动0.2个位移量:
O
+
0.5
X
→
+
0.2
Y
→
O+0.5\overrightarrow X+0.2\overrightarrow Y
O+0.5X+0.2Y
就是在:原点 O O O基础上往 X → \overrightarrow X X偏移0.5的量,再往 Y → \overrightarrow Y Y偏移0.2的量
O
=
(
0
,
0
,
0
)
,
X
→
=
(
1
,
0
,
0
)
,
Y
→
=
(
0
,
1
,
0
)
O=(0,0,0), \overrightarrow X=(1,0,0),\overrightarrow Y=(0,1,0)
O=(0,0,0),X=(1,0,0),Y=(0,1,0)
P
点
=
O
+
0.5
X
→
+
0.2
Y
→
P点=O+0.5\overrightarrow X+0.2\overrightarrow Y
P点=O+0.5X+0.2Y
=
(
0
,
0
,
0
)
+
0.5
⋅
(
1
,
0
,
0
)
+
0.2
⋅
(
0
,
1
,
0
)
=(0,0,0)+0.5\cdot(1,0,0)+0.2\cdot(0,1,0)
=(0,0,0)+0.5⋅(1,0,0)+0.2⋅(0,1,0)
=
(
0.5
,
0
,
0
)
+
(
0
,
0.2
,
0
)
=(0.5,0,0)+(0,0.2,0)
=(0.5,0,0)+(0,0.2,0)
=
(
0.5
,
0.2
,
0
)
=(0.5,0.2,0)
=(0.5,0.2,0)
然后再向前(Z轴正方向)移动0.5个位移量:
O
+
0.5
X
→
+
0.2
Y
→
+
0.5
Z
→
O+0.5\overrightarrow X+0.2\overrightarrow Y+0.5\overrightarrow Z
O+0.5X+0.2Y+0.5Z
就是在:原点 O O O基础上往 X → \overrightarrow X X偏移0.5的量,再往 Y → \overrightarrow Y Y偏移0.2的量,再往 Z → \overrightarrow Z Z偏移0.5的量
O
=
(
0
,
0
,
0
)
,
X
→
=
(
1
,
0
,
0
)
,
Y
→
=
(
0
,
1
,
0
)
,
Z
→
=
(
0
,
0
,
1
)
O=(0,0,0), \overrightarrow X=(1,0,0),\overrightarrow Y=(0,1,0),\overrightarrow Z=(0,0,1)
O=(0,0,0),X=(1,0,0),Y=(0,1,0),Z=(0,0,1)
P
点
=
O
+
0.5
X
→
+
0.2
Y
→
+
0.5
Z
→
P点=O+0.5\overrightarrow X+0.2\overrightarrow Y+0.5\overrightarrow Z
P点=O+0.5X+0.2Y+0.5Z
=
(
0
,
0
,
0
)
+
0.5
⋅
(
1
,
0
,
0
)
+
0.2
⋅
(
0
,
1
,
0
)
+
0.5
⋅
(
0
,
0
,
1
)
=(0,0,0)+0.5\cdot(1,0,0)+0.2\cdot(0,1,0)+0.5\cdot(0,0,1)
=(0,0,0)+0.5⋅(1,0,0)+0.2⋅(0,1,0)+0.5⋅(0,0,1)
=
(
0.5
,
0
,
0
)
+
(
0
,
0.2
,
0
)
+
(
0
,
0
,
0.5
)
=(0.5,0,0)+(0,0.2,0)+(0,0,0.5)
=(0.5,0,0)+(0,0.2,0)+(0,0,0.5)
=
(
0.5
,
0.2
,
0.5
)
=(0.5,0.2,0.5)
=(0.5,0.2,0.5)
OK,这就是点 P ( 0.5 , 0.2 , 0.5 ) P(0.5,0.2,0.5) P(0.5,0.2,0.5)在此坐标系下的位置。
更重要的是,我们得出了一个式子: O + 0.5 X → + 0.2 Y → + 0.5 Z → O+0.5\overrightarrow X+0.2\overrightarrow Y+0.5\overrightarrow Z O+0.5X+0.2Y+0.5Z
再把: P ( 0.5 , 0.2 , 0.5 ) P(0.5,0.2,0.5) P(0.5,0.2,0.5),换成: P ( p x , p y , p z ) P(p_x,p_y,p_z) P(px,py,pz)
那么式子: O + 0.5 X → + 0.2 Y → + 0.5 Z → O+0.5\overrightarrow X+0.2\overrightarrow Y+0.5\overrightarrow Z O+0.5X+0.2Y+0.5Z
变成了: O + p x ⋅ X → + p y ⋅ Y → + p z ⋅ Z → O+p_x\cdot \overrightarrow X+p_y\cdot \overrightarrow Y+p_z\cdot \overrightarrow Z O+px⋅X+py⋅Y+pz⋅Z
其实这式子就是3D图形学中4x4矩阵的前身,因为这个式子可以表示一次变换。
注意:而使用矩阵的原因是因为矩阵数据集合本身可以修改。假设我们有一个矩阵变换(坐标系)A,只要我们用另一个矩阵变换 B 去修改它,那么它就相当于在原来的矩阵的变换(或是说坐标系)基础上,再次改变了坐标系的轴(旋转或缩放),变成了另一个新的坐标系:C, C = B ⋅ A C = B\cdot A C=B⋅A,这个 C 矩阵就相当于柔和了 A 和 B 矩阵变换的累积变换。
我就举个机械手臂的例子:
将他们的每个子矩阵变换,或者说是子坐标系,罗列一下可以有这么几个
有:
- A A A 它是我们的肱二头肌
- B B B 是手腕
- C C C 是类似人类手掌
- D 1 , D 2 D_1,D_2 D1,D2 是手指 D D D关节1、2
- E 1 , E 2 E_1,E_2 E1,E2 是手指 E E E关节1、2
- F 1 , F 2 F_1,F_2 F1,F2 是手指 F F F关节1、2
- G G G 是我们的红球
其中 G 会比较特殊
从 A A A到 G G G都的坐标系之前的嵌套可以理解为:
A
|
+--->B
|
+--->C
|
+--->D_1
| |
| +--->D2
+--->E_1
| |
| +--->E_2
+--->F_1
|
+--->F_2
G 呢?它没有属于机械手臂的坐标系吗?因为我们的机械手臂的D,E,F关节都是可以旋转而夹紧红球 G,并且 D,E,F 不再变换后,D,E,F,G都数据 C 的子坐标系。
只要 G 求没有东西固定它,它都属于世界坐标。
当然,我们上面通常是简单的游戏中的制作方法,更好的方法是直接使用物理系统来给球体、机械手臂添加物理刚体网格,让他们使用物理系统来驱动相应。
每一个关节都有一个坐标系,或是说:矩阵变换。
如果我们驱动 A 矩阵变换,那么其他所有的 子坐标 或是说 子矩阵变换 都一同跟着变换,如果我们驱动 B,那么 A 保持不动,B 下的所有 子矩阵变换 又会跟着变化。(有些系统是例外,如:实现IK动力学,是需要 子矩阵变换 来计算驱动 父级矩阵 如何变化的)
这些每个坐标系相对世界坐标系统( I , 单 位 矩 阵 , 对 角 都 是 1 , 其 余 都 为 0 I,单位矩阵,对角都是1,其余都为0 I,单位矩阵,对角都是1,其余都为0)来说,他们都可以这么表示:
- I ⋅ A I\cdot A I⋅A
- I ⋅ A ⋅ B I\cdot A \cdot B I⋅A⋅B
- I ⋅ A ⋅ B ⋅ C I\cdot A \cdot B \cdot C I⋅A⋅B⋅C
- I ⋅ A ⋅ B ⋅ C ⋅ D 1 I\cdot A \cdot B \cdot C \cdot D_1 I⋅A⋅B⋅C⋅D1
- I ⋅ A ⋅ B ⋅ C ⋅ D 1 ⋅ D 2 I\cdot A \cdot B \cdot C \cdot D_1 \cdot D_2 I⋅A⋅B⋅C⋅D1⋅D2
- I ⋅ A ⋅ B ⋅ C ⋅ E 1 I\cdot A \cdot B \cdot C \cdot E_1 I⋅A⋅B⋅C⋅E1
- I ⋅ A ⋅ B ⋅ C ⋅ E 1 ⋅ E 2 I\cdot A \cdot B \cdot C \cdot E_1 \cdot E_2 I⋅A⋅B⋅C⋅E1⋅E2
- I ⋅ A ⋅ B ⋅ C ⋅ F 1 I\cdot A \cdot B \cdot C \cdot F_1 I⋅A⋅B⋅C⋅F1
- I ⋅ A ⋅ B ⋅ C ⋅ F 1 ⋅ F 2 I\cdot A \cdot B \cdot C \cdot F_1 \cdot F_2 I⋅A⋅B⋅C⋅F1⋅F2
继续上面的 坐标系下点的定位的 内容,我们可以将: O + p x ⋅ X → + p y ⋅ Y → + p z ⋅ Z → O+p_x\cdot \overrightarrow X+p_y\cdot \overrightarrow Y+p_z\cdot \overrightarrow Z O+px⋅X+py⋅Y+pz⋅Z写成矩阵的方式:
首先是每个轴,我们都写成3x3中某个列的向量来表示:
X
→
=
[
∣
X
→
∣
]
,
Y
→
=
[
∣
Y
→
∣
]
,
Z
→
=
[
∣
Z
→
∣
]
\overrightarrow X=\begin{bmatrix}|\\\overrightarrow X\\|\end{bmatrix},\overrightarrow Y=\begin{bmatrix}|\\\overrightarrow Y\\|\end{bmatrix},\overrightarrow Z=\begin{bmatrix}|\\\overrightarrow Z\\|\end{bmatrix}
X=⎣⎡∣X∣⎦⎤,Y=⎣⎡∣Y∣⎦⎤,Z=⎣⎡∣Z∣⎦⎤
这样我们的三个 正交基向量 就构成了一个3x3矩阵:
M
M
M
M
=
[
∣
∣
∣
X
→
Y
→
Z
→
∣
∣
∣
]
M=\begin{bmatrix} | & | & |\\ \overrightarrow X & \overrightarrow Y & \overrightarrow Z\\ | & | & | \end{bmatrix}
M=⎣⎡∣X∣∣Y∣∣Z∣⎦⎤
然后是我们的点
P
P
P可以是表示为一个3x1的向量(矩阵):
(
p
x
p
y
p
z
)
\left(\begin{matrix} p_x\\ p_y\\ p_z\end{matrix}\right)
⎝⎛pxpypz⎠⎞
然后是点
P
P
P在
M
M
M坐标系(变换矩阵)中定位后(变换后)的坐标定义为:
P
1
P1
P1,那么
P
1
P1
P1等于:
(
p
1
x
p
1
y
p
1
z
)
\left(\begin{matrix} p1_x\\ p1_y\\ p1_z\end{matrix}\right)
⎝⎛p1xp1yp1z⎠⎞
P
1
=
M
⋅
P
→
(
p
1
x
p
1
y
p
1
z
)
=
[
∣
∣
∣
X
→
Y
→
Z
→
∣
∣
∣
]
⋅
(
p
x
p
y
p
z
)
→
p
x
⋅
X
→
+
p
y
⋅
Y
→
+
p
z
⋅
Z
→
P1=M\cdot P \rightarrow \left(\begin{matrix} p1_x\\ p1_y\\ p1_z\end{matrix}\right) =\begin{bmatrix} | & | & |\\ \overrightarrow X & \overrightarrow Y & \overrightarrow Z\\ | & | & | \end{bmatrix} \cdot \left(\begin{matrix} p_x\\ p_y\\ p_z\end{matrix}\right) \rightarrow p_x\cdot \overrightarrow X+p_y\cdot \overrightarrow Y+p_z\cdot \overrightarrow Z
P1=M⋅P→⎝⎛p1xp1yp1z⎠⎞=⎣⎡∣X∣∣Y∣∣Z∣⎦⎤⋅⎝⎛pxpypz⎠⎞→px⋅X+py⋅Y+pz⋅Z
等等,我们是否少了个 O O O 原点了?
先理解这个 O O O,它是原点,我们都可以理解为相对上一个坐标系的原点偏移位置。
因为它为(0,0,0),我暂时没写,那么,如果 O O O不为0呢?那么我们还是把它加上吧。
3x3扩展4x4添加偏移量
按照3x3矩阵乘法(之前说过我这里不再说明矩阵乘法了,因为那篇文章说的足够清晰),我们的偏移量在3x3表示不了了。
所以我们给3x3扩展维度到4x4
M
=
[
∣
∣
∣
O
x
X
→
Y
→
Z
→
O
y
∣
∣
∣
O
z
0
0
0
1
]
M=\begin{bmatrix} | & | & | & O_x\\ \overrightarrow X & \overrightarrow Y & \overrightarrow Z & O_y\\ | & | & | & O_z\\ 0 & 0 & 0 & 1 \end{bmatrix}
M=⎣⎢⎢⎡∣X∣0∣Y∣0∣Z∣0OxOyOz1⎦⎥⎥⎤
然后将我们3x1的
P
P
P也需要扩展到4x1:
(
p
x
p
y
p
z
1
)
\left(\begin{matrix} p_x\\ p_y\\ p_z\\ 1\end{matrix}\right)
⎝⎜⎜⎛pxpypz1⎠⎟⎟⎞
同样
P
1
P1
P1也扩展到4x1:
(
p
1
x
p
1
y
p
1
z
1
)
\left(\begin{matrix} p1_x\\ p1_y\\ p1_z\\ 1\end{matrix}\right)
⎝⎜⎜⎛p1xp1yp1z1⎠⎟⎟⎞
齐次坐标
P 、 P 1 P、P1 P、P1我们多在行维度多添加了一个维度(x,y,z,w),并且最后一个分量w设置为1。
这个新的坐标可称为:齐次坐标。
齐次坐标有下面的特点:
(
1
,
2
,
3
,
1
)
(
2
,
4
,
6
,
2
)
(
3
,
6
,
9
,
3
)
(1,2,3,1)\\ (2,4,6,2)\\ (3,6,9,3)
(1,2,3,1)(2,4,6,2)(3,6,9,3)
上面三个点都表示为3D中的同一个点。
引用 songho 博主 齐次坐标 与 2D笛卡尔坐标 关系 的一张图:
齐次坐标有两个作用:
- 便于位移信息加入4x4矩阵实现带位移的线性变换:为了将位移量添加到4x4矩阵的第4列,以配合齐次坐标中的最后一个分量1相乘即可累加该轴向的偏移量。
- 在3D渲染中常用的方式,一般会将当前对象的世界坐标/视图坐标的Z值,经过投影矩阵的 M [ 4 ] [ 3 ] M[4][3] M[4][3]的1或-1的矩阵变换后,来传入到齐次坐标的w分量中,以便 OpenGL 底层处理齐次坐标透视除法时用。这样Z值越大,投影平面坐标X,Y分量将收缩得越小,以此来实现透视效果。
齐次坐标的透视除法: ( w x , w y , w z , w ) = ( w x / w , w y / w , w z / w , w / w ) = ( x , y , z , 1 ) (wx,wy,wz,w)=(wx/w,wy/w,wz/w,w/w)=(x,y,z,1) (wx,wy,wz,w)=(wx/w,wy/w,wz/w,w/w)=(x,y,z,1)
那么
w
w
w越大,或说是 投影变换前 的 视图空间/坐标系 下的坐标的
z
z
z越大(它就是我们齐次坐标中的
w
w
w,也是 与相机之间的z分量的距离值),那么该齐次坐标的 透视除法 后变为投影近平面上的坐标就越小,透视除法后 的这个坐标是 NDC坐标,NDC坐标的原点是正中间的,所以那些 视图空间/坐标系 下的
z
z
z越大的坐标到了 NDC坐标后就越小,就靠近屏幕中间。想象一下我们站在长长的火车铁轨上(务必保证没有火车),望着远处的铁轨尽头,两排铁轨本是平行的,但怎么越是远处看起来就相交了呢?这就是透视现象(这是由于我们的人类的肉眼结构决定的,但是不同物种的透视是不同的)。想象一下,如果齐次坐标的第四个分量如果为 0 的话,会怎么样呢?透视除法后,所有值都无穷大,意思就是无穷远的一个点,这个点几乎是看不到的,因为太远,所以我们通常会看到,3D坐标中,如果第四个分量为1,都会当作是一个坐标值处理(点),如果第四个分量为0,都会当作是一个方向处理(向量)。
(Win Paint.exe上鼠标简单作画,以前的老画板找不到了,-_-!!!)
根据齐次坐标特性,所以我们的式子重新调整为:矩阵由3x3调整为4x4,并将坐标原点偏移量放置到第四列的前三个分量;与矩阵相乘的3x1向量调整为4x1,第四个分量填充1:
P
1
=
M
⋅
P
→
(
p
1
x
p
1
y
p
1
z
1
)
=
[
∣
∣
∣
O
x
X
→
Y
→
Z
→
O
y
∣
∣
∣
O
z
0
0
0
1
]
⋅
(
p
x
p
y
p
z
1
)
→
p
x
⋅
X
→
+
p
y
⋅
Y
→
+
p
z
⋅
Z
→
+
O
P1=M\cdot P \rightarrow \left(\begin{matrix} p1_x\\ p1_y\\ p1_z\\ \color{#ff0000}1\end{matrix}\right) =\begin{bmatrix} | & | & | & O_x\\ \overrightarrow X & \overrightarrow Y & \overrightarrow Z & O_y\\ | & | & | & O_z\\ \color{#ff0000}0 & \color{#ff0000}0 & \color{#ff0000}0 & \color{#ff0000}1 \end{bmatrix} \cdot \left(\begin{matrix} p_x\\ p_y\\ p_z\\ \color{#ff0000}1\end{matrix}\right) \rightarrow p_x\cdot \overrightarrow X+p_y\cdot \overrightarrow Y+p_z\cdot \overrightarrow Z + O
P1=M⋅P→⎝⎜⎜⎛p1xp1yp1z1⎠⎟⎟⎞=⎣⎢⎢⎡∣X∣0∣Y∣0∣Z∣0OxOyOz1⎦⎥⎥⎤⋅⎝⎜⎜⎛pxpypz1⎠⎟⎟⎞→px⋅X+py⋅Y+pz⋅Z+O
我们将尾部的式子调整为 标量 与 向量 的 乘法(这里不再说明,可以看那个网站上的说明)
尾部的式子:
p
x
⋅
X
→
+
p
y
⋅
Y
→
+
p
z
⋅
Z
→
+
O
将各向量
X
→
、
Y
→
、
Z
→
展
开
分
量
:
=
p
x
⋅
(
X
→
x
X
→
y
X
→
z
0
)
+
p
y
⋅
(
Y
→
x
Y
→
y
Y
→
z
0
)
+
p
z
⋅
(
Z
→
x
Z
→
y
Z
→
z
0
)
+
1
⋅
(
O
x
O
y
O
z
1
)
将点
P
的分量(标量)乘进
X
→
、
Y
→
、
Z
→
、
O
→
(这里的O把它当做向量来运算,这样就可以把O的分量的分别偏移原来点) 向量:
=
(
p
x
⋅
X
→
x
p
x
⋅
X
→
y
p
x
⋅
X
→
z
p
x
⋅
0
)
+
(
p
y
⋅
Y
→
x
p
y
⋅
Y
→
y
p
y
⋅
Y
→
z
p
y
⋅
0
)
+
(
p
z
⋅
Z
→
x
p
z
⋅
Z
→
y
p
z
⋅
Z
→
z
p
z
⋅
0
)
+
(
1
⋅
O
x
1
⋅
O
y
1
⋅
O
z
1
⋅
1
)
\text{尾部的式子:} p_x\cdot \overrightarrow X+p_y\cdot \overrightarrow Y+p_z\cdot \overrightarrow Z + O \\ \text{将各向量$\overrightarrow X、\overrightarrow Y、\overrightarrow Z 展开分量$:}=p_x \cdot \left(\begin{matrix} \color{#ff0000}\overrightarrow X_x\\ \color{#ff0000}\overrightarrow X_y\\ \color{#ff0000}\overrightarrow X_z\\ 0 \end{matrix}\right) + p_y \cdot \left(\begin{matrix} \color{#ff0000}\overrightarrow Y_x\\ \color{#ff0000}\overrightarrow Y_y\\ \color{#ff0000}\overrightarrow Y_z\\ 0 \end{matrix}\right) + p_z \cdot \left(\begin{matrix} \color{#ff0000}\overrightarrow Z_x\\ \color{#ff0000}\overrightarrow Z_y\\ \color{#ff0000}\overrightarrow Z_z\\ 0 \end{matrix}\right) + 1 \cdot \left(\begin{matrix} \color{#ff0000}O_x\\ \color{#ff0000}O_y\\ \color{#ff0000}O_z\\ 1 \end{matrix}\right) \\ \text{将点$P$的分量(标量)乘进$\overrightarrow X、\overrightarrow Y、\overrightarrow Z、\overrightarrow O$(这里的O把它当做向量来运算,这样就可以把O的分量的分别偏移原来点) 向量:} \\=\left(\begin{matrix} p_x\cdot\overrightarrow X_x\\ p_x\cdot\overrightarrow X_y\\ p_x\cdot\overrightarrow X_z\\ p_x\cdot0 \end{matrix}\right) + \left(\begin{matrix} p_y\cdot\overrightarrow Y_x\\ p_y\cdot\overrightarrow Y_y\\ p_y\cdot\overrightarrow Y_z\\ p_y\cdot 0 \end{matrix}\right) + \left(\begin{matrix} p_z\cdot\overrightarrow Z_x\\ p_z\cdot\overrightarrow Z_y\\ p_z\cdot\overrightarrow Z_z\\ p_z\cdot0 \end{matrix}\right) + \left(\begin{matrix} 1\cdot O_x\\ 1\cdot O_y\\ 1\cdot O_z\\ 1\cdot 1 \end{matrix}\right)
尾部的式子:px⋅X+py⋅Y+pz⋅Z+O将各向量X、Y、Z展开分量:=px⋅⎝⎜⎜⎜⎛XxXyXz0⎠⎟⎟⎟⎞+py⋅⎝⎜⎜⎜⎛YxYyYz0⎠⎟⎟⎟⎞+pz⋅⎝⎜⎜⎜⎛ZxZyZz0⎠⎟⎟⎟⎞+1⋅⎝⎜⎜⎛OxOyOz1⎠⎟⎟⎞将点P的分量(标量)乘进X、Y、Z、O(这里的O把它当做向量来运算,这样就可以把O的分量的分别偏移原来点) 向量:=⎝⎜⎜⎜⎛px⋅Xxpx⋅Xypx⋅Xzpx⋅0⎠⎟⎟⎟⎞+⎝⎜⎜⎜⎛py⋅Yxpy⋅Yypy⋅Yzpy⋅0⎠⎟⎟⎟⎞+⎝⎜⎜⎜⎛pz⋅Zxpz⋅Zypz⋅Zzpz⋅0⎠⎟⎟⎟⎞+⎝⎜⎜⎛1⋅Ox1⋅Oy1⋅Oz1⋅1⎠⎟⎟⎞
然后根据 向量加法 (这里不再说明,之前说的那篇文章有说明,也足够清晰)
=
(
p
x
⋅
X
→
x
+
p
y
⋅
Y
→
x
+
p
z
⋅
Z
→
x
+
1
⋅
O
x
p
x
⋅
X
→
y
+
p
y
⋅
Y
→
y
+
p
z
⋅
Z
→
y
+
1
⋅
O
y
p
x
⋅
X
→
z
+
p
y
⋅
Y
→
z
+
p
z
⋅
Z
→
z
+
1
⋅
O
z
p
x
⋅
0
+
p
y
⋅
0
+
p
z
⋅
0
+
1
)
=\left(\begin{matrix} p_x\cdot\overrightarrow X_x+p_y\cdot\overrightarrow Y_x+p_z\cdot\overrightarrow Z_x+1\cdot O_x\\ p_x\cdot\overrightarrow X_y+p_y\cdot\overrightarrow Y_y+p_z\cdot\overrightarrow Z_y+1\cdot O_y\\ p_x\cdot\overrightarrow X_z+p_y\cdot\overrightarrow Y_z+p_z\cdot\overrightarrow Z_z+1\cdot O_z\\ p_x\cdot0+p_y\cdot 0+p_z\cdot0+1 \end{matrix}\right)
=⎝⎜⎜⎜⎛px⋅Xx+py⋅Yx+pz⋅Zx+1⋅Oxpx⋅Xy+py⋅Yy+pz⋅Zy+1⋅Oypx⋅Xz+py⋅Yz+pz⋅Zz+1⋅Ozpx⋅0+py⋅0+pz⋅0+1⎠⎟⎟⎟⎞
调整一下排版便于理解:
=
(
p
x
⋅
X
→
x
+
p
y
⋅
Y
→
x
+
p
z
⋅
Z
→
x
+
1
⋅
O
x
p
x
⋅
X
→
y
+
p
y
⋅
Y
→
y
+
p
z
⋅
Z
→
y
+
1
⋅
O
y
p
x
⋅
X
→
z
+
p
y
⋅
Y
→
z
+
p
z
⋅
Z
→
z
+
1
⋅
O
z
p
x
⋅
0
+
p
y
⋅
0
+
p
z
⋅
0
+
1
)
=\left(\begin{matrix} p_x\cdot\overrightarrow X_x & + & p_y\cdot\overrightarrow Y_x & + & p_z\cdot\overrightarrow Z_x & + & 1\cdot O_x\\ p_x\cdot\overrightarrow X_y & + & p_y\cdot\overrightarrow Y_y & + & p_z\cdot\overrightarrow Z_y & + & 1\cdot O_y\\ p_x\cdot\overrightarrow X_z & + & p_y\cdot\overrightarrow Y_z & + & p_z\cdot\overrightarrow Z_z & + & 1\cdot O_z\\ p_x\cdot0 & + & p_y\cdot 0 & + & p_z\cdot0 & + & 1 \end{matrix}\right)
=⎝⎜⎜⎜⎛px⋅Xxpx⋅Xypx⋅Xzpx⋅0++++py⋅Yxpy⋅Yypy⋅Yzpy⋅0++++pz⋅Zxpz⋅Zypz⋅Zzpz⋅0++++1⋅Ox1⋅Oy1⋅Oz1⎠⎟⎟⎟⎞
结果和 矩阵和矩阵的乘法 一毛一样(4x4
⋅
\cdot
⋅ 4x1),或是 矩阵和向量的乘法 也可以,把后面的4x1当做列向量。
(结果我还是不小心又重复了 矩阵的乘法内容,其实直观的本质东西,就是那些抽象的同本质的东西的另一种更好理解的表述方式。)
注意 O O O与 P w P_w Pw也就是 P P P向量(点)的第四个分量 1 1 1, 1 1 1乘以任何实数等于它本身,也就是保留偏移量的意思,所以你现在应该弄懂了我之前说的齐次坐标的两个作用当中的第一个作用了吧?
缩放矩阵
前面演示了 点(向量)如何在一下矩阵坐标系下显示/定位的,那么使用的是一个像是2D的坐标系(因为有一个Z轴垂直于XY平面),它是一个左手坐标系(我要是早知道GGB的3D视图只能有右手坐标系的话,我就早应该用右手坐标系来说明了,但是能确定 OpenGL 的 NDC 空间下是左手坐标系的,因为我使用过一个矩阵旋转时,看旋转方式就知道了)
那么 为了演示缩放,我使用 GGB 的 3D视图来演示,这是一个右手坐标系。
P
2
P2
P2点就是我们上面说的沿着3个轴,分量平移后得到的最终坐标。
如果在这个坐标轴下,我们缩放坐标轴,那么
P
2
P2
P2点会怎么样的变换呢?
从观察结果得到一个结论:
- P 2 P2 P2点没去手动调整它的坐标,但是调整了它所在的坐标系的 正交基向量 的坐标轴,它却跟着变化了。
- 其实也可以不调整坐标系的轴,改而直接调整 P 2 P2 P2点的坐标,一样可以达到同样的效果。
先来看看之前的式子:
(
p
1
x
p
1
y
p
1
z
1
)
=
[
∣
∣
∣
O
x
X
→
Y
→
Z
→
O
y
∣
∣
∣
O
z
0
0
0
1
]
⋅
(
p
x
p
y
p
z
1
)
\left(\begin{matrix} p1_x\\ p1_y\\ p1_z\\ 1\end{matrix}\right) =\begin{bmatrix} | & | & | & O_x\\ \overrightarrow X & \overrightarrow Y & \overrightarrow Z & O_y\\ | & | & | & O_z\\ 0 & 0 & 0 & 1 \end{bmatrix} \cdot \left(\begin{matrix} p_x\\ p_y\\ p_z\\ 1\end{matrix}\right)
⎝⎜⎜⎛p1xp1yp1z1⎠⎟⎟⎞=⎣⎢⎢⎡∣X∣0∣Y∣0∣Z∣0OxOyOz1⎦⎥⎥⎤⋅⎝⎜⎜⎛pxpypz1⎠⎟⎟⎞
按照上面示意图中,那么对应矩阵会作何调整呢?很明显直接对坐标轴缩放就可以了,假设要放大整体的X,Y,Z轴的2倍,那么可以这么表示:
(
2
p
1
x
2
p
1
y
2
p
1
z
1
)
=
[
∣
∣
∣
O
x
2
X
→
2
Y
→
2
Z
→
O
y
∣
∣
∣
O
z
0
0
0
1
]
⋅
(
p
x
p
y
p
z
1
)
\left(\begin{matrix} 2p1_x\\ 2p1_y\\ 2p1_z\\ 1\end{matrix}\right) =\begin{bmatrix} | & | & | & O_x\\ 2\overrightarrow X & 2\overrightarrow Y & 2\overrightarrow Z & O_y\\ | & | & | & O_z\\ 0 & 0 & 0 & 1 \end{bmatrix} \cdot \left(\begin{matrix} p_x\\ p_y\\ p_z\\ 1\end{matrix}\right)
⎝⎜⎜⎛2p1x2p1y2p1z1⎠⎟⎟⎞=⎣⎢⎢⎡∣2X∣0∣2Y∣0∣2Z∣0OxOyOz1⎦⎥⎥⎤⋅⎝⎜⎜⎛pxpypz1⎠⎟⎟⎞
那么最终的式子的代入、展开后的结果为:
=
(
p
x
⋅
(
2
X
→
x
)
+
p
y
⋅
(
2
Y
→
x
)
+
p
z
⋅
(
2
Z
→
x
)
+
1
⋅
O
x
p
x
⋅
(
2
X
→
y
)
+
p
y
⋅
(
2
Y
→
y
)
+
p
z
⋅
(
2
Z
→
y
)
+
1
⋅
O
y
p
x
⋅
(
2
X
→
z
)
+
p
y
⋅
(
2
Y
→
z
)
+
p
z
⋅
(
2
Z
→
z
)
+
1
⋅
O
z
p
x
⋅
0
+
p
y
⋅
0
+
p
z
⋅
0
+
1
)
=\left(\begin{matrix} p_x\cdot(2\overrightarrow X_x) + p_y\cdot(2\overrightarrow Y_x) + p_z\cdot(2\overrightarrow Z_x) + 1\cdot O_x\\ p_x\cdot(2\overrightarrow X_y) + p_y\cdot(2\overrightarrow Y_y) + p_z\cdot(2\overrightarrow Z_y) + 1\cdot O_y\\ p_x\cdot(2\overrightarrow X_z) + p_y\cdot(2\overrightarrow Y_z) + p_z\cdot(2\overrightarrow Z_z) + 1\cdot O_z\\ p_x\cdot0 + p_y\cdot 0 + p_z\cdot0 + 1 \end{matrix}\right)
=⎝⎜⎜⎜⎛px⋅(2Xx)+py⋅(2Yx)+pz⋅(2Zx)+1⋅Oxpx⋅(2Xy)+py⋅(2Yy)+pz⋅(2Zy)+1⋅Oypx⋅(2Xz)+py⋅(2Yz)+pz⋅(2Zz)+1⋅Ozpx⋅0+py⋅0+pz⋅0+1⎠⎟⎟⎟⎞
然后我们把:
X
→
x
=
1
,
X
→
y
=
0
,
X
→
z
=
0
\overrightarrow X_x=1, \overrightarrow X_y=0,\overrightarrow X_z=0
Xx=1,Xy=0,Xz=0
Y
→
x
=
0
,
Y
→
y
=
1
,
Y
→
z
=
0
\overrightarrow Y_x=0, \overrightarrow Y_y=1,\overrightarrow Y_z=0
Yx=0,Yy=1,Yz=0
Z
→
x
=
0
,
Z
→
y
=
0
,
Z
→
z
=
1
\overrightarrow Z_x=0, \overrightarrow Z_y=0,\overrightarrow Z_z=1
Zx=0,Zy=0,Zz=1
O
x
=
0
,
O
y
=
0
,
O
z
=
0
O_x=0, O_y=0,O_z=0
Ox=0,Oy=0,Oz=0
都代入:
=
(
p
x
⋅
(
2
⋅
1
)
+
p
y
⋅
(
2
⋅
0
)
+
p
z
⋅
(
2
⋅
0
)
+
1
⋅
0
p
x
⋅
(
2
⋅
0
)
+
p
y
⋅
(
2
⋅
1
)
+
p
z
⋅
(
2
⋅
0
)
+
1
⋅
0
p
x
⋅
(
2
⋅
0
)
+
p
y
⋅
(
2
⋅
0
)
+
p
z
⋅
(
2
⋅
1
)
+
1
⋅
0
p
x
⋅
0
+
p
y
⋅
0
+
p
z
⋅
0
+
1
)
=
(
2
p
1
x
2
p
1
y
2
p
1
z
1
)
=\left(\begin{matrix} p_x\cdot(2\cdot 1) + p_y\cdot(2\cdot 0) + p_z\cdot(2\cdot 0) + 1\cdot 0\\ p_x\cdot(2\cdot 0) + p_y\cdot(2\cdot 1) + p_z\cdot(2\cdot 0) + 1\cdot 0\\ p_x\cdot(2\cdot 0) + p_y\cdot(2\cdot 0) + p_z\cdot(2\cdot 1) + 1\cdot 0\\ p_x\cdot0 + p_y\cdot 0 + p_z\cdot0 + 1 \end{matrix}\right)= \left(\begin{matrix} 2p1_x\\ 2p1_y\\ 2p1_z\\ 1\end{matrix}\right)
=⎝⎜⎜⎛px⋅(2⋅1)+py⋅(2⋅0)+pz⋅(2⋅0)+1⋅0px⋅(2⋅0)+py⋅(2⋅1)+pz⋅(2⋅0)+1⋅0px⋅(2⋅0)+py⋅(2⋅0)+pz⋅(2⋅1)+1⋅0px⋅0+py⋅0+pz⋅0+1⎠⎟⎟⎞=⎝⎜⎜⎛2p1x2p1y2p1z1⎠⎟⎟⎞
( 2 p 1 x 2 p 1 y 2 p 1 z 1 ) \left(\begin{matrix} 2p1_x\\ 2p1_y\\ 2p1_z\\ 1\end{matrix}\right) ⎝⎜⎜⎛2p1x2p1y2p1z1⎠⎟⎟⎞就是我们想要结果。
写成 LaTex 公式就是:
[
s
c
l
a
e
x
0
0
0
0
s
c
a
l
e
y
0
0
0
0
s
c
a
l
e
z
0
0
0
0
1
]
⋅
(
x
y
z
1
)
=
(
s
c
a
l
e
x
⋅
x
s
c
a
l
e
y
⋅
y
s
c
a
l
e
z
⋅
z
1
)
,
\begin{bmatrix} \color{#ff0000} sclae_x & \color{#ff0000} 0 & \color{#ff0000} 0 & \color{#ff0000}0\\ \color{#008800} 0 & \color{#008800} scale_y & \color{#008800} 0 & \color{#008800}0\\ \color{#0000ff} 0 & \color{#0000ff} 0 & \color{#0000ff} scale_z & \color{#0000ff}0\\ \color{#bb00bb} 0 & \color{#bb00bb} 0 & \color{#bb00bb} 0 & \color{#bb00bb}1 \end{bmatrix}\cdot \left(\begin{matrix} x\\ y\\ z\\ 1 \end{matrix}\right)= \left(\begin{matrix} {\color{#ff0000} scale_x} \cdot x\\ {\color{#008800} scale_y} \cdot y\\ {\color{#0000ff} scale_z} \cdot z\\ 1 \end{matrix}\right),
⎣⎢⎢⎡sclaex0000scaley0000scalez00001⎦⎥⎥⎤⋅⎝⎜⎜⎛xyz1⎠⎟⎟⎞=⎝⎜⎜⎛scalex⋅xscaley⋅yscalez⋅z1⎠⎟⎟⎞,
注意 ,我们的 缩放 矩阵现在新的坐标系的正交基向量分别为:
X
→
=
(
s
c
l
a
e
x
0
0
0
)
,
Y
→
=
(
0
s
c
l
a
e
y
0
0
)
,
Z
→
=
(
0
0
s
c
l
a
e
z
0
)
\overrightarrow X= \left(\begin{matrix} \color{#ff0000} sclae_x\\ \color{#008800} 0\\ \color{#0000ff} 0\\ \color{#bb00bb} 0 \end{matrix}\right), \overrightarrow Y= \left(\begin{matrix} \color{#ff0000} 0\\ \color{#008800} sclae_y\\ \color{#0000ff} 0\\ \color{#bb00bb} 0 \end{matrix}\right), \overrightarrow Z= \left(\begin{matrix} \color{#ff0000} 0\\ \color{#008800} 0\\ \color{#0000ff} sclae_z\\ \color{#bb00bb} 0 \end{matrix}\right)
X=⎝⎜⎜⎛sclaex000⎠⎟⎟⎞,Y=⎝⎜⎜⎛0sclaey00⎠⎟⎟⎞,Z=⎝⎜⎜⎛00sclaez0⎠⎟⎟⎞
旋转矩阵
旋转矩阵 变换 这篇里面没有详细讲解,他也是建议想深入了解的朋友去看 可汗学院 的教程。
他列出了矩阵公式:
上面的公式,可能基础稍微缺一些的同学,可能就会看懵
以:沿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{#ff0000} \cos\theta & \color{#ff0000} -\sin\theta & \color{#ff0000} 0 & \color{#ff0000}0\\ \color{#008800} \sin\theta & \color{#008800}\cos\theta & \color{#008800} 0 & \color{#008800}0\\ \color{#0000ff} 0 & \color{#0000ff}0 & \color{#0000ff} 1 & \color{#0000ff}0\\ \color{#bb00bb} 0 & \color{#bb00bb}0 & \color{#bb00bb} 0 & \color{#bb00bb}1 \end{bmatrix}\cdot \left(\begin{matrix} x\\ y\\ z\\ 1 \end{matrix}\right)= \left(\begin{matrix} {\color{#ff0000} \cos\theta} \cdot x {\color{#ff0000} -\sin\theta} \cdot y\\ {\color{#008800} \sin\theta} \cdot x + {\color{#008800} \cos\theta} \cdot y\\ z\\ 1 \end{matrix}\right)
⎣⎢⎢⎡cosθsinθ00−sinθcosθ0000100001⎦⎥⎥⎤⋅⎝⎜⎜⎛xyz1⎠⎟⎟⎞=⎝⎜⎜⎛cosθ⋅x−sinθ⋅ysinθ⋅x+cosθ⋅yz1⎠⎟⎟⎞
先从单个 X \color{#ff0000}X X 点的旋转情况来看,现在在 XY 平面中,有两个点: X s o u r c e , X X_{source},\color{#ff0000}X Xsource,X。
其实还有一个 Z 轴,但是与 XY 平面垂直了,在画面中值呈现于一个奇点(与原点 O O O重合了),就不画出来了。
该 Z 轴的增量方向(正方向)指向着屏幕内的方向,所以整个坐标是一个:左手坐标。
X s o u r c e X_{source} Xsource 是 X \color{#ff0000}X X 点的原始位置,为了直观,我将 X s o u r c e , X X_{source},\color{#ff0000}X Xsource,X 两个点的位置,用了向量链接着,向量的起点在原点,终点分别指向两个点。
上图中,左上角分别是:degree(角度)=20,radius( X s o u r c e X_{source} Xsource点与原点的半径距离)=1。
这时如果我们调整 degree的话,就可以看到旋转的过程:
因为 radius 等于 1,我们可以不先,这样可以让图像更清晰一些:
可以看到:原来在 X 轴上的 X s o u r c e X_{source} Xsource 点作为:(1,0),在旋转后就变成了: X ( cos d , sin d ) \color{#ff0000}X(\cos d, \sin d) X(cosd,sind)(这个三角函数的知识点在初中就学过,希望你还没有忘记)。
我们可以把上面的 X s o u r c e X_{source} Xsource点当做是X轴的单位轴。
在回头看看之前的沿着Z轴的旋转矩阵:
[
cos
θ
−
sin
θ
0
0
sin
θ
cos
θ
0
0
0
0
1
0
0
0
0
1
]
⋅
(
x
y
z
w
)
=
(
cos
θ
⋅
x
−
sin
θ
⋅
y
sin
θ
⋅
x
+
cos
θ
⋅
y
z
w
)
\begin{bmatrix} \color{#ff0000} \cos\theta & \color{#ff0000} -\sin\theta & \color{#ff0000} 0 & \color{#ff0000}0\\ \color{#008800} \sin\theta & \color{#008800}\cos\theta & \color{#008800} 0 & \color{#008800}0\\ \color{#0000ff} 0 & \color{#0000ff}0 & \color{#0000ff} 1 & \color{#0000ff}0\\ \color{#bb00bb} 0 & \color{#bb00bb}0 & \color{#bb00bb} 0 & \color{#bb00bb}1 \end{bmatrix}\cdot \left(\begin{matrix} x\\ y\\ z\\ w \end{matrix}\right)= \left(\begin{matrix} {\color{#ff0000} \cos\theta} \cdot x {\color{#ff0000} -\sin\theta} \cdot y\\ {\color{#008800} \sin\theta} \cdot x + {\color{#008800} \cos\theta} \cdot y\\ z\\ w \end{matrix}\right)
⎣⎢⎢⎡cosθsinθ00−sinθcosθ0000100001⎦⎥⎥⎤⋅⎝⎜⎜⎛xyzw⎠⎟⎟⎞=⎝⎜⎜⎛cosθ⋅x−sinθ⋅ysinθ⋅x+cosθ⋅yzw⎠⎟⎟⎞
它其实对应着:
[
X
轴
x
分
量
−
sin
θ
0
0
X
轴
y
分
量
cos
θ
0
0
0
0
1
0
0
0
0
1
]
⋅
(
x
y
z
w
)
=
(
X
轴
x
分
量
⋅
x
−
sin
θ
⋅
y
X
轴
y
分
量
⋅
x
+
cos
θ
⋅
y
z
w
)
\begin{bmatrix} \color{#ff0000} X轴_{x分量} & \color{#ff0000} -\sin\theta & \color{#ff0000} 0 & \color{#ff0000}0\\ \color{#008800} X轴_{y分量} & \color{#008800}\cos\theta & \color{#008800} 0 & \color{#008800}0\\ \color{#0000ff} 0 & \color{#0000ff}0 & \color{#0000ff} 1 & \color{#0000ff}0\\ \color{#bb00bb} 0 & \color{#bb00bb}0 & \color{#bb00bb} 0 & \color{#bb00bb}1 \end{bmatrix}\cdot \left(\begin{matrix} x\\ y\\ z\\ w \end{matrix}\right)= \left(\begin{matrix} {\color{#ff0000} X轴_{x分量}} \cdot x {\color{#ff0000} -\sin\theta} \cdot y\\ {\color{#008800} X轴_{y分量} } \cdot x + {\color{#008800} \cos\theta} \cdot y\\ z\\ w \end{matrix}\right)
⎣⎢⎢⎡X轴x分量X轴y分量00−sinθcosθ0000100001⎦⎥⎥⎤⋅⎝⎜⎜⎛xyzw⎠⎟⎟⎞=⎝⎜⎜⎛X轴x分量⋅x−sinθ⋅yX轴y分量⋅x+cosθ⋅yzw⎠⎟⎟⎞
同样的,我们将Y轴的单位轴添加上,也添加上对应旋转后的Y轴方向
它其实就是对应着矩阵:
[
X
轴
x
分
量
Y
轴
x
分
量
0
0
X
轴
y
分
量
Y
轴
y
分
量
0
0
0
0
1
0
0
0
0
1
]
⋅
(
x
y
z
w
)
=
(
X
轴
x
分
量
⋅
x
+
Y
轴
x
分
量
⋅
y
X
轴
y
分
量
⋅
x
+
Y
轴
y
分
量
⋅
y
z
w
)
\begin{bmatrix} \color{#ff0000} X轴_{x分量} & \color{#ff0000} Y轴_{x分量} & \color{#ff0000} 0 & \color{#ff0000}0\\ \color{#008800} X轴_{y分量} & \color{#008800} Y轴_{y分量} & \color{#008800} 0 & \color{#008800}0\\ \color{#0000ff} 0 & \color{#0000ff}0 & \color{#0000ff} 1 & \color{#0000ff}0\\ \color{#bb00bb} 0 & \color{#bb00bb}0 & \color{#bb00bb} 0 & \color{#bb00bb}1 \end{bmatrix}\cdot \left(\begin{matrix} x\\ y\\ z\\ w \end{matrix}\right)= \left(\begin{matrix} {\color{#ff0000} X轴_{x分量}} \cdot x + {\color{#ff0000} Y轴_{x分量}} \cdot y\\ {\color{#008800} X轴_{y分量}} \cdot x + {\color{#008800} Y轴_{y分量}} \cdot y\\ z\\ w \end{matrix}\right)
⎣⎢⎢⎡X轴x分量X轴y分量00Y轴x分量Y轴y分量0000100001⎦⎥⎥⎤⋅⎝⎜⎜⎛xyzw⎠⎟⎟⎞=⎝⎜⎜⎛X轴x分量⋅x+Y轴x分量⋅yX轴y分量⋅x+Y轴y分量⋅yzw⎠⎟⎟⎞
这时可以确定:
X轴
- X 轴 x 分 量 = cos d {\color{#ff0000} X轴_{x分量}}=\cos d X轴x分量=cosd
- X 轴 y 分 量 = sin d {\color{#008800} X轴_{y分量}}=\sin d X轴y分量=sind
Y轴
- Y 轴 x 分 量 = − sin d {\color{#ff0000} Y轴_{x分量}}=-\sin d Y轴x分量=−sind
- Y 轴 y 分 量 = cos d {\color{#008800} Y轴_{y分量}}=\cos d Y轴y分量=cosd
我们的任意点:
(
x
y
z
w
)
\left(\begin{matrix} x\\ y\\ z\\ w \end{matrix}\right)
⎝⎜⎜⎛xyzw⎠⎟⎟⎞
现在新的X、Y轴向为:
X
轴
=
(
X
轴
x
分
量
,
X
轴
y
分
量
)
=
(
cos
d
,
sin
d
)
X轴=({\color{#ff0000} X轴_{x分量}},{\color{#008800} X轴_{y分量}})=({\color{#ff0000}\cos d}, {\color{#008800} \sin d})
X轴=(X轴x分量,X轴y分量)=(cosd,sind)
Y
轴
=
(
Y
轴
x
分
量
,
Y
轴
y
分
量
)
=
(
−
sin
d
,
cos
d
)
Y轴=({\color{#ff0000} Y轴_{x分量}},{\color{#008800} Y轴_{y分量}})=({\color{#ff0000} -\sin d},{\color{#008800} \cos d})
Y轴=(Y轴x分量,Y轴y分量)=(−sind,cosd)
注意 ,我们的 旋转 矩阵现在新的坐标系的 正交基向量 分别为:
X
→
=
(
X
轴
x
分
量
X
轴
y
分
量
0
0
)
,
Y
→
=
(
Y
轴
x
分
量
Y
轴
y
分
量
0
0
)
,
Z
→
=
(
0
0
1
0
)
\overrightarrow X= \left(\begin{matrix} \color{#ff0000} X轴_{x分量}\\ \color{#008800} X轴_{y分量}\\ \color{#0000ff} 0\\ \color{#bb00bb} 0 \end{matrix}\right), \overrightarrow Y= \left(\begin{matrix} \color{#ff0000} Y轴_{x分量}\\ \color{#008800} Y轴_{y分量}\\ \color{#0000ff} 0\\ \color{#bb00bb} 0 \end{matrix}\right), \overrightarrow Z= \left(\begin{matrix} \color{#ff0000} 0\\ \color{#008800} 0\\ \color{#0000ff} 1\\ \color{#bb00bb} 0 \end{matrix}\right)
X=⎝⎜⎜⎛X轴x分量X轴y分量00⎠⎟⎟⎞,Y=⎝⎜⎜⎛Y轴x分量Y轴y分量00⎠⎟⎟⎞,Z=⎝⎜⎜⎛0010⎠⎟⎟⎞
然是沿着X,Y轴旋转的原理是一样的,就不再说明了。
欧拉角
Roll, Pitch, Yaw
一般我们在看学习资料中,欧拉角都由:Roll, Yaw, Pitch 几个角来定方向的
可以参考:
- Yaw, Pitch and Roll - What’s it?
- Roll, Pitch, and Yaw
- Aircraft principal axes
- 1.1 Vertical axis (yaw)
- 1.2 Transverse axis (pitch)
- 1.3 Longitudinal axis (roll)
总结一张图:
Heading, Pitch, Bank
Object Rotation, Heading, Pitch and Bank
下面以 C4D 中的 H,P,B 来做演示
下面时 H, P, B 均为0
Heading -45
Pitch 45
Bank 45
具体旋转应用
以下参考:unity中的欧拉角
判断方法1:
假设对此系统按yxz的顺序进行旋转,则:
1,首先绕y轴旋转,子节点x,z轴会发生变化。
2,再绕x轴旋转,由于上一步中x已发生变化,所以是绕变化后的x轴旋转。由于z轴是x轴子节点,所以z轴发生变化。
3,再绕z轴旋转,由于前两步中z轴已发生变化,所以是绕变化后的z轴旋转。
由此可见,按yxz顺序旋转的话,每一步都是在上一步变化后的基础上进行旋转,即每一步都是相对于当前物体坐标系进行旋转,所以此系统是“yxz物体空间旋转系统”,即正是unity所采用的欧拉角系统。
判断方法2:
假设对此系统按zxy的顺序进行旋转,则:
1,首先绕z轴进行旋转,由于z轴是终端节点,所以x轴和y轴都不会发生变化。
2,再绕x轴进行旋转,由于x轴在上一步旋转中没有发生变化,所以就等价于绕惯性空间的x轴旋转。由于y轴是x轴父节点,所以y轴不会发生变化。
3,再绕y轴进行旋转,由于y轴在上一步旋转中没有发生变化,所以就等价于绕惯性空间的y轴旋转。
由此可见,按zxy顺序旋转的话,每一步都等价于相对于惯性坐标系进行旋转,所以此系统是“zxy惯性空间旋转系统”,即正是unity所采用的欧拉角系统。
旋转矩阵来表示
yxz 欧拉旋转(下面是左乘,先将应用的变换放置右边,新应用的变换放左边)
M
r
=
M
R
z
⋅
M
R
x
⋅
M
R
y
M_r = M_{Rz} \cdot M_{Rx} \cdot M_{Ry}
Mr=MRz⋅MRx⋅MRy
zxy 欧拉旋转
M
r
=
M
R
y
⋅
M
R
x
⋅
M
R
z
M_r = M_{Ry} \cdot M_{Rx} \cdot M_{Rz}
Mr=MRy⋅MRx⋅MRz
有了
M
r
M_r
Mr 就可以用来变换 方向
V
′
=
M
r
⋅
V
V'=M_r \cdot V
V′=Mr⋅V
之前有位 同事 问我如何将一个 欧拉角转 方向
那么在 Unity CSharp API 下可以这么使用:
float x = 0.0f, y = 0.0f, z = 0.0f;
var forward = Vector3.forward;
var newForward = Quaternion.Euler(x, y, z) * forward;
在 shader 中,需要自己构建欧拉角对应的矩阵
M r = M R y ⋅ M R x ⋅ M R z M_r = M_{Ry} \cdot M_{Rx} \cdot M_{Rz} Mr=MRy⋅MRx⋅MRz
M r = [ R X x R Y x R Z x R X y R Y y R Z y R X Z R Y z R Z z ] M_r =\begin{bmatrix} R_{X_x} & R_{Y_x} & R_{Z_x} \\ R_{X_y} & R_{Y_y} & R_{Z_y} \\ R_{X_Z} & R_{Y_z} & R_{Z_z} \end{bmatrix} Mr=⎣⎡RXxRXyRXZRYxRYyRYzRZxRZyRZz⎦⎤
F ′ = ( F x ′ F y ′ F z ′ ) F'=\left(\begin{matrix} F_x'\\ F_y'\\ F_z'\end{matrix}\right) F′=⎝⎛Fx′Fy′Fz′⎠⎞
F = ( F x F y F z ) F=\left(\begin{matrix} F_x\\ F_y\\ F_z\end{matrix}\right) F=⎝⎛FxFyFz⎠⎞
F ′ = M r ⋅ F F'=M_r \cdot F F′=Mr⋅F
( F x ′ F y ′ F z ′ ) = [ R X x R Y x R Z x R X y R Y y R Z y R X Z R Y z R Z z ] ⋅ ( F x F y F z ) \left(\begin{matrix} F_x'\\ F_y'\\ F_z'\end{matrix}\right) =\begin{bmatrix} R_{X_x} & R_{Y_x} & R_{Z_x} \\ R_{X_y} & R_{Y_y} & R_{Z_y} \\ R_{X_Z} & R_{Y_z} & R_{Z_z} \end{bmatrix} \cdot \left(\begin{matrix} F_x\\ F_y\\ F_z\end{matrix}\right) ⎝⎛Fx′Fy′Fz′⎠⎞=⎣⎡RXxRXyRXZRYxRYyRYzRZxRZyRZz⎦⎤⋅⎝⎛FxFyFz⎠⎞
因为一般我们的 forward 都事 z=1,x,y 都事0
F
=
(
0
0
1
)
F=\left(\begin{matrix} 0\\ 0\\ 1\end{matrix}\right)
F=⎝⎛001⎠⎞
那么其实只要关注:
F
′
=
[
R
X
x
R
Y
x
R
Z
x
R
X
y
R
Y
y
R
Z
y
R
X
Z
R
Y
z
R
Z
z
]
⋅
(
0
0
1
)
F'=\begin{bmatrix} R_{X_x} & R_{Y_x} & R_{Z_x} \\ R_{X_y} & R_{Y_y} & R_{Z_y} \\ \color{#ff0000} R_{X_Z} & \color{#ff0000}R_{Y_z} & \color{#ff0000}R_{Z_z} \end{bmatrix} \cdot \left(\begin{matrix} 0\\ 0\\ \color{#ff0000}1\end{matrix}\right)
F′=⎣⎡RXxRXyRXZRYxRYyRYzRZxRZyRZz⎦⎤⋅⎝⎛001⎠⎞
另外,欧拉旋转方式有多种组合:
具体参考: How to convert Euler angles to directional vector? 中的
typedef float Matrix[3][3];
struct EulerAngle { float X,Y,Z; };
// Euler Order enum.
enum EEulerOrder
{
ORDER_XYZ,
ORDER_YZX,
ORDER_ZXY,
ORDER_ZYX,
ORDER_YXZ,
ORDER_XZY
};
Matrix EulerAnglesToMatrix(const EulerAngle &inEulerAngle,EEulerOrder EulerOrder)
{
// Convert Euler Angles passed in a vector of Radians
// into a rotation matrix. The individual Euler Angles are
// processed in the order requested.
Matrix Mx;
const FLOAT Sx = sinf(inEulerAngle.X);
const FLOAT Sy = sinf(inEulerAngle.Y);
const FLOAT Sz = sinf(inEulerAngle.Z);
const FLOAT Cx = cosf(inEulerAngle.X);
const FLOAT Cy = cosf(inEulerAngle.Y);
const FLOAT Cz = cosf(inEulerAngle.Z);
switch(EulerOrder)
{
case ORDER_XYZ:
Mx.M[0][0]=Cy*Cz;
Mx.M[0][1]=-Cy*Sz;
Mx.M[0][2]=Sy;
Mx.M[1][0]=Cz*Sx*Sy+Cx*Sz;
Mx.M[1][1]=Cx*Cz-Sx*Sy*Sz;
Mx.M[1][2]=-Cy*Sx;
Mx.M[2][0]=-Cx*Cz*Sy+Sx*Sz;
Mx.M[2][1]=Cz*Sx+Cx*Sy*Sz;
Mx.M[2][2]=Cx*Cy;
break;
case ORDER_YZX:
Mx.M[0][0]=Cy*Cz;
Mx.M[0][1]=Sx*Sy-Cx*Cy*Sz;
Mx.M[0][2]=Cx*Sy+Cy*Sx*Sz;
Mx.M[1][0]=Sz;
Mx.M[1][1]=Cx*Cz;
Mx.M[1][2]=-Cz*Sx;
Mx.M[2][0]=-Cz*Sy;
Mx.M[2][1]=Cy*Sx+Cx*Sy*Sz;
Mx.M[2][2]=Cx*Cy-Sx*Sy*Sz;
break;
case ORDER_ZXY:
Mx.M[0][0]=Cy*Cz-Sx*Sy*Sz;
Mx.M[0][1]=-Cx*Sz;
Mx.M[0][2]=Cz*Sy+Cy*Sx*Sz;
Mx.M[1][0]=Cz*Sx*Sy+Cy*Sz;
Mx.M[1][1]=Cx*Cz;
Mx.M[1][2]=-Cy*Cz*Sx+Sy*Sz;
Mx.M[2][0]=-Cx*Sy;
Mx.M[2][1]=Sx;
Mx.M[2][2]=Cx*Cy;
break;
case ORDER_ZYX:
Mx.M[0][0]=Cy*Cz;
Mx.M[0][1]=Cz*Sx*Sy-Cx*Sz;
Mx.M[0][2]=Cx*Cz*Sy+Sx*Sz;
Mx.M[1][0]=Cy*Sz;
Mx.M[1][1]=Cx*Cz+Sx*Sy*Sz;
Mx.M[1][2]=-Cz*Sx+Cx*Sy*Sz;
Mx.M[2][0]=-Sy;
Mx.M[2][1]=Cy*Sx;
Mx.M[2][2]=Cx*Cy;
break;
case ORDER_YXZ:
Mx.M[0][0]=Cy*Cz+Sx*Sy*Sz;
Mx.M[0][1]=Cz*Sx*Sy-Cy*Sz;
Mx.M[0][2]=Cx*Sy;
Mx.M[1][0]=Cx*Sz;
Mx.M[1][1]=Cx*Cz;
Mx.M[1][2]=-Sx;
Mx.M[2][0]=-Cz*Sy+Cy*Sx*Sz;
Mx.M[2][1]=Cy*Cz*Sx+Sy*Sz;
Mx.M[2][2]=Cx*Cy;
break;
case ORDER_XZY:
Mx.M[0][0]=Cy*Cz;
Mx.M[0][1]=-Sz;
Mx.M[0][2]=Cz*Sy;
Mx.M[1][0]=Sx*Sy+Cx*Cy*Sz;
Mx.M[1][1]=Cx*Cz;
Mx.M[1][2]=-Cy*Sx+Cx*Sy*Sz;
Mx.M[2][0]=-Cx*Sy+Cy*Sx*Sz;
Mx.M[2][1]=Cz*Sx;
Mx.M[2][2]=Cx*Cy+Sx*Sy*Sz;
break;
}
return(Mx);
}
注意万向锁
还是参考:unity中的欧拉角
可见,万向锁就是:在指定旋转顺序下,绕第二轴旋转正或负90至使一三两轴平行或反向平行,一三两轴成了等效轴,于是物体由原来的可绕三个轴旋转变为只能绕两个轴旋转,丢失了一个旋转自由度。
另外还还可以看到,当发生万向锁时,rotation所对应的欧拉角不唯一,如果一三两轴平行,则绕第一轴的旋转角A1和绕第三轴的旋转角A3之和不变即可保证rotation不变;如果一三两轴反向平行,则只要A1和A3之差不变,即可保证rotation不变。
矩阵中的列就是新的坐标系的基向量
以主列序的矩阵为例,每一列,都是新的坐标系的一个坐标轴
3x3,或是 3x4 或是 4x4 中的前三行三列中的 3 列都可以这么理解
他们分别是变换到指定的矩阵变换后的新的坐标系的基向量
如:有个矩阵
M
M
M ,它的每一个列的对应下面的 x,y,z 轴
M
=
[
∣
∣
∣
X
Y
Z
∣
∣
∣
]
M= \begin{bmatrix} | & | & |\\ X & Y & Z\\ | & | & | \end{bmatrix}
M=⎣⎡∣X∣∣Y∣∣Z∣⎦⎤
再有一个
p
p
p 点:
p
=
(
p
x
p
y
p
z
)
p= \left(\begin{matrix} p_x\\ p_y\\ p_z\\ \end{matrix}\right)
p=⎝⎛pxpypz⎠⎞
那么将
p
′
p'
p′ 理解为
p
p
p 应用了
M
M
M 矩阵变换后的坐标
p
′
=
M
⋅
p
p'=M \cdot p
p′=M⋅p
那么此时的 p ′ p' p′ 所在的坐标系的三个 x,y,z 的基向量,都是 M M M 矩阵中对应的前三列基向量
这部分可以参考:《3D数学基础:图形与游戏开发》中的 7.2
在 Shader 中实现一些旋转
着色器中使用矩阵来旋转纹理、顶点、等效果
旋转顶点
Shader 的代码:
// jave.lin - testing_matrix_rotate_vertex.vert - 测试 shader 矩阵旋转顶点着色器
#version 450 compatibility
uniform float time;
attribute vec3 vPos;
attribute vec2 vUV;
varying vec2 fUV;
void main() {
#define ROT_TYPE 3 // 有3种方式应用旋转
#if ROT_TYPE == 1
float s = 0, c = 0;
// sincos(time, s, c); // glsl 没有类似 cg 的 sincos
s = sin(time);
c = cos(time);
vec3 p = vec3( // 非矩阵方式,直接矩阵的轴数据拿出来计算
c * vPos.x - s * vPos.y,
s * vPos.x + c * vPos.y,
vPos.z
);
gl_Position = vec4(p, 1.0);
#elif ROT_TYPE == 2
float s = 0, c = 0;
// sincos(time, s, c); // glsl 没有类似 cg 的 sincos
s = sin(time);
c = cos(time);
vec3 p = vPos;
mat2 rM = mat2(c, s, -s, c); // 主列设置,即:先设置的是每一列的向量轴
/*
// 或是写成下面的方式更好理解全文介绍矩阵的旋转的 正交基向量,作为坐标轴
// 我们只要调整坐标轴,即可调整属于这个坐标以下的向量做变换
vec2 x_a = vec2( c, s); // X 轴
vec2 y_a = vec2(-s, c); // Y 轴
mat2 rM = mat2( // X,Y轴组成的坐标系,或叫矩阵变换
// X轴 Y轴
x_a, y_a
);
*/
p.xy = rM * p.xy;
gl_Position = vec4(p, 1.0);
#else
float s = 0, c = 0;
// sincos(time, s, c); // glsl 没有类似 cg 的 sincos
s = sin(time);
c = cos(time);
vec3 p = vPos;
// 或是写成下面的方式更好理解全文介绍矩阵的旋转的 正交基向量,作为坐标轴
// 我们只要调整坐标轴,即可调整属于这个坐标以下的向量做变换
vec2 x_a = vec2( c, s); // X 轴
vec2 y_a = vec2(-s, c); // Y 轴
mat2 rM = mat2( // X,Y轴组成的坐标系,或叫矩阵变换
// X轴 Y轴
x_a, y_a
);
p.xy = rM * p.xy;
gl_Position = vec4(p, 1.0);
#endif
fUV = vUV;
}
// jave.lin - testing_matrix_rotate_vertex.frag - 测试 shader 矩阵旋转片段着色器
#version 450 compatibility
varying vec2 fUV; // uv 坐标
varying vec3 fPos;
uniform sampler2D main_tex; // 主纹理
uniform sampler2D mask_tex; // 遮罩纹理
uniform sampler2D flash_light_tex; // 闪光/流光纹理
uniform float time; // 时间(秒)用于动画
void main() {
vec3 mainCol = texture(main_tex, fUV).rgb;
float mask = texture(mask_tex, fUV).r;
vec4 flashCol = texture(flash_light_tex, fUV + vec2(-time, 0));
flashCol *= flashCol.a * mask;
mainCol = mainCol + flashCol.rgb;
gl_FragColor = vec4(mainCol, 1.0);
}
fragment shader
片段着色器的代码不用看,因为和上一节的一样。
主要我们改动的是 vertex shader
顶点着色器。
在代码中,注释写的非常的清楚 关于矩阵旋转的 三个写法,本质 上是 同一 种处理。
运行效果
旋转纹理 UV
这回不旋转顶点,改而旋转 UV 纹理坐标,我选择在片段着色器处理。
Shader 代码:
// jave.lin - testing_matrix_rotate_uv.vert - 测试矩阵旋转 UV 纹理坐标,顶点着色器不需要处理
#version 450 compatibility
attribute vec3 vPos;
attribute vec2 vUV;
varying vec2 fUV;
varying vec3 fPos;
void main() {
gl_Position = vec4(vPos, 1.0);
fUV = vUV;
fPos = vPos;
}
// jave.lin - testing_matrix_rotate_uv.vert - 测试矩阵旋转 UV 纹理坐标的片段着色器
#version 450 compatibility
varying vec2 fUV; // uv 坐标
varying vec3 fPos;
uniform sampler2D main_tex; // 主纹理
uniform sampler2D mask_tex; // 遮罩纹理
uniform sampler2D flash_light_tex; // 闪光/流光纹理
uniform float time; // 时间(秒)用于动画
void main() {
float t = (sin(time) * 0.5 + 0.5) * 3.14159265359 * 2;
// t *= length(fPos); // 半径越大旋转越多
// t *= 1 / length(fPos); // 半径约小旋转越多
vec2 uv = fUV;
uv -= 0.5;
uv = vec2(
cos(t) * uv.x - sin(t) * uv.y,
sin(t) * uv.x + cos(t) * uv.y
);
uv += 0.5;
vec3 mainCol = texture(main_tex, uv).rgb;
float mask = texture(mask_tex, uv).r;
vec4 flashCol = texture(flash_light_tex, uv + vec2(-time, 0));
flashCol *= flashCol.a * mask;
mainCol = mainCol + flashCol.rgb;
gl_FragColor = vec4(mainCol, 1.0);
// gl_FragColor = vec4(uv,0,1);
}
运行效果
调整一下控制参数,实现按半径大小来控制旋转量:
运行效果:
调整参数:
使用 半径越小旋转越大的方式,再次运行效果:
最后我们使用UV坐标来当做颜色来输出看到数据,一般调式 shader 常用方法之一:
在运行看看:
总结
3D 中的矩阵的线性变换引用是非常广泛的,建议理解其中的意义,对于后续制作其他的效果是很有帮助的。
之所以用矩阵,因为方便。
因为照样可以用一堆零散的变量来存贮线性变换方程组里的每个常量,每个变换就是一个方程组的系数,我只要让向量都应用这些系数来作为一次变换,一样是可以的。
但用使用矩阵后,书写上会变得很方便。
不要认为矩阵有多神奇,它其实是古代数学家、物理学家、等,先辈在计算线性变换时,想到的一个方便计算的数学工具而已。
其实我现在才发现自己还是挺喜欢数学的,因为现在很多要制作的内容,或是工作需要,都是需要数学的,现在只能在空闲的时候偶尔去看看一些零散的资料,好在现在发现 可汗学院 的官方网,好后悔以前数学没学好啊,-_-!!!我这只能算初中水平的,也忘记的七七八八了,也很佩服那些数学系的。
References
- Matrices as transformations
- 机器学习和深度学习之数学基础-线性代数 第四节 线性变换及其与矩阵的关系
- 理解矩阵(一)
- 理解矩阵(二)
- 理解矩阵(三)
- 线性代数之——向量空间 - 理解向量空间
- 向量空间相关概念总结-基 - 理解坐标系、基
- 《3D数学基础:图形与游戏开发》中的 7.2