探索 OpenGL 音视频渲染技术(7):变换

这个系列文章我们来介绍一位海外工程师如何探索 OpenGL 音视频渲染技术,对于想要开始学习音视频技术的朋友,这些文章是份不错的入门资料,这是第 7 篇:OpenGL 变换。

—— 来自公众号“关键帧Keyframe”的分享

现在我们已经知道如何创建对象、为它们着色,甚至使用纹理给它们一个详细的外观,但它们仍然是静态的,这使得它们不太有趣。我们可以通过每帧更改顶点并重新配置缓冲区来让它们移动,但这很繁琐且耗费大量处理能力。有更简单的方法可以变换对象,那就是使用(多个)矩阵对象。这并不意味着我们要讨论功夫和一个巨大的数字人工世界。

矩阵是一种非常强大的数学结构,乍一看很吓人,但一旦你习惯了它们,它们会非常有用。在讨论矩阵时,我们不得不稍微深入向量的一些数学知识。对于更喜欢数学的读者,我会发布额外的资源供进一步阅读。

然而,为了完全理解变换,我们首先必须更深入地讨论向量,然后才能讨论矩阵。本章的重点是为你提供我们稍后需要的主题的基本数学背景。如果这些主题很难,请尽量理解它们,并在需要时回到本章复习概念。

1、向量

在最基本的定义中,向量是方向,仅此而已。向量具有方向和大小(也称为强度或长度)。你可以将向量想象成藏宝图上的方向:“向左走 10 步,现在向北走 3 步,然后向右走 5 步”;这里的“左”是方向,“10 步”是向量的大小。藏宝图的方向因此包含 3 个向量。向量可以有任意维度,但我们通常处理 2 到 4 维。如果一个向量是二维的,它代表平面上的一个方向(想象一下 2D 图表);当它有三维时,它可以代表 3D 世界中的任何方向。

下图显示了 3 个向量,每个向量在 2D 图表中以 (x, y)形式表示为箭头。由于在 2D 中显示向量更直观(而不是 3D),你可以将 2D 向量视为 z 坐标为 0的 3D 向量。由于向量表示方向,因此向量的起点不会改变其值。在下图中,我们可以看到向量 ˉv 和 ˉw 是相等的,即使它们的起点不同:

探索 OpenGL 音视频渲染技术(7):变换

数学家通常喜欢用带有小横线的字符符号来描述向量,比如 ˉv。此外,在公式中显示向量时,它们通常显示为:

ˉv=(xyz)

由于向量被指定为方向,有时很难将它们可视化为位置。如果我们想将向量可视化为位置,我们可以想象方向向量的起点是 (0,0,0),然后指向某个方向,从而指定一个点,使其成为一个位置向量(我们也可以指定一个不同的起点,然后说:“这个向量从这个起点指向空间中的那个点”)。位置向量 (3,5)然后将指向图表上的 (3,5),起点为 (0,0)。因此,使用向量我们可以描述 2D 和 3D 空间中的方向和位置。

与普通数字一样,我们也可以在向量上定义几种操作(你已经见过其中一些)。

2、标量向量运算

标量是一个单独的数字。当将向量与标量相加/相减/相乘或相除时,我们只需将向量的每个元素与标量相加/相减/相乘或相除。对于加法,它看起来是这样的:

(123)+x→(123)+(xxx)=(1+x2+x3+x)

其中 + 可以是 +、−、⋅ 或 ÷,⋅ 是乘法运算符。

3、向量取反

取反一个向量会得到一个方向相反的向量。指向东北方向的向量取反后将指向西南方向。要取反一个向量,我们在每个分量前加上负号(也可以将其表示为标量-向量乘法,标量值为 -1):

−ˉv=−(vxvyvz)=(−vx−vy−vz)

4、加减法

两个向量的加法定义为逐分量加法,即一个向量的每个分量与另一个向量的相同分量相加:

ˉv=(123),ˉk=(456)→ˉv+ˉk=(1+42+53+6)=(579)

