Learn OpenGL with Qt——变换

如果你是中途开始学习本教程,即使你对OpenGL已经非常熟悉,请至少了解一下以下几个章节,因为Qt中提供了OpenGL的很多便捷操作,熟悉这些操作可以让我们在Qt中高效的使用OpenGL进行绘图。

创建OpenGL窗口

着色器程序已经着色器的创建

创建与加载纹理

Qt-OpenGL的几个优势:

  1. Qt内嵌了opengl的相关环境,不需要我们自己来搭建,这对小白来说是很友好的。
  2. Qt和opengl都具有优良的跨平台特性,使用Qt做opengl开发可谓是强强联合。
  3. Qt可以轻松的控制窗口的各种处理事件以及窗口属性。
  4. Qt提供了opengl函数的C++封装,使得opengl原来的C风格API可以通过C++的面向对象技术来实现。
  5. Qt提供了十分完善的官方文档,有助于我们掌握QtOpenGL的各种细节。

这个教程将完全使用Qt对openglAPI的C++封装,内容板块尽量与learnopengl保持一致。笔者会逐步的实现教程里的demo,尽可能的说明每一个操作细节。你可以在文章的右上角找到本节的索引目录,如果什么地方操作失败,你可以直接复制代码节点的代码,尝试运行一下,再对比一下自己的代码,看自己是否什么地方出问题了,如果还不能解决问题,可以在下方评论区留言,笔者看到一定第一时间解答。

笔者对openGL了解不是很深,如果什么地方存在问题,希望朋友们能够详细指出。

变换

尽管我们现在已经知道了如何创建一个物体、着色、加入纹理,给它们一些细节的表现,但因为它们都还是静态的物体,仍是不够有趣。我们可以尝试着在每一帧改变物体的顶点并且重配置缓冲区从而使它们移动,但这太繁琐了,而且会消耗很多的处理时间。我们现在有一个更好的解决方案,使用(多个)矩阵(Matrix)对象可以更好的变换(Transform)一个物体。当然,这并不是说我们会去讨论武术和数字虚拟世界(译注:Matrix同样也是电影「黑客帝国」的英文名,电影中人类生活在数字虚拟世界,主角会武术)。

矩阵是一种非常有用的数学工具,尽管听起来可能有些吓人,不过一旦你理解了它们后,它们会变得非常有用。在讨论矩阵的过程中,我们需要使用到一些数学知识。对于一些愿意多了解这些知识的读者,我会附加一些资源给你们阅读。

为了深入了解变换,我们首先要在讨论矩阵之前进一步了解一下向量。这一节的目标是让你拥有将来需要的最基础的数学背景知识。如果你发现这节十分困难,尽量尝试去理解它们,当你以后需要它们的时候回过头来复习这些概念。

向量

向量最基本的定义就是一个方向。或者更正式的说,向量有一个方向(Direction)和大小(Magnitude,也叫做强度或长度)。你可以把向量想像成一个藏宝图上的指示:“向左走10步,向北走3步,然后向右走5步”;“左”就是方向,“10步”就是向量的长度。那么这个藏宝图的指示一共有3个向量。向量可以在任意维度(Dimension)上,但是我们通常只使用2至4维。如果一个向量有2个维度,它表示一个平面的方向(想象一下2D的图像),当它有3个维度的时候它可以表达一个3D世界的方向。

下面你会看到3个向量,每个向量在2D图像中都用一个箭头(x, y)表示。我们在2D图片中展示这些向量,因为这样子会更直观一点。你可以把这些2D向量当做z坐标为0的3D向量。由于向量表示的是方向,起始于何处并不会改变它的值。下图我们可以看到向量v¯v¯和w¯w¯是相等的,尽管他们的起始点不同:

数学家喜欢在字母上面加一横表示向量,比如说向量v。当用在公式中时它们通常是这样的:

由于向量是一个方向,所以有些时候会很难形象地将它们用位置(Position)表示出来。为了让其更为直观,我们通常设定这个方向的原点为(0, 0, 0),然后指向一个方向,对应一个点,使其变为位置向量(Position Vector)(你也可以把起点设置为其他的点,然后说:这个向量从这个点起始指向另一个点)。比如说位置向量(3, 5)在图像中的起点会是(0, 0),并会指向(3, 5)。我们可以使用向量在2D或3D空间中表示方向位置.

