Qt+OpenGL使用教程(6)纹理

发布时间 2023-08-22 17:37:44作者: Clemens

在上一节我们绘制了矩形,这一节,我们将在上一节的基础上绘制纹理。有关纹理的基础知识在此不再赘述,详情参考LearnOpenGL(6) 纹理

 

一、准备工作

首先我们将顶点着色器程序和片段着色器程序单独放到两个文件里面,分别是shader.vs和shader.fs。

我们将两个着色器程序以资源的方式添加进资源文件,前缀为shader。

我们将我们需要的图片以资源的方式添加进资源文件,前缀为image。

 

二、绘制纹理

我们将纹理坐标添加进顶点属性数组:

float vertices[] = {
    // 位置               // 颜色              // 纹理坐标
    0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f, // 右上
    0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f, // 右下
    -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f, // 左下
    -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f  // 左上
};

 

因为我们修改了着色器程序的位置,我们应该修改读取着色器程序的代码:

    // 创建顶点着色器对象,并编译
    m_vshader = new QOpenGLShader(QOpenGLShader::Vertex);
    m_vshader->compileSourceFile(QString(":/shader/shader.vs"));
    // 创建片段着色器对象,并编译
    m_fshader = new QOpenGLShader(QOpenGLShader::Fragment);
    m_fshader->compileSourceFile(QString(":/shader/shader.fs"));

 

顶点属性发生了变化,我们也应该重新修改链接属性,并启用他们:

    // 链接顶点属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), 0);
    // 顶点属性默认是禁用的,启用顶点属性0(location=0)
    m_program->enableAttributeArray(0);
    // 颜色属性
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(3 * sizeof(float)));
    m_program->enableAttributeArray(1);
    // 纹理坐标属性
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(6 * sizeof(float)));
    m_program->enableAttributeArray(2);

 

加载纹理

Qt封装了纹理相关的操作到QOpenGLTexture类,这给我们编写程序带来了极大的方便。

我们创建纹理对象,并设置纹理环绕方式以及多级渐远纹理过滤方式:

//glwidget.h
class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
...
private:
    ...
    QOpenGLTexture *m_texture1 ;          //纹理对象
}

// glwidget.cpp
void GLWidget::initializeGL()
{
    ...
    // 加载纹理
    m_texture1 = new QOpenGLTexture(QImage(":/image/container.jpg").mirrored());
    // 设置纹理环绕方式
    m_texture1->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture1->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // 设置多级渐远纹理过滤方式
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);
    ...
}

 

渲染

在渲染循环内,我们将纹理绑定即可:

void GLWidget::paintGL()
{
    ...
    // 将纹理绑定到当前激活的纹理单元
    m_texture1->bind();
    ...
}

 

效果如图:

完整代码如下:

查看代码
 //glwidget.h
#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>

class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
public:
    explicit GLWidget(QWidget *parent = nullptr);
    ~GLWidget();

protected:
    void resizeGL(int w, int h) override;
    void initializeGL() override;
    void paintGL() override;

private:
    QOpenGLVertexArrayObject *m_vao;    //顶点数组对象
    QOpenGLBuffer *m_vbo;               //顶点缓冲对象
    QOpenGLBuffer *m_ebo;               //元素缓冲对象
    QOpenGLShader *m_vshader;           //顶点着色器
    QOpenGLShader *m_fshader;           //片段着色器
    QOpenGLShaderProgram *m_program;    //着色器程序对象
    QOpenGLTexture *m_texture1 ;          //纹理对象

};
#endif // GLWIDGET_H

//glwidget.cpp
#include "glwidget.h"
#include <QDir>
#include <QFile>
#include <QDebug>
#include <QImage>

float vertices[] = {
    // 位置               // 颜色              // 纹理坐标
    0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f, // 右上
    0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f, // 右下
    -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f, // 左下
    -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f  // 左上
};
unsigned int indices[] = {
    // 注意索引从0开始!
    // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
    // 这样可以由下标代表顶点组合成矩形
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

GLWidget::GLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
    makeCurrent();

}

GLWidget::~GLWidget()
{
    if (m_program == nullptr) { return; }
    makeCurrent();
    delete m_program;
    delete m_vshader;
    delete m_fshader;
    m_vbo->destroy();
    m_vao->destroy();
    doneCurrent();
}

//设置OpenGL视口、投影等。每当窗口部件被调整大小时调用(也在第一次显示时调用,因为所有新创建的窗口部件都自动获得调整大小事件)。
void GLWidget::resizeGL(int w, int h)
{

}