视觉上,这在向量 v=(4,2)和 k=(1,2)上看起来像这样,其中第二个向量加在第一个向量的末端以找到结果向量的终点(首尾相连法):

探索 OpenGL 音视频渲染技术(7):变换

与普通的加减法一样,向量减法与加法相同,只是第二个向量被取反:

ˉv=(123),ˉk=(456)→ˉv+−ˉk=(1+(−4)2+(−5)3+(−6))=(−3−3−3)

从两个向量中减去另一个向量会得到一个向量,该向量是两个向量所指向位置的差。这在某些情况下非常有用,例如我们需要获取两个点之间的差向量。

探索 OpenGL 音视频渲染技术(7):变换

5、长度

要获取向量的长度/大小,我们使用你可能在数学课上记得的毕达哥拉斯定理。当你将向量的个别 x和 y分量可视化为三角形的两边时,向量形成一个三角形:

探索 OpenGL 音视频渲染技术(7):变换

由于已知两边 (x, y)的长度,我们希望使用毕达哥拉斯定理计算倾斜边 ˉv 的长度:

||ˉv||=√x2+y2

其中 ||ˉv|| 表示向量 ˉv 的长度。这很容易扩展到 3D,只需在方程中加上 z2。

在这种情况下,向量 (4, 2)的长度等于:

||ˉv||=√42+22=√16+4=√20=4.47

即 4.47

还有一种特殊的向量类型,我们称之为单位向量。单位向量有一个额外的属性,即其长度正好为 1。我们可以通过将向量的每个分量除以其长度来从任何向量计算单位向量 ˆn:

ˆn=ˉv||ˉv||

我们称此过程为归一化一个向量。单位向量显示时头上有一个小尖帽,通常更容易处理,特别是当我们只关心它们的方向时(如果改变向量的长度,方向不会改变)。

6、向量-向量乘法

乘以两个向量是一种有点奇怪的情况。普通乘法在向量上没有真正定义,因为它没有视觉意义,但我们有两种特定情况可以选择:一种是点积,表示为 ˉv⋅ˉk,另一种是叉积,表示为 ˉv×ˉk。

6.1、点积

两个向量的点积等于它们长度的标量乘积乘以它们之间角度的余弦。如果这听起来很混乱,请查看其公式:

ˉv⋅ˉk=||ˉv||⋅||ˉk||⋅cosθ

其中它们之间的角度表示为 theta (θ)。为什么这很有趣呢?想象一下,如果 ˉv 和 ˉk 是单位向量,它们的长度将等于 1。这实际上将公式简化为:

ˆv⋅ˆk=1⋅1⋅cosθ=cosθ

现在点积表示两个向量之间的角度。你可能记得,当角度为 90 度时,余弦或 cos 函数变为 0,当角度为 0 时变为 1。这使我们能够轻松地使用点积测试两个向量是否正交或平行(正交意味着向量彼此垂直)。如果你想了解更多关于 sin或 cos函数的信息,我建议观看可汗学院关于基本三角函数的视频。

你也可以计算两个非单位向量之间的角度,但需要将两个向量的长度从结果中除掉,以得到 cosθ。

那么我们如何计算点积呢?点积是逐分量乘法,其中我们将结果相加。以下是两个单位向量的点积示例(你可以验证它们的长度都正好是 1):

(0.6−0.80)⋅(010)=(0.6∗0)+(−0.8∗1)+(0∗0)=−0.8

为了计算这两个单位向量之间的角度,我们使用反余弦函数 cos−1,结果为 143.1度。我们现在实际上计算了这两个向量之间的角度。点积在后面的光照计算中非常有用。

6.2、叉积

叉积仅在 3D 空间中定义,接受两个非平行向量作为输入,生成一个与两个输入向量都正交的第三个向量。如果两个输入向量彼此正交,叉积将得到三个正交向量;这将在后面的章节中非常有用。下图显示了 3D 空间中的样子:

探索 OpenGL 音视频渲染技术(7):变换