和普通数字一样,我们也可以用向量进行多种运算(其中一些你可能已经看到过了)。

向量与标量运算

标量(Scalar)只是一个数字(或者说是仅有一个分量的向量)。当把一个向量加/减/乘/除一个标量,我们可以简单的把向量的每个分量分别进行该运算。对于加法来说会像这样:

其中的+可以是+,-,·或÷,其中·是乘号。注意-和÷运算时不能颠倒(标量-/÷向量),因为颠倒的运算是没有定义的。

向量取反

对一个向量取反(Negate)会将其方向逆转。一个指向东北的向量取反后就指向西南方向了。我们在一个向量的每个分量前加负号就可以实现取反了(或者说用-1数乘该向量):

向量加减

向量的加法可以被定义为是分量的(Component-wise)相加,即将一个向量中的每一个分量加上另一个向量的对应分量:

向量v = (4, 2)k = (1, 2)可以直观地表示为:

就像普通数字的加减一样,向量的减法等于加上第二个向量的相反向量:

两个向量的相减会得到这两个向量指向位置的差。这在我们想要获取两点的差会非常有用。

长度

我们使用勾股定理(Pythagoras Theorem)来获取向量的长度(Length)/大小(Magnitude)。如果你把向量的x与y分量画出来,该向量会和x与y分量为边形成一个三角形:

因为两条边(x和y)是已知的,如果希望知道斜边v¯的长度,我们可以直接通过勾股定理来计算:

表示向量v的长度,我们也可以加上z^2把这个公式拓展到三维空间。

例子中向量(4, 2)的长度等于:

结果是4.47。

有一个特殊类型的向量叫做单位向量(Unit Vector)。单位向量有一个特别的性质——它的长度是1。我们可以用任意向量的每个分量除以向量的长度得到它的单位向量n^:

我们把这种方法叫做一个向量的标准化(Normalizing)。单位向量头上有一个^样子的记号。通常单位向量会变得很有用,特别是在我们只关心方向不关心长度的时候(如果改变向量的长度,它的方向并不会改变)。

向量相乘

两个向量相乘是一种很奇怪的情况。普通的乘法在向量上是没有定义的,因为它在视觉上是没有意义的。但是在相乘的时候我们有两种特定情况可以选择:一个是点乘(Dot Product),记作 ,另一个是叉乘(Cross Product),记作

点乘

两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。可能听起来有点费解,我们来看一下公式:

它们之间的夹角记作θ。为什么这很有用?想象如果v和k都是单位向量,它们的长度会等于1。这样公式会有效简化成:

现在点积定义了两个向量的夹角。你也许记得90度的余弦值是0,0度的余弦值是1。使用点乘可以很容易测试两个向量是否正交(Orthogonal)或平行(正交意味着两个向量互为直角)。如果你想要了解更多关于正弦或余弦函数的知识,我推荐你看可汗学院的基础三角学视频。

所以,我们该如何计算点乘呢?点乘是通过将对应分量逐个相乘,然后再把所得积相加来计算的。两个单位向量的(你可以验证它们的长度都为1)点乘会像是这样:

要计算两个单位向量间的夹角,我们可以使用反余弦函数cos−1cos−1 ,可得结果是143.1度。现在我们很快就计算出了这两个向量的夹角。点乘会在计算光照的时候非常有用。

叉乘

叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量。如果输入的两个向量也是正交的,那么叉乘之后将会产生3个互相正交的向量。接下来的教程中这会非常有用。下面的图片展示了3D空间中叉乘的样子:

不同于其他运算,如果你没有钻研过线性代数,可能会觉得叉乘很反直觉,所以只记住公式就没问题啦(记不住也没问题)。下面你会看到两个正交向量A和B叉积:

是不是看起来毫无头绪?不过只要你按照步骤来了,你就能得到一个正交于两个输入向量的第三个向量。

矩阵

现在我们已经讨论了向量的全部内容,是时候看看矩阵了!简单来说矩阵就是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。下面是一个2×3矩阵的例子:

矩阵可以通过(i, j)进行索引,i是行,j是列,这就是上面的矩阵叫做2×3矩阵的原因(3列2行,也叫做矩阵的维度(Dimension))。这与你在索引2D图像时的(x, y)相反,获取4的索引是(2, 1)(第二行,第一列)(译注:如果是图像索引应该是(1, 2),先算列,再算行)。

矩阵基本也就是这些了,它就是一个矩形的数学表达式阵列。和向量一样,矩阵也有非常漂亮的数学属性。矩阵有几个运算,分别是:矩阵加法、减法和乘法。

矩阵的加减

矩阵与标量之间的加减定义如下:

标量值要加到矩阵的每一个元素上。矩阵与标量的减法也相似:

矩阵与矩阵之间的加减就是两个矩阵对应元素的加减运算,所以总体的规则和与标量运算是差不多的,只不过在相同索引下的元素才能进行运算。这也就是说加法和减法只对同维度的矩阵才是有定义的。一个3×2矩阵和一个2×3矩阵(或一个3×3矩阵与4×4矩阵)是不能进行加减的。我们看看两个2×2矩阵是怎样相加的:

同样的法则也适用于减法:

矩阵的数乘

和矩阵与标量的加减一样,矩阵与标量之间的乘法也是矩阵的每一个元素分别乘以该标量。下面的例子展示了乘法的过程:

现在我们也就能明白为什么这些单独的数字要叫做标量(Scalar)了。简单来说,标量就是用它的值缩放(Scale)矩阵的所有元素(译注:注意Scalar是由Scale + -ar演变过来的)。前面那个例子中,所有的元素都被放大了2倍。

到目前为止都还好,我们的例子都不复杂。不过矩阵与矩阵的乘法就不一样了。

矩阵相乘

矩阵之间的乘法不见得有多复杂,但的确很难让人适应。矩阵乘法基本上意味着遵照规定好的法则进行相乘。当然,相乘还有一些限制:

  1. 只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘。
  2. 矩阵相乘不遵守交换律(Commutative),也就是说A⋅B≠B⋅A

我们先看一个两个2×2矩阵相乘的例子:

现在你可能会在想了:天哪,刚刚到底发生了什么? 矩阵的乘法是一系列乘法和加法组合的结果,它使用到了左侧矩阵的行和右侧矩阵的列。我们可以看下面的图片:

Matrix Multiplication

我们首先把左侧矩阵的行和右侧矩阵的列拿出来。这些挑出来行和列将决定我们该计算结果2x2矩阵的哪个输出值。如果取的是左矩阵的第一行,输出值就会出现在结果矩阵的第一行。接下来再取一列,如果我们取的是右矩阵的第一列,最终值则会出现在结果矩阵的第一列。这正是红框里的情况。如果想计算结果矩阵右下角的值,我们要用第一个矩阵的第二行和第二个矩阵的第二列(译注:简单来说就是结果矩阵的元素的行取决于第一个矩阵,列取决于第二个矩阵)。

计算一项的结果值的方式是先计算左侧矩阵对应行和右侧矩阵对应列的第一个元素之积,然后是第二个,第三个,第四个等等,然后把所有的乘积相加,这就是结果了。现在我们就能解释为什么左侧矩阵的列数必须和右侧矩阵的行数相等了,如果不相等这一步的运算就无法完成了!

结果矩阵的维度是(n, m),n等于左侧矩阵的行数,m等于右侧矩阵的列数。

如果在脑子里想象出这一乘法有些困难,别担心。不断地动手计算,如果遇到困难再回头看这页的内容。随着时间流逝,矩阵乘法对你来说会变成很自然的事。

我们用一个更大的例子来结束对矩阵相乘的讨论。试着使用颜色来寻找规律。作为一个有用的练习,你可以试着自己解答一下这个乘法问题,再将你的结果和图中的这个进行对比(如果用笔计算,你很快就能掌握它们)。

可以看到,矩阵相乘非常繁琐而容易出错(这也是我们通常让计算机做这件事的原因),而且当矩阵变大以后很快就会出现问题。如果你仍然希望了解更多,或对矩阵的数学性质感到好奇,我强烈推荐你看看可汗学院的矩阵教程。

不管怎样,现在我们知道如何进行矩阵相乘了,我们可以开始学习好东西了。