//设置OpenGL资源和状态。在第一次调用resizeGL()或paintGL()之前调用一次。
void GLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 设置清屏颜色
    glEnable(GL_DEPTH_TEST);    // 启用深度测试
    glEnable(GL_COLOR_BUFFER_BIT);  // 启用颜色缓冲区

    // 创建顶点着色器对象,并编译
    m_vshader = new QOpenGLShader(QOpenGLShader::Vertex);
    m_vshader->compileSourceFile(QString(":/shader/shader.vs"));
    // 创建片段着色器对象,并编译
    m_fshader = new QOpenGLShader(QOpenGLShader::Fragment);
    m_fshader->compileSourceFile(QString(":/shader/shader.fs"));
    // 创建着色器程序对象,添加顶点着色器和片段着色器,并链接它们
    m_program = new QOpenGLShaderProgram;
    m_program->addShader(m_vshader);
    m_program->addShader(m_fshader);
    m_program->link();
    // 创建顶点数组对象,并绑定到当前上下文
    m_vao = new QOpenGLVertexArrayObject;
    m_vao->create();
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 创建顶点缓冲对象,
    m_vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
    m_vbo->create();
    m_vbo->bind();
    // 分配显存大小,并搬运至显存
    m_vbo->allocate(vertices, sizeof(vertices));

    // 创建元素缓冲对象,
    m_ebo = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer);
    m_ebo->create();
    m_ebo->bind();
    // 分配显存大小,并搬运至显存
    m_ebo->allocate(indices, sizeof(indices));
    // 链接顶点属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), 0);
    // 顶点属性默认是禁用的,启用顶点属性0(location=0)
    m_program->enableAttributeArray(0);
    // 颜色属性
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(3 * sizeof(float)));
    m_program->enableAttributeArray(1);
    // 纹理坐标属性
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(6 * sizeof(float)));
    m_program->enableAttributeArray(2);

    // 加载纹理
    m_texture1 = new QOpenGLTexture(QImage(":/image/container.jpg").mirrored());
    // 设置纹理环绕方式
    m_texture1->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture1->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // 设置多级渐远纹理过滤方式
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);

    m_vao->release();
    m_vbo->release();
    // VAO还在使用时,不能释放EBO
}

//渲染OpenGL场景。每当窗口部件需要更新时调用。
void GLWidget::paintGL()
{
    // 清空颜色缓冲区和深度缓冲区
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 将纹理绑定到当前激活的纹理单元
    m_texture1->bind();
    // 绑定着色器程序至当前上下文,相当于调用glUseProgram()
    m_program->bind();
    // 绑定VAO,调用设置的一组状态
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 绘制三角形
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    m_program->release();
    // 交换缓存
    auto *context = QOpenGLContext::currentContext();
    context->swapBuffers(context->surface());
    //使用swapBuffers后,必须调用makeCurrent后才能使用其他OpenGL函数。
    makeCurrent();
}

 

顶点着色器程序:

查看代码
 #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 = vec2(aTexCoord.x, aTexCoord.y);
}

 

片段着色器程序:

#version 330 core

out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D texture1;

void main()
{
    FragColor = texture(texture1, TexCoord);
}

 

 

三、纹理单元

一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的激活纹理单元,所以教程前面部分我们没有分配一个位置值。

纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。

通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。

所以我们接下来使用两个纹理单元,将他们叠加到一起。

我们修改片段着色器程序,定义两个采样器,并将他们混合。

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

GLSL内建的mix函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值。如果第三个值是0.0,它会返回第一个输入;如果是1.0,会返回第二个输入值。0.2会返回80%的第一个输入颜色和20%的第二个输入颜色,即返回两个纹理的混合色。

 

我们再创建一个纹理对象,方法同上,不再叙述。

    // 加载纹理
    m_texture2 = new QOpenGLTexture(QImage(":/image/awesomeface.png").mirrored());
    // 设置纹理环绕方式
    m_texture2->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture2->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // m_texture2
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);

 

我们需要手动设置哪个采样器对应哪个纹理单元:

void GLWidget::initializeGL()
{
...
    // 我们需要手动设置哪个采样器对应哪个纹理单元
    m_program->bind();  //在修改uniform值之前,一定要绑定着色器程序到当前激活的opengl上下文
    m_program->setUniformValue("texture1", 0);
    m_program->setUniformValue("texture2", 1);
...
}

 

在只有一个纹理单元的时候,纹理单元0默认是激活的,我们的纹理对象也是默认bind到纹理单元0上,现在我们有了两个纹理对象,我们需要手动设置哪个纹理对象绑定哪个纹理单元:

void GLWidget::paintGL()
{
    ...
    // 将纹理绑定到当前激活的纹理单元
    m_texture1->bind(0);
    m_texture2->bind(1);
    ...
}

 

运行程序,我们将得到如下:

 

完整代码如下:

查看代码
 //glwidget.h
#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>

class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
public:
    explicit GLWidget(QWidget *parent = nullptr);
    ~GLWidget();

protected:
    void resizeGL(int w, int h) override;
    void initializeGL() override;
    void paintGL() override;
