Learn OpenGL with Qt

您所在的位置:网站首页 色彩纹理作业图片 Learn OpenGL with Qt

Learn OpenGL with Qt

2024-07-11 12:53| 来源: 网络整理| 查看: 265

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

创建OpenGL窗口

着色器程序以及着色器的创建

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

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

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

纹理

我们上节中了解到,可以为每个顶点添加颜色来增加图形的细节,从而创建出有趣的图像。但是,如果想让图形看起来更真实,我们就必须有足够多的顶点,从而指定足够多的颜色。这将会产生很多额外开销,因为每个模型都会需求更多的顶点,每个顶点又需求一个颜色属性。

艺术家和程序员更喜欢使用纹理(Texture)。纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。

注:这节我们使用【上一节】中创建的三角形代码,来进行纹理的扩展。你可以点击这里,找到相关的代码,不过为了方便我们加载纹理,我们把三角形改为矩形。不过这次我们不再通过IBO(EBO)来绘制矩形,而是直接通过四个顶点,通过图元GL_POLYGON来绘制,所以我们对代码做一个简单的调整。

修改顶点数据为(注意保证要按顺序放置顶点):

vertices={ // 位置 // 颜色 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下 -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下 -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, // 左下 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // 右上 };

 修改绘制代码为:

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

如果成功的话,你会看到这样的图形:

创建与加载纹理

Qt提供了一个用于管理纹理的类——QOpenGLTexture

废话不多说,我们直接开始创建纹理吧!

首先头文件中包含,添加私有成员变量texture;

QOpenGLTexture texture;

 必须在构造函数中初始化texture的纹理目标,并且对构造函数应该是下面的样子。

Widget::Widget(QWidget *parent) : QOpenGLWidget(parent) , VBO(QOpenGLBuffer::VertexBuffer) , texture(QOpenGLTexture::Target2D) { vertices={ // 位置 // 颜色 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下 -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下 -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, // 左下 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // 右上 }; }

这里我们使用一张2D的纹理图片,你可以右键图片另存为来下载。

 下载之后需要将这张图片添加到我们的项目中,方法跟添加着色器一样,在Resources文件夹中右键qrc文件——添加现有文件——然后打开我们保存的图片(我们最好把图片放到项目中),打开后我们的项目结构是这样的。

然后我们在initializeGL函数中创建并加载纹理。关于setData,Qt提供了很多种加载方式,具体你可以点击这里,了解更多。

texture.create(); texture.setData(QImage(":/opengl.jpg"));

 接下来我们对纹理进行一些设置。

纹理环绕方式

纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但QOpenGLTexture提供了更多的选择:

环绕方式描述

QOpenGLTexture::Repeat

对纹理的默认行为。重复纹理图像。

QOpenGLTexture::MirroredRepeat

和GL_REPEAT一样,但每次重复图片是镜像放置的。

QOpenGLTexture::ClampToEdge

纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。

QOpenGLTexture::ClampToBorder

超出的坐标为用户指定的边缘颜色。

当纹理坐标超出默认范围时,每个选项都有不同的视觉效果输出。我们来看看这些纹理图像的例子:

前面提到的每个选项都可以使用setWrapMode函数对单独的一个坐标轴设置(S、T(如果是使用3D纹理那么还有一个R)它们和x、y、z是等价的):

texture.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat); texture.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);

第一个参数指定了应用的纹理轴。

第二个参数我们传递一个环绕方式(Wrapping)。

如果我们选择QOpenGLTexture::ClampToBorder选项,我们还需要指定一个边缘的颜色。这需要使用setBorderColor函数,并且传递一个QColor作为边缘的颜色值:

texture.setBorderColor(QColor(1.0f,1.0f,1.0f,1.0f)); 纹理过滤

纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素(Texture Pixel,也叫Texel,译注1)映射到纹理坐标。当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了。你可能已经猜到了,QOpenGLTexutre也有对于纹理过滤(Texture Filtering)的选项。纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:QOpenGLTexture::Nearest和QOpenGLTexture::Linear

译注1

Texture Pixel也叫Texel,你可以想象你打开一张.jpg格式图片,不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色。

QOpenGLTexture::Nearest(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为QOpenGLTexture::Nearest的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:

QOpenGLTexture::Linear(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:

那么这两种纹理过滤方式有怎样的视觉效果呢?让我们看看在一个很大的物体上应用一张低分辨率的纹理会发生什么吧(纹理被放大了,每个纹理像素都能看到):

 

QOpenGLTexture::Nearest产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而QOpenGLTexture::Linear能够产生更平滑的图案,很难看出单个的纹理像素。QOpenGLTexture::Linear可以产生更真实的输出,但有些开发者更喜欢8-bit风格,所以他们会用GL_NEAREST选项。

当进行放大(Magnify)和缩小(Minify)操作的时候可以设置纹理过滤的选项,比如你可以在纹理被缩小的时候使用邻近过滤,被放大时使用线性过滤。我们需要使用setMinMagFilters函数为放大和缩小指定过滤方式:

texture.setMinMagFilters(QOpenGLTexture::Nearest,QOpenGLTexture::Linear);

当然我们也可以直接调用setMagnificationFilter和 setMinificationFilter单独进行设置

多级渐远纹理

想象一下,假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。

OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。让我们看一下多级渐远纹理是什么样子的:

 

手工为每个纹理图像创建一系列多级渐远纹理很麻烦,幸好QOpenGLTexture有一个generateMipMaps()函数,在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作了。后面的教程中你会看到该如何使用它。

在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用QOpenGLTexture::Nearest和QOpenGLTexture::Linear过滤。为了指定不同多级渐远纹理级别之间的过滤方式,你可以使用下面四个选项中的一个代替原有的过滤方式:

过滤方式描述QOpenGLTexture::NearestMipMapNearest使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样QOpenGLTexture::LinearMipMapNearest使用最邻近的多级渐远纹理级别,并使用线性插值进行采样QOpenGLTexture::NearestMipMapLinear在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样QOpenGLTexture::LinearMipMapLinear在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样

就像纹理过滤一样,我们可以使用glTexParameteri将过滤方式设置为前面四种提到的方法之一:

 texture.setMinMagFilters(QOpenGLTexture::Nearest,QOpenGLTexture::Linear);

texture.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);

一个常见的错误是,将放大过滤的选项设置为多级渐远纹理过滤选项之一。这样没有任何效果,因为多级渐远纹理主要是使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。

绘制纹理

为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。

纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。下面的图片展示了我们是如何把纹理坐标映射到三角形上的。

 

 根据上方的例子,我们来设置矩形的纹理坐标,将矩形的顶点属性数组修改为:

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, // 右上 };

由于我们修改了顶点属性数组的结构,影响到了我们之前设置的顶点属性的步长,

因此我们需要在initializeGL中修改顶点属性的解析结构,并且增加顶点纹理坐标属性(aTexCoord)

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");

接着我们需要调整顶点着色器使其能够接受顶点坐标为一个顶点属性,并把坐标传给片段着色器:

#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; void main() { gl_Position = vec4(aPos, 1.0); ourColor = aColor; TexCoord = aTexCoord; }

片段着色器应该接下来会把输出变量TexCoord作为输入变量。

片段着色器也应该能访问纹理对象,但是我们怎样能把纹理对象传给片段着色器呢?GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D。我们可以简单声明一个uniform sampler2D把一个纹理添加到片段着色器中,稍后我们会把纹理赋值给这个uniform。

#version 330 core out vec4 FragColor; in vec3 ourColor; in vec2 TexCoord; uniform sampler2D ourTexture; void main() { FragColor = texture(ourTexture, TexCoord); }

我们使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。texture函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。

现在只剩下在调用glDrawArray之前绑定纹理了,它会自动把纹理赋值给片段着色器的采样器。我们修改paintGL函数为:

void Widget::paintGL() { this->glClearColor(0.1f,0.5f,0.7f,1.0f); //设置清屏颜色 this->glClear(GL_COLOR_BUFFER_BIT); //清除颜色缓存 shaderProgram.bind(); //使用shaderProgram着色程序 { texture.bind(0); //将texture绑定到纹理单元0 shaderProgram.setUniformValue("ourTexture",0); //让ourTexture着色采样器从纹理单元0中获取纹理数据 QOpenGLVertexArrayObject::Binder{&VAO}; this->glDrawArrays(GL_POLYGON,0,4); //使用以0开始,长度为4的顶点数据来绘制多边形 } }

关于纹理单元,我们后面再讲,相信你已经迫不及待准备运行我们的程序看看,那就来吧!

 你可能注意到纹理上下颠倒了!这是因为OpenGL要求坐标原点是在图片的左下方,但是图片的坐标原点通常在左上方。很幸运,QImage能够在图像加载时帮助我们翻转y轴,只需要在加载任何图像的时候调用QImage::mirrored():

texture.setData(QImage(":/opengl.jpg").mirrored());

然后我们再运行看看:

至此,我们已经完成了纹理的使用,但还没完,我们还需要做一个收尾的操作。

销毁纹理对象

当纹理对象不需要使用时,我们应当及时将它进行销毁,而在Qt中我们只需要调用纹理的destroy方法就能销毁对象,但这需要一个有效的OpenGL上下文。我们可以在窗口的析构函数中销毁纹理,操作如下:

 

Widget::~Widget() { makeCurrent(); texture.destroy(); doneCurrent(); } 代码节点 widget.h #ifndef WIDGET_H #define WIDGET_H #include #include #include #include #include #include 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 vertices; QOpenGLShaderProgram shaderProgram; QOpenGLBuffer VBO; QOpenGLVertexArrayObject VAO; QOpenGLTexture texture; }; #endif // WIDGET_H

 widget.cpp

#include "widget.h" #include Widget::Widget(QWidget *parent) : QOpenGLWidget(parent) , VBO(QOpenGLBuffer::VertexBuffer) , texture(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, // 右上 }; } Widget::~Widget() { makeCurrent(); texture.destroy(); doneCurrent(); } void Widget::initializeGL() { this->initializeOpenGLFunctions(); //初始化opengl函数 if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){ //添加并编译顶点着色器 qDebug()


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3