矩阵与向量相乘

目前为止,通过这些教程我们已经相当了解向量了。我们用向量来表示位置,表示颜色,甚至是纹理坐标。让我们更深入了解一下向量,它其实就是一个N×1矩阵,N表示向量分量的个数(也叫N维(N-dimensional)向量)。如果你仔细思考一下就会明白。向量和矩阵一样都是一个数字序列,但它只有1列。那么,这个新的定义对我们有什么帮助呢?如果我们有一个M×N矩阵,我们可以用这个矩阵乘以我们的N×1向量,因为这个矩阵的列数等于向量的行数,所以它们就能相乘。

但是为什么我们会关心矩阵能否乘以一个向量?好吧,正巧,很多有趣的2D/3D变换都可以放在一个矩阵中,用这个矩阵乘以我们的向量将变换(Transform)这个向量。如果你仍然有些困惑,我们来看一些例子,你很快就能明白了。

单位矩阵

在OpenGL中,由于某些原因我们通常使用4×4的变换矩阵,而其中最重要的原因就是大部分的向量都是4分量的。我们能想到的最简单的变换矩阵就是单位矩阵(Identity Matrix)。单位矩阵是一个除了对角线以外都是0的N×N矩阵。在下式中可以看到,这种变换矩阵使一个向量完全不变:

向量看起来完全没变。从乘法法则来看就很容易理解来:第一个结果元素是矩阵的第一行的每个元素乘以向量的每个对应元素。因为每行的元素除了第一个都是0,可得:,向量的其他3个元素同理。

缩放

对一个向量进行缩放(Scaling)就是对向量的长度进行缩放,而保持它的方向不变。由于我们进行的是2维或3维操作,我们可以分别定义一个有2或3个缩放变量的向量,每个变量缩放一个轴(x、y或z)。

我们先来尝试缩放向量。我们可以把向量沿着x轴缩放0.5,使它的宽度缩小为原来的二分之一;我们将沿着y轴把向量的高度缩放为原来的两倍。我们看看把向量缩放(0.5, 2)倍所获得的s是什么样的:

记住,OpenGL通常是在3D空间进行操作的,对于2D的情况我们可以把z轴缩放1倍,这样z轴的值就不变了。我们刚刚的缩放操作是不均匀(Non-uniform)缩放,因为每个轴的缩放因子(Scaling Factor)都不一样。如果每个轴的缩放因子都一样那么就叫均匀缩放(Uniform Scale)。