与其他操作不同,叉积在不深入线性代数的情况下并不直观,因此最好记住公式,你就会没事的(或者不记住,可能也没关系)。下面是你将看到的两个正交向量 A 和 B 的叉积:

(AxAyAz)×(BxByBz)=(Ay⋅Bz−Az⋅ByAz⋅Bx−Ax⋅BzAx⋅By−Ay⋅Bx)

如你所见,这似乎没有意义。然而,如果你按照这些步骤操作,你会得到另一个与输入向量正交的向量。

7、矩阵

现在我们已经讨论了几乎所有关于向量的内容,是时候进入矩阵了! 矩阵是一个矩形的数字、符号和/或数学表达式的数组。矩阵中的每个单独项称为矩阵的元素。下面显示了一个 2×3 矩阵的示例:[123456] 矩阵通过 (i,j)进行索引,其中 i是行,j是列,这就是为什么上述矩阵被称为 2×3 矩阵(3 列和 2 行,也称为矩阵的维度)。这与你在 2D 图表中索引 (x,y)时的习惯相反。要获取值 4,我们将索引为 (2,1)(第二行,第一列)。

矩阵基本上就是这些,只是数学表达式的矩形数组。它们确实有一组非常不错的数学属性,与向量一样,我们可以在矩阵上定义几种操作,即:加法、减法和乘法。

8、加减法

两个矩阵之间的加法和减法是逐元素进行的。因此,我们熟悉的普通数字的相同规则也适用于此,但操作的是两个矩阵中相同索引的元素。这意味着加法和减法仅对相同维度的矩阵定义。一个 3×2 矩阵和一个 2×3 矩阵(或一个 3×3 矩阵和一个 4×4 矩阵)不能相加或相减。让我们看看两个 2×2 矩阵的加法是如何工作的:

[1234]+[5678]=[1+52+63+74+8]=[681012]

矩阵减法的规则相同:

[4216]−[2401]=[4−22−41−06−1]=[2−215]

9、矩阵-标量乘积

矩阵-标量乘积将矩阵的每个元素乘以一个标量。以下示例说明了乘法:

2⋅[1234]=[2⋅12⋅22⋅32⋅4]=[2468]

现在也明白了为什么这些单独的数字被称为标量。标量基本上将矩阵的所有元素按其值进行缩放。在前面的例子中,所有元素都按 2进行了缩放。

到目前为止,所有的情况都不算太复杂。直到我们开始矩阵-矩阵乘法。

10、矩阵-矩阵乘法

乘以矩阵不一定复杂,但要习惯它可能有点难。矩阵乘法基本上是遵循一组预定义规则的乘法。不过有几个限制:

  1. 只有当左边矩阵的列数等于右边矩阵的行数时,才能相乘两个矩阵。
  2. 矩阵乘法不满足交换律,即 A⋅B≠B⋅A。

让我们从两个 2x2矩阵的乘法开始:

[1234]⋅[5678]=[1⋅5+2⋅71⋅6+2⋅83⋅5+4⋅73⋅6+4⋅8]=[19224350]

现在你可能正在试图弄清楚到底发生了什么?矩阵乘法是左边矩阵的行与右边矩阵的列的普通乘法和加法的组合。让我们尝试用以下图像来讨论这一点:

探索 OpenGL 音视频渲染技术(7):变换

我们首先取左边矩阵的上一行,然后取右边矩阵的一列。我们选择的行和列决定了结果 2x2矩阵中的哪个值我们将计算。如果我们取左边矩阵的第一行,结果值将出现在结果矩阵的第一行,然后我们选择一列,如果是第一列,结果值将出现在结果矩阵的第一列。这就是红路径的情况。要计算右下角的结果,我们取第一个矩阵的下一行和第二个矩阵的最右一列。

要计算结果值,我们将行和列的每个元素分别用普通乘法相乘,然后将各个乘法的结果相加,得到我们的结果。现在也明白了为什么其中一个要求是左边矩阵的列数和右边矩阵的行数必须相等,否则我们无法完成操作!

