前情回顾
按照惯例,先回顾一下之前的内容,在之前的博文里面,已经介绍了如何使用OpenGL在VS2022开发环境里面,创建一个图形,如何使用着色器给图形添加颜色,如何添加纹理。这些内容都可以访问我的博客文章进行回顾。
[!TIP]
我的部分博客文章的列表:
从零开始写游戏引擎(开发环境VS2022+OpenGL)之三 HelloWorld窗口的实现,代码与解释,步步教会系列_写个游戏引擎-优快云博客
从零开始写游戏引擎(开发环境VS2022+OpenGL)之四 GPU简单作图的实现,代码与解释,保姆包教会系列-优快云博客
从零开始写游戏引擎(开发环境VS2022+OpenGL)之五 如何编写自己的着色器,保姆包教会系列-优快云博客
从零开始写3D游戏引擎(开发环境VS2022+OpenGL)之六 如何给自己的图形添加纹理,包含代码与解释的保姆包教会系列-优快云博客
下面我要先介绍一下这篇博文要实现的技术目标。
本文的技术目标
很简单实现图像的翻转,移动,让图像动起来。
实现的源码可以在下面的提示里面找到
[!TIP]
旋转的效果如下:
还有沿特点位置旋转
接下来就是让多个目标同时旋转以及缩放
实现的源码可以在下面的提示里面找到
[!TIP]
当然在这些目标统统实现之前,我们需要讲解一下这些代码实现背后的数学。当然,如果您觉得下面的数学原理部分实在是没有必要,可以直接点击目录,参看后面的技术实现环节。但是,作为一个有志青年,中年或者闲的无聊的老年(?)数学还有有点用的,当然,这里指的是矩阵与向量(大学线性代数部分或者高中的线性代数部分,原理很简单,形式逻辑而已)
真可惜,读者们可以跳过,但是我作为文章的撰写者却必须写,呜呜呜。
转换与数学(Transformations)
我们现在知道如何创建对象,为它们上色和/或使用纹理赋予它们详细的外观,但它们仍然不是那么有趣,因为它们都是静态对象。我们可以尝试通过改变它们的顶点和每帧重新配置它们的缓冲区来让它们移动,但这很麻烦,而且需要花费相当多的处理能力。有更好的方法来转换一个对象,那就是使用(多个)矩阵对象。这并不意味着我们要谈论功夫和一个巨大的数字人工世界。
矩阵是非常强大的数学结构,一开始看起来很可怕,但一旦你习惯了它们,它们就会证明是非常有用的。在讨论矩阵时,我们必须稍微深入一些数学,对于更倾向于数学的读者,我将发布额外的参考资料以供进一步阅读。
然而,为了完全理解变换,我们首先必须在讨论矩阵之前更深入地研究向量。本章的重点是给你一个基本的数学背景,我们将在后面的主题中用到。如果这些主题很难,试着尽可能多地理解它们,然后在需要的时候回到本章复习这些概念。
向量
在最基本的定义中,向量就是方向,仅此而已。矢量有方向和大小(也称为强度或长度)。你可以把矢量想象成藏宝图上的方向:“向左走10步,现在向北走3步,向右走5步”;这里的“左”是方向,“10步”是矢量的大小。因此,藏宝图的方向包含3个向量。向量的维数可以是任意的,但是我们通常处理的维数是2到4。如果一个矢量有2个维度,它代表平面上的一个方向(想想2D图形),当它有3个维度时,它可以代表3D世界中的任何方向。
下面你会看到3个向量,每个向量在2D图中用( x ‾ \overline{x} x,$ \overline{y} )作为箭头表示。因为在 2 D (而不是 3 D )中显示向量更直观,所以可以将 2 D 向量视为 z 坐标为 0 的 3 D 向量。由于矢量表示方向,矢量的原点不会改变其值。在下面的图中我们可以看到向量 )作为箭头表示。因为在2D(而不是3D)中显示向量更直观,所以可以将2D向量视为z坐标为0的3D向量。由于矢量表示方向,矢量的原点不会改变其值。在下面的图中我们可以看到向量 )作为箭头表示。因为在2D(而不是3D)中显示向量更直观,所以可以将2D向量视为z坐标为0的3D向量。由于矢量表示方向,矢量的原点不会改变其值。在下面的图中我们可以看到向量\overline{v} 和 和 和\overline{w}$是相等的,尽管它们的起源不同:
当描述向量时,数学家通常更喜欢将向量描述为在其头上有一个像
v
‾
\overline{v}
v这样的小条的字符符号. 同样,在公式中显示矢量时,它们通常如下所示:
v
‾
=
(
x
y
z
)
\overline{v}=\begin{pmatrix} \color {red}{x} \\ \color {green}{y} \\ \color {blue}{z} \end{pmatrix}
v=
xyz
因为向量被指定为方向,所以有时很难把它们想象成位置。如果我们想把向量想象成位置,我们可以想象方向向量的原点是(0,0,0),然后指向指定点的某个方向,使其成为位置向量(我们也可以指定一个不同的原点,然后说:“这个向量从这个原点指向空间中的那个点”)。然后,位置向量(3,5)将指向原点为(0,0)的图形上的(3,5)。使用矢量,我们可以在二维和三维空间中描述方向和位置。
就像处理普通数字一样,我们也可以在向量上定义一些操作(其中一些你已经看到了)。
标量向量运算
标量是一个单位数。当向量与标量相加、减、乘或除时,我们只需将向量的每个元素与标量相加、减、乘或除即可。对于加法,它看起来像这样:
(
1
2
3
)
+
x
→
(
1
2
3
)
+
(
x
x
x
)
=
(
1
+
x
2
+
x
3
+
x
)
\begin{pmatrix} \color {red}{1} \\ \color {green}{2} \\ \color {blue}{3} \end{pmatrix}+x \rightarrow \begin{pmatrix} \color {red}{1} \\ \color {green}{2} \\ \color {blue}{3} \end{pmatrix}+\begin{pmatrix} \color {red}{x} \\ \color {green}{x} \\ \color {blue}{x} \end{pmatrix}=\begin{pmatrix} \color {red}{1+x} \\ \color {green}{2+x} \\ \color {blue}{3+x} \end{pmatrix}
123
+x→
123
+
xxx
=
1+x2+x3+x
这里加号可以被减号,乘号以及除号代替。
向量取负运算
减去一个向量会得到一个方向相反的向量。指向东北的矢量在否定后指向西南。要求一个向量的负数,我们在每个分量上加一个负号(你也可以把它表示为标量值为-1的标量向量乘法):
− v ‾ = − ( v x v y v z ) = ( − v x − v y − v z ) -\overline{v} =-\begin{pmatrix} \color {red}{v_{x}} \\ \color {green}{v_{y}} \\ \color {blue}{v_{z}} \end{pmatrix} =\begin{pmatrix} \color {red}{-v_{x}} \\ \color {green}{-v_{y}} \\ \color {blue}{-v_{z}} \end{pmatrix} −v=− vxvyvz = −vx−vy−vz
加法与减法
两个向量的加法被定义为分量加法,即一个向量的每个分量加到另一个向量的相同分量上,如下所示:
v
‾
=
(
1
2
3
)
,
k
‾
=
(
4
5
6
)
→
v
‾
+
k
‾
=
(
1
+
4
2
+
5
3
+
6
)
=
(
5
7
9
)
\overline{v}= \begin{pmatrix} \color {red}{1} \\ \color {green}{2} \\ \color {blue}{3} \end{pmatrix},\overline{k}=\begin{pmatrix} \color {red}{4} \\ \color {green}{5} \\ \color {blue}{6} \end{pmatrix} \rightarrow \overline{v}+\overline{k} =\begin{pmatrix} \color {red}{1+4} \\ \color {green}{2+5} \\ \color {blue}{3+6} \end{pmatrix}= \begin{pmatrix} \color {red}{5} \\ \color {green}{7} \\ \color {blue}{9} \end{pmatrix}
v=
123
,k=
456
→v+k=
1+42+53+6
=
579
从视觉上看,在向量v=(4,2)和k=(1,2)上看起来是这样的,其中第二个向量被添加到第一个向量的末端顶部,以找到结果向量的端点(头到尾方法):
就像普通的加法和减法一样,向量减法与第二个负向量的加法相同:
v
‾
=
(
1
2
3
)
,
k
‾
=
(
4
5
6
)
→
v
‾
+
(
−
k
‾
)
=
(
1
+
(
−
4
)
2
+
(
−
5
)
3
+
(
−
6
)
)
=
(
−
3
−
3
−
3
)
\overline{v}=\begin{pmatrix} \color {red}{1} \\ \color {green}{2} \\ \color {blue}{3} \end{pmatrix},\overline{k}=\begin{pmatrix} \color {red}{4} \\ \color {green}{5} \\ \color {blue}{6} \end{pmatrix} \rightarrow \overline{v}+(-\overline{k}) = \begin{pmatrix} \color {red}{1+(-4)} \\ \color {green}{2+(-5)} \\ \color {blue}{3+(-6)}\end{pmatrix}= \begin{pmatrix} \color {red}{-3} \\ \color {green}{-3} \\ \color {blue}{-3} \end{pmatrix}
v=
123
,k=
456
→v+(−k)=
1+(−4)2+(−5)3+(−6)
=
−3−3−3
两个向量相减得到的向量是两个向量所指向的位置之差。这在某些情况下是有用的当我们需要检索一个向量表示两点之间的差。
长度
为了获取矢量的长度/大小,我们使用你可能在数学课上记得的毕达哥拉斯定理。当你把一个向量的x和y分量想象成三角形的两条边时,它就形成了一个三角形:
因为两条边(x, y)的长度是已知的我们想知道倾斜边的长度
v
‾
\overline{v}
v我们可以用勾股定理/毕达哥拉斯定理来计算:
∣
∣
v
‾
∣
∣
=
x
2
+
y
2
\lvert\lvert\overline{v}\rvert\rvert=\sqrt{\color {red}{x}^2 \color {black}+\color {green}y^2}
∣∣v∣∣=x2+y2
这里,
∣
∣
v
‾
∣
∣
\lvert\lvert\overline{v}\rvert\rvert
∣∣v∣∣表示为向量
v
‾
\overline{v}
v的长度。 这很容易通过添加
z
2
z^2
z2到方程,从而使2D扩展到3D的情况。
还有一种特殊的向量,我们称之为单位向量。单位向量有一个额外的性质就是它的长度正好是1。我们可以计算一个单位向量
n
^
\hat{n}
n^。方法很简单,让任意向量的每个分量除以它的长度:
n
^
=
v
‾
∣
∣
v
‾
∣
∣
\hat{n} =\frac{\overline{v}}{\lvert\lvert\overline{v}\rvert\rvert}
n^=∣∣v∣∣v
我们称它为向量的规范化。单位向量在显示时带有一个小屋顶,通常更容易处理,特别是当我们只关心它们的方向时(如果我们改变向量的长度,方向不会改变)。
向量向量乘法
两个向量相乘是一个有点奇怪的例子。普通的乘法并不是真正定义在向量上的,因为它没有视觉意义,但是我们有两种具体的情况可以在乘法中选择:
一种是点积(点乘),表示为 v ‾ \overline{v} v⋅ k ‾ \overline{k} k
另一个是叉乘,记作 v ‾ \overline{v} v× k ‾ \overline{k} k
点积(点乘)
两个向量的点积等于它们长度的标量积乘以它们夹角的余弦。如果这听起来令人困惑,看看它的公式:
v
‾
⋅
k
‾
=
∣
∣
v
‾
∣
∣
⋅
∣
∣
k
‾
∣
∣
⋅
c
o
s
(
θ
)
\overline{v}⋅\overline{k}= {\lvert\lvert\overline{v}\rvert\rvert}⋅{\lvert\lvert\overline{k}\rvert\rvert}⋅cos(\theta)
v⋅k=∣∣v∣∣⋅∣∣k∣∣⋅cos(θ)
它们之间的夹角表示为θ。为什么这很有趣?想象一下如果
v
‾
\overline{v}
v和
k
‾
\overline{k}
k,这两个变量都是单位向量,那么它们的长度就等于1。这将有效地将公式简化为:
v
‾
⋅
k
‾
=
∣
∣
v
‾
∣
∣
⋅
∣
∣
k
‾
∣
∣
⋅
c
o
s
(
θ
)
=
1
⋅
1
⋅
c
o
s
(
θ
)
=
c
o
s
(
θ
)
\overline{v}⋅\overline{k} ={\lvert\lvert\overline{v}\rvert\rvert}⋅{\lvert\lvert\overline{k}\rvert\rvert}⋅cos(\theta)=1⋅1⋅cos(\theta)=cos(\theta)
v⋅k=∣∣v∣∣⋅∣∣k∣∣⋅cos(θ)=1⋅1⋅cos(θ)=cos(θ)
那么如何计算点积呢?点积是一种分式乘法,我们把结果加在一起。它看起来像这样有两个单位向量(你可以验证它们的长度都是1):
(
0.6
−
0.8
0
)
⋅
(
0
1
0
)
=
(
0.6
∗
0
)
+
(
−
0.8
∗
1
)
+
(
0
∗
0
)
=
−
0.8
\begin{pmatrix}\color {red}{0.6} \\\color {green}{-0.8} \\\color {blue}{0}\end{pmatrix} ⋅ \begin{pmatrix}\color {red}{0} \\\color {green}{1} \\\color {blue}{0}\end{pmatrix} =\color {red}{(0.6*0)}\color {black}+\color {green}{(-0.8*1)}\color {black}+\color {blue}{(0*0)}=-0.8
0.6−0.80
⋅
010
=(0.6∗0)+(−0.8∗1)+(0∗0)=−0.8
为了计算这两个单位向量之间的度数,我们使用反余弦函数,结果是143.1度。我们现在有效地计算了这两个向量之间的夹角。点积在以后的光照计算中被证明是非常有用的。
叉乘
叉乘仅在三维空间中定义,并以两个不平行的向量作为输入,并产生与两个输入向量正交的第三个向量。如果两个输入向量彼此正交,叉乘将得到3个正交向量;这将在接下来的章节中证明是有用的。下图显示了在3D空间中的效果:
不像其他运算,叉乘不是很直观,除非深入研究线性代数,所以最好记住公式,你就会很好(或者不记住,你可能也会很好)。下面你会看到两个正交向量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
)
\begin{pmatrix}\color {red}{A_x} \\\color {green}{A_y} \\\color {blue}{A_z}\end{pmatrix}×\begin{pmatrix}\color {red}{B_x} \\\color {green}{B_y} \\\color {blue}{B_z}\end{pmatrix}= \begin{pmatrix}\color {green}{A_y}*\color {blue}{B_z}- \color {blue}{A_z}*\color {green}{B_y}\\ \color {blue}{A_z}*\color {red}{B_x}-\color {red}{A_x}* \color {blue}{B_z}\\ \color {red}{A_x}*\color {green}{B_y}-\color {green}{A_y}*\color {red}{B_x}\end{pmatrix}
AxAyAz
×
BxByBz
=
Ay∗Bz−Az∗ByAz∗Bx−Ax∗BzAx∗By−Ay∗Bx
正如你所看到的,这似乎没有意义。然而,如果你只是按照这些步骤,你会得到另一个与输入向量正交的向量。
矩阵
现在我们已经讨论了几乎所有的向量,是时候进入矩阵了!矩阵是由数字、符号和/或数学表达式组成的矩形数组。矩阵中的每个单独的项称为矩阵的一个元素。2x3矩阵的例子如下所示:
[
1
2
3
4
5
6
]
\begin{bmatrix}1 &&2 &&3 \\4&&5&&6\end{bmatrix}
[142536]
矩阵以(i,j)为索引,其中i是行,j是列,这就是为什么上面的矩阵被称为2x3矩阵(3列2行,也称为矩阵的维度)。这与将2D图索引为(x,y)时使用的方法相反。为了检索值4,我们将其索引为(2,1)(第二行,第一列)。
矩阵基本上就是数学表达式的矩形数组。它们确实有一组很好的数学性质,就像向量一样,我们可以在矩阵上定义几种运算,即:加法,减法和乘法。
矩阵的加减法
两个矩阵之间的加法和减法是在每个元素的基础上完成的。所以同样的一般规则适用于我们熟悉的普通数字,但是对两个矩阵中具有相同索引的元素进行处理。这意味着加法和减法只适用于相同维数的矩阵。一个3x2矩阵和一个2x3矩阵(或者一个3x3矩阵和一个4x4矩阵)不能加或减在一起。让我们看看两个2x2矩阵的加法是如何工作的:
[
1
2
3
4
]
+
[
5
6
7
8
]
=
[
1
+
5
2
+
6
3
+
7
4
+
8
]
=
[
6
8
10
12
]
\begin{bmatrix}\color{red}1 &&\color{red}2 \\\color{green}3&&\color{green}4\end{bmatrix} +\begin{bmatrix}\color{red}5 &&\color{red}6 \\\color{green}7&&\color{green}8\end{bmatrix} =\begin{bmatrix}\color{red}1+5 &&\color{red}2+6 \\\color{green}3+7&&\color{green}4+8\end{bmatrix} =\begin{bmatrix}\color{red}6 &&\color{red}8 \\\color{green}10&&\color{green}12\end{bmatrix}
[1324]+[5768]=[1+53+72+64+8]=[610812]
同样的法则也适用于矩阵的减法。
[
1
2
3
4
]
−
[
5
6
7
8
]
=
[
1
−
5
2
−
6
3
−
7
4
−
8
]
=
[
−
4
−
4
−
4
−
4
]
\begin{bmatrix}\color{red}1 &&\color{red}2 \\\color{green}3&&\color{green}4\end{bmatrix} -\begin{bmatrix}\color{red}5 &&\color{red}6 \\\color{green}7&&\color{green}8\end{bmatrix} =\begin{bmatrix}\color{red}1-5 &&\color{red}2-6 \\\color{green}3-7&&\color{green}4-8\end{bmatrix} =\begin{bmatrix}\color{red}-4 &&\color{red}-4 \\\color{green}-4&&\color{green}-4\end{bmatrix}
[1324]−[5768]=[1−53−72−64−8]=[−4−4−4−4]
矩阵与标量的乘积
矩阵-标量乘积是矩阵的每个元素乘以一个标量。下面的例子说明了乘法:
2
∗
[
1
2
3
4
]
=
[
2
∗
1
2
∗
2
2
∗
3
2
∗
4
]
=
[
2
4
6
8
]
2*\begin{bmatrix}\color{red}1 &&\color{red}2 \\\color{green}3&&\color{green}4\end{bmatrix}=\begin{bmatrix}\color{red}2*1 &&\color{red}2*2 \\\color{green}2*3&&\color{green}2*4\end{bmatrix} =\begin{bmatrix}\color{red}2 &&\color{red}4 \\\color{green}6&&\color{green}8\end{bmatrix}
2∗[1324]=[2∗12∗32∗22∗4]=[2648]
这也解释了为什么这些单独的数被称为标量。标量基本上是用它的值对矩阵的所有元素进行缩放。在前面的示例中,所有元素都按2缩放。
到目前为止都很好,我们所有的案例都不是很复杂。也就是说,直到我们开始矩阵-矩阵乘法。
矩阵-矩阵乘法
矩阵的乘法不一定复杂,但是很难掌握。矩阵乘法基本上是指在乘法时遵循一组预定义的规则。不过也有一些限制:
[!WARNING]
只有当左边矩阵的列数等于右边矩阵的行数时,两个矩阵才能相乘。
矩阵乘法是不可交换的即A·B≠B·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
]
\begin{bmatrix}\color{red}1 &&\color{red}2 \\\color{green}3&&\color{green}4\end{bmatrix} * \begin{bmatrix}\color{blue}5 &&\color{purple}6 \\\color{blue}7&&\color{purple}8\end{bmatrix} =\begin{bmatrix}\color{red}1*\color{blue}5+\color{red}2*\color{blue}7 &&\color{red}1*\color{purple}6+\color{red}2*\color{purple}8 \\\color{green}3*\color{blue}5+\color{green}4*\color{blue}7&&\color{green}3*\color{purple}6+\color{green}4*\color{purple}8\end{bmatrix} =\begin{bmatrix}19 &&22 \\43&&50\end{bmatrix}
[1324]∗[5768]=[1∗5+2∗73∗5+4∗71∗6+2∗83∗6+4∗8]=[19432250]
我们首先取左边矩阵的上一行然后取右边矩阵的一列。我们选择的行和列决定了我们要计算的2x2矩阵的输出值。如果我们取左边矩阵的第一行结果值将会在结果矩阵的第一行结束,然后我们选择一列如果它是第一列结果值将会在结果矩阵的第一列结束。这正是红色通道的情况。为了计算右下角的结果,我们取第一个矩阵的最下面一行和第二个矩阵的最右边一列。
为了计算结果值,我们使用普通乘法将行和列的第一个元素相乘,我们对第二个、第三个、第四个元素做同样的处理。然后将单个乘法的结果加起来,我们就得到了结果。现在还有一个要求是左矩阵的列和右矩阵的行大小相等,否则我们无法完成运算!
结果是一个维数为(n,m)的矩阵,其中n等于左边矩阵的行数m等于右边矩阵的列数。
如果你在脑海中想象乘法有困难,也不要担心。只要继续尝试手工计算,遇到困难就回到这一页。随着时间的推移,矩阵乘法成为你的第二天性。
让我们用一个更大的例子来结束对矩阵-矩阵乘法的讨论。试着用颜色把图案形象化。作为一个有用的练习,看看您是否可以得出自己的乘法答案,然后将它们与得到的矩阵进行比较(一旦您尝试手工进行矩阵乘法,您将很快掌握它们)。
正如你所看到的,矩阵-矩阵乘法是一个相当麻烦的过程,而且很容易出错(这就是为什么我们通常让计算机来做这个),当矩阵变大时,这个问题很快就会出现。
矩阵向量乘法
到目前为止,我们已经有了相当多的向量。我们用它们来表示位置、颜色甚至纹理坐标。让我们再深入一点,告诉您向量基本上是Nx1矩阵,其中N是向量的组件数(也称为N维向量)。如果你仔细想想,这很有道理。向量就像矩阵——一个数字数组,但只有一列。那么,这条新信息对我们有什么帮助呢?如果我们有一个MxN矩阵我们可以将这个矩阵与Nx1向量相乘,因为矩阵的列数等于向量的行数,因此矩阵乘法是有定义的。
但是为什么我们要关心矩阵和向量的乘法呢?好吧,碰巧有很多有趣的2D/3D变换我们可以放在一个矩阵里,把这个矩阵和一个向量相乘然后变换那个向量。如果你仍然有点困惑,让我们从几个例子开始,你很快就会明白我们的意思。
单位矩阵
在OpenGL中,我们通常使用4x4变换矩阵,其中一个原因是大多数向量的大小都是4。我们能想到的最简单的变换矩阵是单位矩阵。单位矩阵是一个除了对角线上只有0的NxN矩阵。正如你将看到的,这个变换矩阵留下了一个完全无损的向量:
[
1
0
0
0
0
1
0
0
0
0
1
0
0
0
0
1
]
∗
[
1
∗
1
1
∗
2
1
∗
3
1
∗
4
]
=
[
1
2
3
4
]
\begin{bmatrix}\color{red}1 &&\color{red}0&&\color{red}0&&\color{red}0 \\ \color{green}0&&\color{green}1&&\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} * \begin{bmatrix}\color{red}1*\color{black}1 \\ \color{green}1*\color{black}2 \\ \color{blue}1*\color{black}3\\\color{purple}1*\color{black}4\end{bmatrix} =\begin{bmatrix}1 \\ 2 \\ 3\\4\end{bmatrix}
1000010000100001
∗
1∗11∗21∗31∗4
=
1234
矢量完全不受影响。这从乘法规则中变得很明显:第一个结果元素是矩阵第一行的每个单独元素与向量的每个元素相乘。
缩放
当我们缩放一个向量的时候我们就是把箭头的长度按我们想要缩放的量增加,同时保持它的方向不变。由于我们在二维或三维空间中工作,我们可以通过2或3个缩放变量的向量来定义缩放,每个变量缩放一个轴(x, y或z)。
我们试着缩放向量 v ‾ \overline{v} v=(3,2). 我们将沿着x轴将向量缩放0.5,从而使其狭窄两倍;我们沿着y轴将向量乘以2,使它的高度是原来的两倍。我们看看如果把向量乘以(0.5,2)等于 s ‾ \overline{s} s会是什么样子:
请记住,OpenGL通常在3D空间中运行,所以对于这个2D情况,我们可以将z轴比例设置为1,使其不受损害。我们刚刚执行的缩放操作是一个非均匀缩放,因为每个轴的缩放因子并不相同。如果标量在所有轴上都相等,则称为均匀标度。
让我们开始建立一个变换矩阵来为我们做缩放。我们从单位矩阵中看到每个对角线元素都与它对应的向量元素相乘。如果我们把单位矩阵中的1变成3呢?在这种情况下,我们将把每个向量元素乘以一个值3,从而有效地将向量均匀地缩放为3。如果我们将缩放变量表示为( S 1 \color{red}S_1 S1, S 2 \color{green}S_2 S2, S 3 \color{blue}S_3 S3),我们可以在任意向量(x,y,z)上定义缩放矩阵为:
[ S 1 0 0 0 0 S 2 0 0 0 0 S 3 0 0 0 0 1 ] ∗ [ x y z 1 ] = [ x ∗ S 1 y ∗ S 2 z ∗ S 3 1 ] \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\\1\end{bmatrix} =\begin{bmatrix}x*\color{red}S_1 \\ y*\color{green}S_2 \\ z*\color{blue}S_3\\1\end{bmatrix} S10000S20000S300001 ∗ xyz1 = x∗S1y∗S2z∗S31
[!NOTE]
注意,我们保持第4个缩放值为1。我们稍后会看到,w分量还有其他用途。
平移
平移是在原向量上添加另一个向量以返回具有不同位置的新向量的过程,从而基于平移向量移动向量。我们已经讨论过向量加法了,所以这个应该不是太新。
就像缩放矩阵一样在一个4 × 4的矩阵中有几个位置我们可以用它们来执行特定的运算对于平移来说,它们是第四列的前三个值。如果我们把平移向量表示为( T 1 \color{red}T_1 T1, T 2 \color{green}T_2 T2, T 3 \color{blue}T_3 T3)我们可以定义平移矩阵为:
[
1
0
0
T
1
0
1
0
T
2
0
0
1
T
3
0
0
0
1
]
∗
[
x
y
z
1
]
=
[
x
+
S
1
y
+
S
2
z
+
S
3
1
]
\begin{bmatrix}\color{red}1 &&\color{red}0&&\color{red}0&&\color{red}T_1 \\ \color{green}0&&\color{green}1&&\color{green}0&&\color{green}T_2 \\ \color{blue}0&&\color{blue}0&&\color{blue}1&&\color{blue}T_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}x+\color{red}S_1 \\ y+\color{green}S_2 \\ z+\color{blue}S_3\\1\end{bmatrix}
100001000010T1T2T31
∗
xyz1
=
x+S1y+S2z+S31
这是有效的,因为所有的转换值都乘以向量的w列,并添加到向量的原始值(记住矩阵乘法规则)。这在3 × 3矩阵中是不可能的。
齐次坐标
向量的w分量也被称为齐次坐标。为了从齐次向量得到三维向量,我们用x, y, z坐标除以它的w坐标。我们通常不会注意到这一点,因为w分量在大多数情况下是1.0。使用齐次坐标有几个优点:它允许我们在3D向量上做矩阵平移(没有w分量我们不能平移向量),在下一章我们将使用w值来创建3D透视图。
同样,当齐次坐标等于0时,这个向量被称为方向向量,因为w坐标为0的向量不能被平移。
有了平移矩阵,我们可以在任意3轴方向(x, y, z)上移动物体,使它成为我们的变换工具箱中非常有用的变换矩阵。
转动
最后几个转换在2D或3D空间中相对容易理解和可视化,但旋转有点棘手。首先我们定义一下向量的旋转是什么。2D或3D中的旋转用角度表示。一个角的单位可以是度或弧度一个圆有360度或2弧度。我更喜欢用角度来解释旋转,因为我们通常更习惯它们。
[!TIP]
大多数旋转函数需要以弧度为单位的角度,但幸运的是,角度很容易转换为弧度:
度角=弧度角* (180 / PI)
弧度角=度角* (PI / 180)
其中PI等于(四舍五入)3.14159265359。
旋转半个圆将我们旋转360/2 = 180度,向右旋转1/5意味着向右旋转360/5 = 72度。这是一个基本的2D向量 k ‾ \overline{k} k,向右旋转72度,或者说顺时针,就得到了 v ‾ \overline{v} v向量。
3D中的旋转是用角度和旋转轴指定的。指定的角度将沿着给定的旋转轴旋转对象。试着把你的头旋转一定程度,同时不断地向下看一个单一的旋转轴,以此来想象这一点。例如,当在3D世界中旋转2D向量时,我们将旋转轴设置为z轴(尝试将其可视化)。
利用三角函数可以将给定角度的向量变换为新旋转的向量。这通常是通过正弦和余弦函数(通常缩写为sin和cos)的巧妙组合来完成的。关于旋转矩阵如何生成的讨论超出了本章的范围。为三维空间中的每个单位轴定义一个旋转矩阵,其中角度表示为θ.
利用旋转矩阵,我们可以围绕三个单位轴之一变换位置向量。为了绕任意一个3D轴旋转,我们可以结合这三个轴,首先绕x轴旋转,然后是Y轴,然后是Z轴。然而,这很快就引入了一个叫做“万向锁”的问题。我们不会讨论细节,但更好的解决方案是立即围绕任意单位轴旋转,例如(0.662,0.2,0.722)(注意这是一个单位向量),而不是组合旋转矩阵。这样的(冗长)矩阵是存在的,下面给出了( R 1 \color{red}R_1 R1, R 2 \color{green}R_2 R2, R 3 \color{blue}R_3 R3)为任意旋转轴的旋转矩阵:
关于生成这样一个矩阵的数学讨论超出了本章的范围。请记住,即使是这个矩阵也不能完全防止万向锁(尽管它变得更加困难)。为了真正防止万向锁,我们必须使用四元数来表示旋转,这不仅更安全,而且在计算上也更友好。然而,四元数的讨论超出了本章的范围。
好了,数学部分就到此为止了。
终于到了本文的技术实现环节
如何在技术上实现图像的缩放,旋转以及平移
既然我们已经解释了转换背后的所有理论,现在是时候看看我们如何实际利用这些知识来发挥我们的优势了。OpenGL没有任何形式的矩阵或矢量知识内置,所以我们必须定义我们自己的数学类和函数。在这本书中,我们宁愿从所有微小的数学细节中抽象出来,简单地使用预制的数学库。幸运的是,有一个易于使用且为opengl量身定制的数学库,称为GLM。
GLM
GLM代表OpenGL Mathematics,是一个仅限头文件的库,这意味着我们只需要包含适当的头文件,我们就完成了;不需要链接和编译。GLM可以从他们的网站下载。将头文件的根目录复制到include文件夹中,开始吧。
我们需要的GLM的大部分功能可以在3个头文件中找到,我们将包括如下:
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
让我们看看是否可以很好地利用变换知识,将向量(1,0,0)平移为向量(1,1,0)(注意,我们将其定义为glm::vec4,其齐次坐标集为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;
我们首先使用GLM的内置向量类定义一个名为vec的向量。接下来,我们定义一个mat4,并通过将矩阵的对角线初始化为1.0来显式地将其初始化为单位矩阵;如果我们不将它初始化为单位矩阵,那么矩阵将是一个空矩阵(所有元素都为0),并且所有后续的矩阵操作也将以空矩阵结束。
下一步是通过将我们的单位矩阵传递给glm::translate函数来创建一个转换矩阵,以及一个平移向量(然后将给定的矩阵与一个平移矩阵相乘,并返回结果矩阵)。
然后我们将向量乘以变换矩阵并输出结果。如果我们还记得矩阵平移是如何工作的,那么结果向量应该是(1+1,0+1,0+0)也就是(2,1,0)这段代码输出210,因此翻译矩阵完成了它的工作。
让我们做一些更有趣的事情,缩放和旋转上一章的容器对象:
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));
首先,我们在每个轴上缩放容器0.5,然后围绕z轴旋转容器90度。GLM期望用弧度表示角度,所以我们使用GLM::radians将角度转换为弧度。注意,纹理矩形在XY平面上,所以我们想要绕z轴旋转。记住,我们旋转的轴应该是一个单位向量,所以如果你不绕X, Y, Z轴旋转,一定要先标准化这个向量。因为我们将矩阵传递给GLM的每个函数,所以GLM会自动将矩阵相乘,得到一个组合了所有变换的变换矩阵。
下一个大问题是:我们如何将变换矩阵转化为着色器?我们在前面简短地提到,GLSL也有mat4类型。所以我们将调整顶点着色器以接受mat4Uniforms变量,并将位置向量乘以矩阵统一:
#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, aTexCoord.y);
}
[!TIP]
GLSL还有mat2和mat3类型,它们允许像矢量一样的混合操作。前面提到的所有数学运算(如标量-矩阵乘法、矩阵-向量乘法和矩阵-矩阵乘法)在矩阵类型上都是允许的。在使用特殊矩阵运算的地方,我们一定会解释发生了什么。
在将其传递给gl_Position之前,我们添加了统一的位置向量并将其与变换矩阵相乘。我们的容器现在应该是原来的两倍小,并旋转90度(向左倾斜)。我们仍然需要将变换矩阵传递给着色器:
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
我们首先查询统一变量的位置,然后使用后缀为Matrix4fv的glUniform将矩阵数据发送给着色器。第一个参数现在应该很熟悉了,它是制服的位置。第二个参数告诉OpenGL我们想要发送多少个矩阵,也就是1。第三个参数问我们是否要转置矩阵,也就是交换列和行。OpenGL开发人员经常使用称为列主排序的内部矩阵布局,这是GLM中的默认矩阵布局,因此不需要转置矩阵;我们可以把它设为GL_FALSE。最后一个参数是实际的矩阵数据,但是GLM存储矩阵数据的方式并不总是符合OpenGL的期望,所以我们首先用GLM的内置函数value_ptr转换数据。
我们的容器确实是向左倾斜的,并且是原来的两倍小,所以转换是成功的。让我们变得更时髦一点,看看我们是否可以随着时间旋转容器,为了好玩,我们还将容器重新定位在窗口的右下角。为了随时间旋转容器,我们必须在渲染循环中更新变换矩阵,因为它需要更新每一帧。我们使用GLFW的时间函数来获得一个随时间变化的角度:
glm::mat4 trans = glm::mat4(1.0f);
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
请记住,在前面的例子中,我们可以在任何地方声明变换矩阵,但是现在我们必须在每次迭代中创建它以不断更新旋转。这意味着我们必须在渲染循环的每次迭代中重新创建转换矩阵。通常在渲染场景时,我们有几个转换矩阵,每帧用新值重新创建。
在这里,我们首先围绕原点(0,0,0)旋转容器,一旦它被旋转,我们将其旋转版本转换到屏幕的右下角。记住,实际的转换顺序应该反过来读:即使在代码中我们先平移然后再旋转,实际的转换首先应用旋转然后再平移。理解所有这些转换组合以及它们如何应用于对象是很难理解的。尝试和实验像这样的转换,你会很快掌握它。
结果出来了。一个随时间旋转的转换容器,所有这些都是由一个变换矩阵完成的!现在您可以看到为什么矩阵是图形领域中如此强大的结构。我们可以定义无限数量的变换并将它们组合在一个矩阵中我们可以随时重用这个矩阵。在顶点着色器中使用这样的转换可以节省我们重新定义顶点数据的工作量,也节省了我们一些处理时间,因为我们不必一直重新发送数据(这是相当慢的);我们要做的就是更新变形服。
[!TIP]
所有的源代码都可以在这里找到:OpenGL如何实现图像平移转动的源代码C++资源-优快云文库
当然,我们也可以在上面做一些变换。
比如将两行位置调换,就像是这样:
int main()
{
[...]
while(!glfwWindowShouldClose(window))
{
[...]
// create transformations
glm::mat4 transform = glm::mat4(1.0f);
transform = glm::rotate(transform, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f)); // switched the order
transform = glm::translate(transform, glm::vec3(0.5f, -0.5f, 0.0f)); // switched the order
[...]
}
}
然后,你就会看到这样的效果:
当然,你也可以试试尝试用另一个调用glDrawElements来绘制第二个容器,但只使用转换将其放置在不同的位置。确保第二个容器放置在窗口的左上角,而不是旋转,它随时间缩放(使用sin函数在这里很有用;请注意,使用sin将导致对象在应用负比例时立即反转):
[!TIP]
注意了,这里所有的代码你都可以在这个链接找到:OpenGL如何实现图像平移转动的源代码C++资源-优快云文库
很好,今天的课程就到这里了,谢谢大家!!!
创作不易,希望大家多多点赞收藏,大家的支持就是我最大的动力!!!拜拜