我们下面会构造一个变换矩阵来为我们提供缩放功能。我们从单位矩阵了解到,每个对角线元素会分别与向量的对应元素相乘。如果我们把1变为3会怎样?这样子的话,我们就把向量的每个元素乘以3了,这事实上就把向量缩放3倍。如果我们把缩放变量表示为(我们可以为任意向量(x,y,z)定义一个缩放矩阵:

注意,第四个缩放向量仍然是1,因为在3D空间中缩放w分量是无意义的。w分量另有其他用途,在后面我们会看到。

位移

位移(Translation)是在原始向量的基础上加上另一个向量从而获得一个在不同位置的新向量的过程,从而在位移向量基础上移动了原始向量。我们已经讨论了向量加法,所以这应该不会太陌生。

和缩放矩阵一样,在4×4矩阵上有几个特别的位置用来执行特定的操作,对于位移来说它们是第四列最上面的3个值。如果我们把位移向量表示为,我们就能把位移矩阵定义为:

这样是能工作的,因为所有的位移值都要乘以向量的w行,所以位移值会加到向量的原始值上(想想矩阵乘法法则)。而如果你用3x3矩阵我们的位移值就没地方放也没地方乘了,所以是不行的。

有了位移矩阵我们就可以在3个方向(x、y、z)上移动物体,它是我们的变换工具箱中非常有用的一个变换矩阵。

旋转

上面几个的变换内容相对容易理解,在2D或3D空间中也容易表示出来,但旋转(Rotation)稍复杂些。如果你想知道旋转矩阵是如何构造出来的,我推荐你去看可汗学院线性代数的视频。

首先我们来定义一个向量的旋转到底是什么。2D或3D空间中的旋转用角(Angle)来表示。角可以是角度制或弧度制的,周角是360角度或2 PI弧度。我个人更喜欢用角度,因为它们看起来更直观。

转半圈会旋转360/2 = 180度,向右旋转1/5圈表示向右旋转360/5 = 72度。下图中展示的2D向量v¯v¯是由k¯k¯向右旋转72度所得的:

在3D空间中旋转需要定义一个角一个旋转轴(Rotation Axis)。物体会沿着给定的旋转轴旋转特定角度。如果你想要更形象化的感受,可以试试向下看着一个特定的旋转轴,同时将你的头部旋转一定角度。当2D向量在3D空间中旋转时,我们把旋转轴设为z轴(尝试想象这种情况)。

使用三角学,给定一个角度,可以把一个向量变换为一个经过旋转的新向量。这通常是使用一系列正弦和余弦函数(一般简称sin和cos)各种巧妙的组合得到的。当然,讨论如何生成变换矩阵超出了这个教程的范围。

旋转矩阵在3D空间中每个单位轴都有不同定义,旋转角度用θθ表示:

沿x轴旋转:

沿y轴旋转:

沿z轴旋转:

利用旋转矩阵我们可以把任意位置向量沿一个单位旋转轴进行旋转。也可以将多个矩阵复合,比如先沿着x轴旋转再沿着y轴旋转。但是这会很快导致一个问题——万向节死锁(Gimbal Lock,可以看看这个视频(优酷)来了解)。在这里我们不会讨论它的细节,但是对于3D空间中的旋转,一个更好的模型是沿着任意的一个轴,比如单位向量$(0.662, 0.2, 0.7222)$旋转,而不是对一系列旋转矩阵进行复合。这样的一个(超级麻烦的)矩阵是存在的,见下面这个公式,其中代表任意旋转轴:

在数学上讨论如何生成这样的矩阵仍然超出了本节内容。但是记住,即使这样一个矩阵也不能完全解决万向节死锁问题(尽管会极大地避免)。避免万向节死锁的真正解决方案是使用四元数(Quaternion),它不仅更安全,而且计算会更有效率。四元数可能会在后面的教程中讨论。

对四元数的理解会用到非常多的数学知识。如果你想了解四元数与3D旋转之间的关系,可以来阅读我的教程。如果你对万向节死锁的概念仍不是那么清楚,可以来阅读我教程的Bonus章节

现在3Blue1Brown也已经开始了一个四元数的视频系列,他采用球极平面投影(Stereographic Projection)的方式将四元数投影到3D空间,同样有助于理解四元数的概念(仍在更新中):https://www.youtube.com/watch?v=d4EgbgTm0Bg

矩阵的组合

使用矩阵进行变换的真正力量在于,根据矩阵之间的乘法,我们可以把多个变换组合到一个矩阵中。让我们看看我们是否能生成一个变换矩阵,让它组合多个变换。假设我们有一个顶点(x, y, z),我们希望将其缩放2倍,然后位移(1, 2, 3)个单位。我们需要一个位移和缩放矩阵来完成这些变换。结果的变换矩阵看起来像这样:

注意,当矩阵相乘时我们先写位移再写缩放变换的。矩阵乘法是不遵守交换律的,这意味着它们的顺序很重要。当矩阵相乘时,在最右边的矩阵是第一个与向量相乘的,所以你应该从右向左读这个乘法。建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会(消极地)互相影响。比如,如果你先位移再缩放,位移的向量也会同样被缩放(译注:比如向某方向移动2米,2米也许会被缩放成1米)!

用最终的变换矩阵左乘我们的向量会得到以下结果:

不错!向量先缩放2倍,然后位移了(1, 2, 3)个单位。

实践

现在我们已经解释了变换背后的所有理论,是时候将这些知识利用起来了。OpenGL没有自带任何的矩阵和向量知识,所以我们必须定义自己的数学类和函数。在教程中我们更希望抽象所有的数学细节,使用已经做好了的数学库。幸运的是,Qt为opengl提供了专门的数学库以及矩阵运算QMatrix4x4

我们使用上一节完成的代码来进行本节的扩展。你可以点击这里找到相关代码

我们首先在顶点着色器中声明这个变量,并且将它用于坐标计算,我们在前面简单提到过GLSL里也有一个mat4类型。所以我们将修改顶点着色器让其接收一个mat4的uniform变量,然后再用矩阵uniform乘以位置向量:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

uniform mat4 trans;
void main()
{
    gl_Position = trans * vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord;
}

然后我们在paintGL中创建变换矩阵trans(QMatrix4x4),并传递给顶点着色器

QMatrix4x4 trans;
shaderProgram.setUniformValue("trans",trans);

平移

 我们调用trans的translate函数进行平移,修改paintGL的代码为:

void Widget::paintGL()
{
    this->glClearColor(0.1f,0.5f,0.7f,1.0f);  //设置清屏颜色
    this->glClear(GL_COLOR_BUFFER_BIT);       //清除颜色缓存
    shaderProgram.bind();                     //使用shaderProgram着色程序
    {

        QMatrix4x4 trans;
        trans.translate(0.5f,0.0f,0.0f);    //向x轴正方向移动0.5个单位
        shaderProgram.setUniformValue("trans",trans);

        texture.bind(0);                                    //将texture绑定到纹理单元0
        shaderProgram.setUniformValue("ourTexture",0);      //让ourTexture从纹理单元0中获取纹理数据

        texture1.bind(1);                                    //将texture绑定到纹理单元1
        shaderProgram.setUniformValue("ourTexture1",1);      //让ourTexture从纹理单元1中获取纹理数据

        QOpenGLVertexArrayObject::Binder{&VAO};
        this->glDrawArrays(GL_POLYGON,0,4);     //使用以0开始,长度为4的顶点数据来绘制多边形
    }
}

你会看到图片向右侧移动了 

旋转 

我们再把trans部分修改为:

QMatrix4x4 trans;
trans.rotate(90,0.0f,0.0f,-1.0f);      // 以z轴负方向为旋转轴,逆时针旋转90°(等价于顺时针)
shaderProgram.setUniformValue("trans",trans);

你会看到如下的图片:

缩放 

我们再把trans部分修改为:

QMatrix4x4 trans;
trans.scale(1.5f,0.5f,1.0f);    //将x放大为原来的1.5倍,y缩放为原来的0.5
shaderProgram.setUniformValue("trans",trans);

你会看到如下的图片:

组合

对变换矩阵的基本操作我们已经大致了解了,下面我们来做一点有趣的东西,我们将这些变换组合到一起,且将它们与时间关联到一起(这里需要我们增加一个定时器)

        float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
        QMatrix4x4 trans;
        trans.translate(0.0f,0.5*qAbs(qSin(time)),0.0f);        //向y轴平移0.5*[0,1]
        trans.scale(0.5*qAbs(qSin(time)),0.5*qAbs(qSin(time))); //x,y在[0,0.5]进行缩放
        trans.rotate(360*time,0.0f,0.0f,-1.0f);                 //旋转360*time

我们首先把图片围绕原点(0, 0, 0)旋转,之后,在把它根据时间向y轴正方向平移,并且进行缩放。记住,实际的变换顺序应该与阅读顺序相反:尽管在代码中我们先位移再旋转,实际的变换却是先应用旋转再是位移的。明白所有这些变换的组合,并且知道它们是如何应用到物体上是一件非常困难的事情。只有不断地尝试和实验这些变换你才能快速地掌握它们。

如果你做对了,你将看到下面的结果:

代码节点

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLTexture>
#include <QTimer>
#include <QTime>
#include <QtMath>
class Widget : public QOpenGLWidget,public QOpenGLExtraFunctions
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();
protected:
    virtual void initializeGL() override;
    virtual void resizeGL(int w,int h) override;
    virtual void paintGL() override;
private:
    QVector<float> vertices;
    QOpenGLShaderProgram shaderProgram;
    QOpenGLBuffer VBO;
    QOpenGLVertexArrayObject VAO;
    QOpenGLTexture texture;
    QOpenGLTexture texture1;
    QTimer timer;
};