结果矩阵的维度为 (n,m),其中 n是左边矩阵的行数,m是右边矩阵的列数。

如果你在脑海中难以想象乘法,请继续用手计算,并在遇到困难时回到这个页面。久而久之,矩阵乘法会变得得心应手。

让我们用一个更大的例子来结束矩阵-矩阵乘法的讨论。试着用颜色来想象这个模式。作为一个有用的练习,试着自己计算乘法的结果,然后与结果矩阵进行比较(一旦你尝试用手进行矩阵乘法,你会很快掌握它们)。

[420081010]⋅[421204942]=[4⋅4+2⋅2+0⋅94⋅2+2⋅0+0⋅44⋅1+2⋅4+0⋅20⋅4+8⋅2+1⋅90⋅2+8⋅0+1⋅40⋅1+8⋅4+1⋅20⋅4+1⋅2+0⋅90⋅2+1⋅0+0⋅40⋅1+1⋅4+0⋅2]=[2081225434204]

如你所见,矩阵-矩阵乘法是一个相当繁琐的过程,非常容易出错(这就是我们通常让计算机来做这件事的原因),而且当矩阵变大时问题会很快变得复杂。如果你还想了解更多,并对矩阵的一些数学属性感到好奇,我强烈建议你看看可汗学院关于矩阵的视频。

无论如何,现在我们已经知道如何将矩阵相乘,我们可以开始进入正题了。

11、矩阵-向量乘法

到目前为止,我们已经对向量有了相当多的了解。我们用它们来表示位置、颜色,甚至纹理坐标。让我们进一步深入探讨,并告诉你,向量基本上是一个 Nx1矩阵,其中 N是向量的分量数量(也称为 N 维向量)。如果你仔细想想,这是很有道理的。向量和矩阵一样,都是数字数组,但只有 1 列。那么,这些新信息如何帮助我们呢?好吧,如果我们有一个 MxN矩阵,我们可以将这个矩阵与我们的 Nx1向量相乘,因为矩阵的列数等于向量的行数,因此定义了矩阵乘法。

但我们为什么要关心能否将矩阵与向量相乘呢?嗯,事实证明有很多有趣的 2D/3D 变换可以放在矩阵中,然后将该矩阵与向量相乘以变换该向量。如果你仍然有点困惑,让我们从几个例子开始,你很快就会明白我们的意思。

12、单位矩阵

在 OpenGL 中,我们通常使用 4x4变换矩阵,原因有几个,其中之一是大多数向量的大小为 4。我们能想到的最简单的变换矩阵是单位矩阵。单位矩阵是一个 NxN矩阵,除了对角线上的元素外,其他元素都是 0。正如你将看到的,这个变换矩阵完全不会改变向量:

[1000010000100001]⋅[1234]=[1⋅11⋅21⋅31⋅4]=[1234]

向量完全未受影响。从乘法规则来看,这变得显而易见:结果的第一个元素是矩阵第一行的每个元素与向量的每个元素相乘。由于每一行的元素除了第一个都是 0,我们得到:1⋅1+0⋅2+0⋅3+0⋅4=1,其他三个元素也是如此。

你可能想知道一个不进行变换的变换矩阵有什么用?单位矩阵通常是生成其他变换矩阵的起点,如果我们更深入线性代数,它是一个用于证明定理和解线性方程的非常有用的矩阵。

13、缩放

当我们缩放一个向量时,我们是在保持其方向不变的情况下增加箭头的长度。由于我们在 2D 或 3D 空间中工作,我们可以通过 2 或 3 个缩放变量来定义缩放,每个变量缩放一个轴(xy或 z)。

让我们尝试缩放向量 ˉv=(3,2)。我们将在 x 轴上缩放向量 0.5,使其宽度减半;在 y 轴上缩放向量 2,使其高度加倍。让我们看看如果我们用 ˉs 缩放向量 (0.5,2)会是什么样子:

请记住,OpenGL 通常在 3D 空间中操作,因此对于这个 2D 情况,我们可以将 z 轴的缩放设置为 1,使其保持不变。我们刚刚执行的缩放操作是一个非均匀缩放,因为每个轴的缩放因子不同。如果所有轴的缩放因子相同,它将被称为均匀缩放。

让我们开始构建一个为我们执行缩放的变换矩阵。我们从单位矩阵中看到,对角线上的每个元素都与相应的向量元素相乘。如果我们把单位矩阵中的 1改为 3,那么我们实际上将每个向量元素乘以 3,从而有效地将向量均匀缩放 3 倍。如果我们用 (S1,S2,S3) 表示缩放变量,我们可以在任何向量 (x,y,z) 上定义一个缩放矩阵为:

[S10000S20000S300001]⋅(xyz1)=(S1⋅xS2⋅yS3⋅z1)

注意我们保持第四个缩放值为 1w分量用于其他目的,我们将在后面看到。

14、平移

平移是将另一个向量加到原始向量上,返回一个新向量,从而改变其位置,即根据平移向量移动向量。我们已经讨论过向量加法,所以这应该不陌生。

与缩放矩阵一样,4×4 矩阵中有几个位置可以用于执行某些操作,对于平移来说,这些位置是第四列的前三个值。如果我们用 (Tx,Ty,Tz) 表示平移向量,我们可以通过以下方式定义平移矩阵:

[100Tx010Ty001Tz0001]⋅(xyz1)=(x+Txy+Tyz+Tz1)

这是因为所有平移值都乘以向量的 w列,并加到向量的原始值上(记住矩阵乘法规则)。这在 3×3 矩阵中是不可能的。

齐次坐标

向量的 w分量也称为齐次坐标。 要从齐次向量中获取 3D 向量,我们将 xy和 z坐标除以其 w坐标。我们通常不会注意到这一点,因为 w分量大多数时候是 1.0。使用齐次坐标有几个优点:它允许我们对 3D 向量进行矩阵平移(没有 w分量我们无法平移向量),在下一章中我们将使用 w值创建 3D 透视效果。此外,当齐次坐标等于 0时,向量特别被称为方向向量,因为 w坐标为 0的向量无法平移。通过平移矩阵,我们可以在任意的 3 个轴方向(xyz)上移动对象,使其成为我们变换工具包中非常有用的变换矩阵。

15、旋转

最后几个变换相对容易理解和在 2D 或 3D 空间中可视化,但旋转有点棘手。如果你想确切地了解这些矩阵是如何构造的,我建议你观看可汗学院线性代数视频中的旋转部分。

首先,让我们定义什么是向量的旋转。2D 或 3D 中的旋转用一个角度表示。角度可以是度数或弧度,其中一整圈是 360 度或 2π 弧度。我更喜欢用度数解释旋转,因为我们通常更习惯它们。

大多数旋转函数需要以弧度为单位的角度,但幸运的是,度数很容易转换为弧度:

角度(度数)= 角度(弧度)× (180 / π)

角度(弧度)= 角度(度数)× (π / 180)

其中 π约等于 3.14159265359

旋转半圈相当于旋转 360/2 = 180 度,向右旋转 1/5 圈意味着我们向右旋转 360/5 = 72 度。这对于一个基本的 2D 向量来说是演示这一点,其中 ˉv 向右(顺时针)旋转 72 度从 ˉk 开始:

3D 中的旋转需要用一个角度和一个旋转轴来指定。指定的角度将围绕给定的旋转轴旋转对象。试着想象一下,通过不断看着一个单一的旋转轴,将你的头旋转某个角度(例如,在 3D 世界中旋转 2D 向量时,我们将旋转轴设置为 z 轴)。

使用三角函数可以将向量转换为新的旋转向量,这通常是通过巧妙地结合 正弦和 余弦函数(通常简写为 sin和 cos)来完成的。关于旋转矩阵是如何生成的讨论超出了本章的范围。

对于 3D 空间中的每个单位轴,定义了一个旋转矩阵,其中角度表示为 theta 符号 θ。

绕 X 轴旋转:

[10000cosθ−sinθ00sinθcosθ00001]⋅(xyz1)=(xcosθ⋅y−sinθ⋅zsinθ⋅y+cosθ⋅z1)

绕 Y 轴旋转:

[cosθ0sinθ00100−sinθ0cosθ00001]⋅(xyz1)=(cosθ⋅x+sinθ⋅zy−sinθ⋅x+cosθ⋅z1)

绕 Z 轴旋转:

[cosθ−sinθ00sinθcosθ0000100001]⋅(xyz1)=(cosθ⋅x−sinθ⋅ysinθ⋅x+cosθ⋅yz1)

使用旋转矩阵,我们可以围绕 3D 空间中的三个单位轴之一变换位置向量。要围绕任意 3D 轴旋转,我们可以通过首先围绕 X 轴旋转,然后 Y 轴,最后 Z 轴来组合所有三个矩阵。然而,这很快引入了一个称为万向节锁的问题。我们不会讨论细节,但一个更好的解决方案是围绕任意单位轴(例如 (0.662,0.2,0.722),注意这是一个单位向量)立即旋转,而不是组合旋转矩阵。这样一个(冗长的)矩阵存在,并且如下所示,其中 (Rx,Ry,Rz) 是任意旋转轴:

[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]

关于如何生成这样一个矩阵的数学讨论超出了本章的范围。记住,即使这个矩阵也不能完全防止万向节锁(尽管这已经很难了)。要真正防止万向节锁,我们需要用四元数来表示旋转,这不仅更安全,而且计算上更友好。然而,关于四元数的讨论超出了本章的范围。

16、矩阵组合

使用矩阵进行变换的真正强大之处在于,我们可以通过矩阵-矩阵乘法将多个变换组合在一个矩阵中。让我们看看是否可以生成一个组合几个变换的变换矩阵。假设我们有一个向量 (x,y,z),我们希望将其缩放 2 倍,然后平移 (1,2,3)。我们需要一个平移矩阵和一个缩放矩阵来执行所需的步骤。结果变换矩阵将如下所示:

Trans.Scale=[1001010200130001].[2000020000200001]=[2001020200230001]

请注意,我们首先执行平移,然后执行缩放变换,当乘法矩阵时。矩阵乘法不满足交换律,这意味着它们的顺序很重要。当乘法矩阵时,最右边的矩阵首先与向量相乘,因此你应该从右到左阅读乘法。建议先进行缩放操作,然后是旋转,最后是平移,否则它们可能会相互影响。例如,如果你先平移再缩放,平移向量也会被缩放!

在我们的向量上运行最终变换矩阵的结果如下:

[2001020200230001].[xyz1]=[2x+12y+22z+31]

太好了!向量首先被缩放两倍,然后平移 (1,2,3)

17、实际应用

现在我们已经解释了变换背后的理论,是时候看看如何实际应用这些知识了。OpenGL 本身没有内置的矩阵或向量知识,所以我们必须定义自己的数学类和函数。在本书中,我们更愿意抽象掉所有这些微小的数学细节,简单地使用现成的数学库。幸运的是,有一个易于使用且专为 OpenGL 定制的数学库,名为 GLM。

18、GLM

GLM 代表 Open GLMathematics,是一个仅包含头文件的库,这意味着我们只需要包含适当的头文件即可;不需要链接和编译。 GLM 可以从其网站下载。将头文件的根目录复制到你的 includes文件夹中,我们就可以开始了。

我们所需的所有 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将度数转换为弧度。注意,我们围绕的旋转轴应该是一个单位向量,因此如果你不是围绕 X、Y 或 Z 轴旋转,请确保先归一化向量。因为我们传递矩阵到 GLM 的每个函数,GLM 自动将矩阵相乘,结果是一个组合所有变换的变换矩阵。

下一个大问题是:我们如何将变换矩阵传递到着色器?我们简要提到过,GLSL 也有一个 mat4类型。因此,我们将修改顶点着色器以接受一个 mat4uniform 变量,并将位置向量乘以矩阵 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, aTexCoord.y);
}

