向量
向量点乘(内积)
A=⎣⎢⎢⎢⎢⎢⎢⎡a11a21a31⋮an1a12a22a22⋮an2.........⋱...a1na2na3n⋮ann⎦⎥⎥⎥⎥⎥⎥⎤,b=⎣⎢⎢⎢⎢⎢⎢⎡b1b2b3⋮bn⎦⎥⎥⎥⎥⎥⎥⎤
此方法可类比物理做功
力 F 和位移 S 已给出,需要用 W=F⋅S 来表示做的功
计算方法就是
W=∣F∣∣S∣cosθ
所以向量点乘(内积)就是向量模相乘再乘夹角余弦,也就是他的几何形式:
a⋅b=∣a∣∣b∣cosθ
代数形式是:
a⋅b=i=1∑na1b1+a2b2+⋅⋅⋅+anbn
或者这个
ab=a⋅bT
例如:
[13−5]⎣⎢⎡4−2−1⎦⎥⎤=[3]=3
两个向量相乘变成了一个单数字矩阵
向量叉乘(外积)
此方法可以
计算方法
a×b=∣a∣∣b∣sinθ n
$ \vec n$ 指向右手坐标系大拇指位置,a 为食指,b 为中指
为什么用右手,因为我们用的就是右手坐标系,又称笛卡尔坐标系
注意:这个坐标轴可以随意旋转,只要不做镜像反转就行
坐标形式
A=(Ax,Ay,Az)
B=(Bx,By,Bz)
⎣⎢⎡iAxBxjAyBykAzBz⎦⎥⎤=[AyByAzBz]i+[AxBxAzBz]j+[AxBxAyBy]k
A×B=(AyBz−AzBy)i+(AxBz−AzBx)j+(AxBy−AyBx)k
系数是这么算的(子行列式)
如果你对这种计算方式感到疑惑,不用意外,我也曾感到意外,因为叉乘仿佛是一个代数和集合杂糅在一起的计算,但他们融合的有非常生硬,很反直觉……吗?其实不是,我们接着看
为什么叉乘的等号前面是括号坐标形式的向量,而等号右边是 i、j、k ?你注意到这个了吗?
我们把等号前面也改成 i、j、k 的形式
\begin{align}
& (A_xi+A_yj+A_zk)(B_xi+B_yj+B_zk) \\
= & A_xB_xi^2+A_yB_yj^2+A_zB_zk^2 \\
& +A_xB_yij+A_xB_zki \\
& +A_yB_xji+A_yB_zjk \\
& +A_zB_xki+A_zB_yjk
\end{align}
没错,我们用的完全就是代数乘法
为什么我将 ij、ji、jk、kj、ik、ki 分开,因为 i、j、k 是每个轴的单位向量,他们相乘需要用到叉乘
通过叉乘计算你就能得到
\begin{cases}
\begin{align}
& ij=-ji=k \\
& ki=-ik=j \\
& jk=-kj=i
\end{align}
\end{cases}
没错同方向或者相反方向的向量相乘,就会失去方向
我们继续合并完这个式子,得到如下
AxBxi2−AyByj2−AzBzk2+(AyBz−AzBy)i+(AxBz−AzBx)j+(AxBy−AyBx)k
这里,相同方向的向量 sinθ=0 ,所以这里的 −AxBx−AyBy−AzBz=0
你可以学习一下四元数的计算,这能更好地帮助你理解向量叉乘 => 四元数的代数意义
矩阵
矩阵乘法
-
数乘
3×⎣⎢⎡adgbehcfi⎦⎥⎤=⎣⎢⎡3a3d3g3b3e3h3c3f3i⎦⎥⎤
-
相乘
⎣⎢⎡147258369⎦⎥⎤⋅⎣⎢⎡adgbehcfi⎦⎥⎤=⎣⎢⎡a+2d+3g4a+5d+6g7a+8d+9gb+2e+3h4b+5e+6h7b+8e+9hc+2f+3i4c+5f+6i7c+8f+9i⎦⎥⎤
[123]⋅⎣⎢⎡adgbeh⎦⎥⎤=[a+2d+3gb+2e+3h]
Am×n⋅Bn×h=Cm×h
前面的行乘以后面的列
也就是,第一个矩阵的行数决定了最终矩阵的行数,第二个矩阵的列数决定了最终矩阵的列数
并且,第一个矩阵一行有几个元素,第二个矩阵一列就得有几个元素,这样才能进行运算
矩阵变换
矩阵变换一般指 要变换的向量或矩阵,左乘变换矩阵,生产成最终变换好的矩阵的过程
左乘和右乘
假设有个矩阵 A ,左乘 A 就是将 A 放到乘号左边,右乘就是将 A 放到乘号右边
⎣⎢⎢⎢⎡S10000S20000S300001⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡xyzw⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡S1⋅xS2⋅yS3⋅z1⋅w⎦⎥⎥⎥⎤
这就是 $ 向量 \begin{bmatrix} x \ y \ z \ w \end{bmatrix} 左乘矩阵 \begin{bmatrix} \color{red}{S_1} & \color{red}0 & \color{red}0 & \color{red}0 \ \color{green}0 & \color{green}{S_2} & \color{green}0 & \color{green}0 \ \color{blue}0 & \color{blue}0 & \color{blue}{S_3} & \color{blue}0 \ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} $
或者说 $ 矩阵 \begin{bmatrix} \color{red}{S_1} & \color{red}0 & \color{red}0 & \color{red}0 \ \color{green}0 & \color{green}{S_2} & \color{green}0 & \color{green}0 \ \color{blue}0 & \color{blue}0 & \color{blue}{S_3} & \color{blue}0 \ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} 右乘向量 \begin{bmatrix} x \ y \ z \ w \end{bmatrix}$
缩放
learnopengl-cn 中使用的矩阵都是 4×4 的矩阵,也就是以下
⎣⎢⎢⎢⎡S10000S20000S300001⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡xyzw⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡S1⋅xS2⋅yS3⋅z1⋅w⎦⎥⎥⎥⎤
这样除了对角线(左上到右下的对角线)其余都是零的矩阵,就叫缩放矩阵(对角线也可以有 0)
⎣⎢⎡S1S2S3⎦⎥⎤ 就是缩放向量
上面我们定义了一个 4 维向量在 3d 空间里的缩放,将 x 方向放大(缩小)为原来的S1 倍,将 y 方向放大(缩小)为原来的 S2 倍,将 z 方向放大(缩小)为原来的 S3 倍
这个用 4 维向量表示 3 维向量的方式,涉及到一个概念叫 齐次坐标!!!建议不了解的可以学一下,后面我们或许还会提到
但其实如果你使用 3×3 的矩阵,道理也是一样的
⎣⎢⎡S1000S2000S3⎦⎥⎤⋅⎣⎢⎡xyz⎦⎥⎤=⎣⎢⎡S1⋅xS2⋅yS3⋅z⎦⎥⎤
后面我依旧使用 4 维矩阵和 4 维向量
位移
⎣⎢⎢⎢⎡100001000010TxTyTz1⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡x+Txy+Tyz+Tz1⎦⎥⎥⎥⎤
⎣⎢⎡TxTyTz⎦⎥⎤ 就是位移向量
位移(Translation)是在原始向量的基础上加上另一个向量从而获得一个在不同位置的新向量的过程,从而在位移向量基础上移动了原始向量。
齐次坐标(Homogeneous Coordinates)
一个向量 vec4(x,y,z,w) ,向量的w分量也叫齐次坐标。
w 分量通常是 1.0。
使用齐次坐标有几点好处:它允许我们在3D向量上进行位移(如果没有w分量我们是不能位移向量的)。
如果一个向量的齐次坐标是 0,这个坐标就是方向向量(Direction Vector),因为w坐标是0,这个向量就不能位移。
旋转
首先,绕哪个轴旋转,哪个轴不动
⎣⎢⎢⎢⎡10000cosθsinθ00−sinθcosθ00001⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡xcosθ⋅y−sinθ⋅zsinθ⋅y+cosθ⋅z1⎦⎥⎥⎥⎤
⎣⎢⎢⎢⎡cosθ0−sinθ00100sinθ0cosθ00001⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡cosθ⋅x+sinθ⋅zy−sinθ⋅x+cosθ⋅z1⎦⎥⎥⎥⎤
⎣⎢⎢⎢⎡cosθsinθ00−sinθcosθ0000100001⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡cosθ⋅x−sinθ⋅ysinθ⋅x+cosθ⋅yz1⎦⎥⎥⎥⎤
绕 x 轴旋转 90° 的例子
⎣⎢⎢⎢⎡100000−100−1000001⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡abc1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡a−c−b1⎦⎥⎥⎥⎤
万向节死锁
利用旋转矩阵我们可以把任意位置向量沿一个单位旋转轴进行旋转。
也可以将多个矩阵复合,比如先沿着x轴旋转再沿着y轴旋转。但是这会很快导致一个问题——万向节死锁(Gimbal Lock)。
万向节死锁的原因是坐标轴会跟着图形一起旋转,而并非一直呈现为标准的坐标轴样子
推荐在 B站 或者 YouTube 搜索一些关于万向节死锁的视频,会帮助你理解这个问题产生的原因
在这里我们不会讨论它的细节,但是对于3D空间中的旋转,一个更好的模型是沿着任意的一个轴,比如单位向量 (0.662,0.2,0.7222) 旋转,而不是对一系列旋转矩阵进行复合。
这样的一个(超级麻烦的)矩阵是存在的,见下面这个公式,其中 (Rx,Ry,Rz) 代表任意旋转轴:
⎣⎢⎢⎢⎡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⎦⎥⎥⎥⎤
以下是 learnopengl-cn 对于万向节死锁的解释
在数学上讨论如何生成这样的矩阵仍然超出了本节内容。
但是记住,即使这样一个矩阵也不能完全解决万向节死锁问题(尽管会极大地避免)。
避免万向节死锁的真正解决方案是使用四元数(Quaternion),它不仅更安全,而且计算会更有效率。四元数可能会在后面的教程中讨论。
对四元数的理解会用到非常多的数学知识。如果你想了解四元数与3D旋转之间的关系,可以来阅读我的教程。如果你对万向节死锁的概念仍不是那么清楚,可以来阅读我教程的Bonus章节。
现在3Blue1Brown也已经开始了一个四元数的视频系列,他采用球极平面投影(Stereographic Projection)的方式将四元数投影到3D空间,同样有助于理解四元数的概念(仍在更新中):https://www.youtube.com/watch?v=d4EgbgTm0Bg
矩阵组合变换
假设一个向量,我们希望将其缩放2倍,然后位移 (1,2,3) 个单位
Trans⋅Scale=⎣⎢⎢⎢⎡1000010000101231⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡2000020000200001⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡2000020000201231⎦⎥⎥⎥⎤
当矩阵相乘时我们先写位移再写缩放变换的。
矩阵乘法是不遵守交换律的,这意味着它们的顺序很重要。
当矩阵相乘时,在最右边的矩阵是第一个与向量相乘的,所以你应该从右向左读这个乘法。
建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会互相影响。
比如,如果你先位移再缩放,位移的向量也会同样被缩放(译注:比如向某方向移动2米,2米也许会被缩放成1米)!
现在我们验证一下,这个矩阵的效果
⎣⎢⎢⎢⎡2000020000201231⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡2x+12y+22z+31⎦⎥⎥⎥⎤
可以看到,他确实放大了两倍,并且平移了 (1,2,3)
实践
glm 库
我们需要一个库 glm
相信你非常容易地就猜出来了,这是 gl-math 的缩写
我们就是通过这个库来实现各种矩阵运算
glm 下载地址
glm-0.9.9+
GLM库从0.9.9版本起,默认会将矩阵类型初始化为一个零矩阵(所有元素均为0),而不是单位矩阵(对角元素为1,其它元素为0)。
如果你使用的是0.9.9或0.9.9以上的版本,你需要将所有的矩阵初始化改为 glm::mat4 mat = glm::mat4(1.0f)。
如果你使用 kotlin 编写 OpenGL 代码,也可以用 glm for kotlin
我们需要的GLM的大多数功能都可以从下面这3个头文件中找到:
1 2 3
| #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/type_ptr.hpp>
|
位移实践
我们以 向量(1,0,0) 位移(1,1,0)为例,并且简单说明一下,glm 的使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f); std::cout << "位移前向量 (" << vec.x << "," << vec.y << "," << vec.z << ")" << std::endl;
glm::mat4 transMat4 = glm::mat4(1.0f);
glm::vec3 transVec3 = glm::vec3(1.0f, 1.0f, 0.0f);
transMat4 = glm::translate(transMat4, transVec3); std::cout << "位移矩阵:\n"; printMatrix(transMat4);
vec = transMat4 * vec; std::cout << "位移后向量 (" << vec.x << "," << vec.y << "," << vec.z << ")" << std::endl;
|
1 2 3 4 5 6 7 8 9
| void printMatrix(const glm::mat4 &matrix) { const float* pMatrix = glm::value_ptr(matrix); for (int i = 0; i < 4; ++i) { for (int j = 0; j < 4; ++j) { std::cout << pMatrix[i + j * 4] << ' '; } std::cout << std::endl; } }
|
1 2 3 4 5 6 7
| 位移前向量 (1,0,0) 位移矩阵: 1 0 0 1 0 1 0 1 0 0 1 0 0 0 0 1 位移后向量 (2,1,0)
|
transMat4 是单位矩阵
glm::translate(transMat4, transVec3) 用来生成位移矩阵
然后向量左乘位移矩阵,实现向量的位移
缩放实践
1 2 3 4 5 6 7 8 9 10 11 12 13
| glm::vec4 vec(1.0f, 1.0f, 0.0f, 1.0f); std::cout << "缩放前向量 (" << vec.x << "," << vec.y << "," << vec.z << ")" << std::endl;
glm::mat4 transMat4 = glm::mat4(1.0f);
glm::vec3 scaleVec3 = glm::vec3(0.5f, 0.8f, 0.0);
transMat4 = glm::scale(transMat4, scaleVec3); std::cout << "缩放矩阵:\n"; printMatrix(transMat4);
vec = transMat4 * vec; std::cout << "缩放后向量 (" << vec.x << "," << vec.y << "," << vec.z << ")\n";
|
1 2 3 4 5 6 7
| 缩放前向量 (1,1,1) 缩放矩阵: 0.5 0 0 0 0 0.8 0 0 0 0 2 0 0 0 0 1 缩放后向量 (0.5,0.8,2)
|
transMat4 是单位矩阵
glm::translate(transMat4, transVec3) 用来生成缩放矩阵
然后向量左乘缩放矩阵,实现向量的缩放
旋转实践
1 2 3 4 5 6 7 8 9 10 11 12 13
| glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f); std::cout << "旋转前向量 (" << vec.x << "," << vec.y << "," << vec.z << ")" << std::endl;
glm::mat4 transMat4 = glm::mat4(1.0f);
glm::vec3 transVec3 = glm::vec3(0.0f, 0.0f, 1.0f);
transMat4 = glm::rotate(transMat4, 0.523598333333333f, transVec3); std::cout << "旋转矩阵:\n"; printMatrix(transMat4);
vec = transMat4 * vec; std::cout << "旋转后向量 (" << vec.x << "," << vec.y << "," << vec.z << ")" << std::endl;
|
1 2 3 4 5 6 7
| 旋转前向量 (1,0,0) 旋转矩阵: 0.866026 -0.5 0 0 0.5 0.866026 0 0 0 0 1 0 0 0 0 1 旋转后向量 (0.866026,0.5,0)
|
transMat4 是单位矩阵
glm::rotate(transMat4, transVec3) 用来生成缩放矩阵
然后向量左乘位移矩阵,实现向量的旋转
opengl