#endif // WIDGET_H

widget.cpp 

#include "widget.h"
#include <QtMath>

Widget::Widget(QWidget *parent)
    : QOpenGLWidget(parent)
    , VBO(QOpenGLBuffer::VertexBuffer)
    , texture(QOpenGLTexture::Target2D)
    , texture1(QOpenGLTexture::Target2D)
{
    vertices={
        // 位置              // 颜色               //纹理坐标
         0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,    1.0f, 0.0f,   // 右下
        -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,    0.0f, 0.0f,   // 左下
        -0.5f,  0.5f, 0.0f,  0.0f, 1.0f, 1.0f,    0.0f, 1.0f,   // 左下
         0.5f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f,    1.0f, 1.0f,   // 右上
    };
    timer.setInterval(18);
    connect(&timer,&QTimer::timeout,this,static_cast<void (Widget::*)()>(&Widget::update));
    timer.start();
}

Widget::~Widget()
{
}

void Widget::initializeGL()
{
    this->initializeOpenGLFunctions();        //初始化opengl函数
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){     //添加并编译顶点着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果编译出错,打印报错信息
    }
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/triangle.frag")){   //添加并编译片段着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果编译出错,打印报错信息
    }
    if(!shaderProgram.link()){                      //链接着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果链接出错,打印报错信息
    }

    QOpenGLVertexArrayObject::Binder{&VAO};

    VBO.create();       //生成VBO对象
    VBO.bind();         //将VBO绑定到当前的顶点缓冲对象(QOpenGLBuffer::VertexBuffer)中

    //将顶点数据分配到VBO中,第一个参数为数据指针,第二个参数为数据的字节长度
    VBO.allocate(vertices.data(),sizeof(float)*vertices.size());

    texture.create();
    texture.setData(QImage(":/opengl.jpg").mirrored());
    texture.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);
    texture.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
    texture.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);

    texture1.create();
    texture1.setData(QImage(":/sea.jpg").mirrored());
    texture1.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);
    texture1.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
    texture1.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);

    //设置顶点解析格式,并启用顶点
    shaderProgram.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 8);
    shaderProgram.enableAttributeArray("aPos");
    shaderProgram.setAttributeBuffer("aColor", GL_FLOAT,sizeof(GLfloat) * 3, 3, sizeof(GLfloat) * 8);
    shaderProgram.enableAttributeArray("aColor");
    shaderProgram.setAttributeBuffer("aTexCoord", GL_FLOAT,sizeof(GLfloat) * 6, 2, sizeof(GLfloat) * 8);
    shaderProgram.enableAttributeArray("aTexCoord");
}