private:
    QOpenGLVertexArrayObject *m_vao;    //顶点数组对象
    QOpenGLBuffer *m_vbo;               //顶点缓冲对象
    QOpenGLBuffer *m_ebo;               //元素缓冲对象
    QOpenGLShader *m_vshader;           //顶点着色器
    QOpenGLShader *m_fshader;           //片段着色器
    QOpenGLShaderProgram *m_program;    //着色器程序对象
    QOpenGLTexture *m_texture1 ;          //纹理对象
    QOpenGLTexture *m_texture2 ;          //纹理对象
};
#endif // GLWIDGET_H

// glwidget.cpp
#include "glwidget.h"
#include <QDir>
#include <QFile>
#include <QDebug>
#include <QImage>

float vertices[] = {
    // 位置               // 颜色              // 纹理坐标
    0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f, // 右上
    0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f, // 右下
    -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f, // 左下
    -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f  // 左上
};
unsigned int indices[] = {
    // 注意索引从0开始!
    // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
    // 这样可以由下标代表顶点组合成矩形
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

GLWidget::GLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
    makeCurrent();

}

GLWidget::~GLWidget()
{
    if (m_program == nullptr) { return; }
    makeCurrent();
    delete m_program;
    delete m_vshader;
    delete m_fshader;
    m_vbo->destroy();
    m_vao->destroy();
    doneCurrent();
}

//设置OpenGL视口、投影等。每当窗口部件被调整大小时调用(也在第一次显示时调用,因为所有新创建的窗口部件都自动获得调整大小事件)。
void GLWidget::resizeGL(int w, int h)
{

}

//设置OpenGL资源和状态。在第一次调用resizeGL()或paintGL()之前调用一次。
void GLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 设置清屏颜色
    glEnable(GL_DEPTH_TEST);    // 启用深度测试
    glEnable(GL_COLOR_BUFFER_BIT);  // 启用颜色缓冲区

    // 创建顶点着色器对象,并编译
    m_vshader = new QOpenGLShader(QOpenGLShader::Vertex);
    m_vshader->compileSourceFile(QString(":/shader/shader.vs"));
    // 创建片段着色器对象,并编译
    m_fshader = new QOpenGLShader(QOpenGLShader::Fragment);
    m_fshader->compileSourceFile(QString(":/shader/shader.fs"));
    // 创建着色器程序对象,添加顶点着色器和片段着色器,并链接它们
    m_program = new QOpenGLShaderProgram;
    m_program->addShader(m_vshader);
    m_program->addShader(m_fshader);
    m_program->link();
    // 创建顶点数组对象,并绑定到当前上下文
    m_vao = new QOpenGLVertexArrayObject;
    m_vao->create();
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 创建顶点缓冲对象,
    m_vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
    m_vbo->create();
    m_vbo->bind();
    // 分配显存大小,并搬运至显存
    m_vbo->allocate(vertices, sizeof(vertices));

    // 创建元素缓冲对象,
    m_ebo = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer);
    m_ebo->create();
    m_ebo->bind();
    // 分配显存大小,并搬运至显存
    m_ebo->allocate(indices, sizeof(indices));
    // 链接顶点属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), 0);
    // 顶点属性默认是禁用的,启用顶点属性0(location=0)
    m_program->enableAttributeArray(0);
    // 颜色属性
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(3 * sizeof(float)));
    m_program->enableAttributeArray(1);
    // 纹理坐标属性
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(6 * sizeof(float)));
    m_program->enableAttributeArray(2);


    // 加载纹理
    m_texture1 = new QOpenGLTexture(QImage(":/image/container.jpg").mirrored());
    // 设置纹理环绕方式
    m_texture1->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture1->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // 设置多级渐远纹理过滤方式
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);

    // 加载纹理
    m_texture2 = new QOpenGLTexture(QImage(":/image/awesomeface.png").mirrored());
    // 设置纹理环绕方式
    m_texture2->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture2->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // m_texture2
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);


    // 我们需要手动设置哪个采样器对应哪个纹理单元
    m_program->bind();  //在修改uniform值之前,一定要绑定着色器程序到当前激活的opengl上下文
    m_program->setUniformValue("texture1", 0);
    m_program->setUniformValue("texture2", 1);


    m_vao->release();
    m_vbo->release();
    // VAO还在使用时,不能释放EBO
}

//渲染OpenGL场景。每当窗口部件需要更新时调用。
void GLWidget::paintGL()
{
    // 清空颜色缓冲区和深度缓冲区
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 将纹理绑定到当前激活的纹理单元
    m_texture1->bind(0);
    m_texture2->bind(1);
    // 绑定着色器程序至当前上下文,相当于调用glUseProgram()
    m_program->bind();
    // 绑定VAO,调用设置的一组状态
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 绘制三角形
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    m_program->release();
    // 交换缓存
    auto *context = QOpenGLContext::currentContext();
    context->swapBuffers(context->surface());
    //使用swapBuffers后,必须调用makeCurrent后才能使用其他OpenGL函数。
    makeCurrent();
}