GLSL 还有 mat2和 mat3类型,允许对矩阵类型进行类似 swizzling 的操作。所有上述数学运算(如标量-矩阵乘法、矩阵-向量乘法和矩阵-矩阵乘法)在矩阵类型上都是允许的。每当使用特殊的矩阵运算时,我们会确保解释发生了什么。

我们在将位置向量传递给 gl_Position之前将其乘以变换矩阵。我们的容器现在应该缩小了一半,并旋转了 90度(向左倾斜)。我们仍然需要将变换矩阵传递到着色器:

unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));

我们首先查询 uniform 变量的位置,然后使用带有 Matrix4fv后缀的 glUniform将矩阵数据发送到着色器。第一个参数应该是我们已经熟悉的 uniform 位置。第二个参数告诉 OpenGL 我们想要发送多少个矩阵,这里是 1。第三个参数询问我们是否想要转置矩阵,即交换列和行。OpenGL 开发者通常使用一种称为列主序的内部矩阵布局,这也是 GLM 的默认矩阵布局,因此不需要转置矩阵;我们可以保持为 GL_FALSE。最后一个参数是实际的矩阵数据,但由于 GLM 存储矩阵数据的方式并不总是符合 OpenGL 的期望,因此我们首先使用 GLM 的内置函数 value_ptr转换数据。

我们创建了一个变换矩阵,在顶点着色器中声明了一个 uniform,并将矩阵发送到着色器,在那里我们变换顶点坐标。结果应该如下所示:

太好了!我们的容器确实向左倾斜并且缩小了一半,因此变换成功了。让我们更进一步,看看是否可以随时间旋转容器,为了好玩,我们还将容器重新定位到窗口的右下角。 为了随时间旋转容器,我们必须在渲染循环中更新变换矩阵,因为它需要每帧更新。我们使用 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)旋转容器,一旦旋转完成,我们将旋转后的容器平移到屏幕的右下角。记住,实际的变换顺序应该反过来读:尽管在代码中我们首先平移,然后旋转,但实际的变换首先应用旋转,然后应用平移。理解这些变换组合以及它们如何应用于对象是困难的。尝试使用这样的变换进行实验,你将很快掌握它。

如果你做对了,你应该得到以下结果:

就这样。一个经过平移的容器随时间旋转,所有这些都由一个变换矩阵完成!现在你可以看到为什么矩阵在图形领域是如此强大的结构。我们可以定义无限数量的变换,并将它们全部组合在一个矩阵中,我们可以根据需要多次重用该矩阵。在顶点着色器中使用这样的变换可以节省我们重新定义顶点数据的麻烦,并且也节省了一些处理时间,因为我们不需要一直重新发送数据(这相当慢);我们所要做的就是更新变换 uniform。

如果你没有得到正确的结果或在其他地方卡住了,请查看源代码和更新的着色器类。

在下一章中,我们将讨论如何使用矩阵为顶点定义不同的坐标空间。这将是我们在 3D 图形中迈出的第一步!

19、进一步阅读

  • 线性代数的本质:由 Grant Sanderson 制作的关于变换和线性代数基础数学的优秀视频教程系列。
  • 矩阵乘法 XYZ:查看这个展示矩阵乘法的出色交互式可视化工具。尝试其中的一些操作应该有助于巩固你的理解。

20、练习

  • 使用容器上的最后一个变换,尝试通过首先旋转然后平移来切换顺序。看看会发生什么,并尝试解释为什么会这样:解决方案。
  • 尝试使用另一个 glDrawElements调用绘制第二个容器,但仅使用变换将其放置在不同位置。确保这个第二个容器位于窗口的左上角,并且不是旋转,而是随时间缩放(这里使用 sin函数很有用;注意,使用 sin会导致对象在应用负缩放时反转):解决方案。

音视频方向学习、求职,欢迎加入我们的星球

丰富的音视频知识、面试题、技术方案干货分享,还可以进行面试辅导

探索 OpenGL 音视频渲染技术(7):变换

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论