void Widget::resizeGL(int w, int h)
{
    this->glViewport(0,0,w,h);                //定义视口区域
}

void Widget::paintGL()
{
    this->glClearColor(0.1f,0.5f,0.7f,1.0f);  //设置清屏颜色
    this->glClear(GL_COLOR_BUFFER_BIT);       //清除颜色缓存

   

    shaderProgram.bind();                     //使用shaderProgram着色程序
    {

        float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
        QMatrix4x4 trans;
        trans.translate(0.0f,0.5*qAbs(qSin(time)),0.0f);        //向y轴平移0.5*[0,1]
        trans.scale(0.5*qAbs(qSin(time)),0.5*qAbs(qSin(time))); //x,y在[0,0.5]进行缩放
        trans.rotate(360*time,0.0f,0.0f,-1.0f);                 //旋转360*time

        shaderProgram.setUniformValue("trans",trans);

        texture.bind(0);                                    //将texture绑定到纹理单元0
        shaderProgram.setUniformValue("ourTexture",0);      //让ourTexture从纹理单元0中获取纹理数据

        texture1.bind(1);                                    //将texture绑定到纹理单元1
        shaderProgram.setUniformValue("ourTexture1",1);      //让ourTexture从纹理单元1中获取纹理数据

        QOpenGLVertexArrayObject::Binder{&VAO};
        this->glDrawArrays(GL_POLYGON,0,4);     //使用以0开始,长度为4的顶点数据来绘制多边形
    }
}

triangle.vert

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

uniform mat4 trans;
void main()
{
    gl_Position = trans * vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord;
}

triangle.frag

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;
uniform sampler2D ourTexture1;
void main()
{
    FragColor = mix(texture(ourTexture, TexCoord), texture(ourTexture1, TexCoord), 0.4);
}

 你可以在上一节的末尾找到图片资源

猜你喜欢

转载自blog.csdn.net/qq_40946921/article/details/105968704