QT+ OpenGL学习

2023-11-19

QT+ OpenGL

本篇完整工程见gitee:QTOpenGL
对应点的tag,由turbolove提供技术支持,您可以关注博主或者私信博主。

什么是opengl

open graphics library 他是一个由Khronos组织制定并且维护的规范

opengl核心是一个c库,同时也支持多种语言的派生

核心模式(core-profile):

  • 也叫可编程管线,提供了更多的灵活性,更高的效率,更重要的是可以深入的理解图形编程。

立即渲染模式(Immediate mode)

  • 早期的OpenGL使用的模式(也就是固定渲染管线)
  • OpenGL的大多数功能都被库隐藏起来,容易使用和理解,但是效率低下
  • 开发者很少能控制OpenGL如何进行计算
  • 因此从OpenGL3.2开始推出核心模式

状态机(state machine)

  • OpenGL是一个聚到的状态机 - 描述如何操作的所有变量的大集合
  • OpenGL的状态通常被称为上下文Context
  • 状态设置函数(State-changing Function)
  • 状态应用函数(State-using Function)

对象(Object)

  • 一个对象是指一些选项的集合,代表OpenGL状态的一个子集
  • 当前状态只有一份,如果每次显示不同的效果,都重新配置会很麻烦
  • 我们需要使用一些小助理(对象),帮忙记录某些状态信息,以便复用
// 创建对象
GLunit objId = 0;
glGenObject(1, &objId);
// 绑定对象到上下文
glBindObject(GL_WINDOW_TARGET, objId);
// 设置GL_WINDOW_TARGET对象的一些选项
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// 将上下文的GL_WINDOW_TARGET对象设置成默认值
glBindObject(GL_WINDOW_TARGET, 0);
// 一旦我们重新绑定这个对象到GL_WINDOW_TARGET位置,这些选项就会重新生效

QOpenGLWidget:不需要GLFW

QOpenGLWidget提供了三个便捷的虚拟函数,可以冲在,用来实现典型的opengl任务

  • paintGL:渲染opengl场景,widget需要更新时候调用
  • resizeGL:设置opengl视口,投影等,widget调整大小(或者首次显示)时候调用
  • initializeGL:设置opengl资源和状态,第一次调用resizeGL()或者paintGL()之前调用一次

如果需要从paintGL()意外的位置触发重新绘制(典型示例是使用计时器设置场景动画),则应该调用widget的update()函数来安排更新。

调用paintGL()、resizeGL()、initializeGL()时,widget的opengl呈现上下文变为当前。如果需要从其他位置(例如在widget的构造函数或者自己的绘制函数中)调用openglAPI函数,则必须首先调用makeCurrent()。

QOpenGLFunction_X_X_Core:不需要GLAD

QOpenGLFunction_X_X_Core提供OpenGL x.x版本核心模式的所有功能,是对OpenGL函数的封装

turboopenglwidget.h

#ifndef QTOPENGL_TURBOOPENGLWIDGET_H
#define QTOPENGL_TURBOOPENGLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions_4_5_Core>

class TurboOpenGLWidget : public QOpenGLWidget, QOpenGLFunctions_4_5_Core
{
    Q_OBJECT
public:
    explicit TurboOpenGLWidget(QWidget *parent = 0);

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

private:

};

#endif //QTOPENGL_TURBOOPENGLWIDGET_H

turboopenglwidget.cpp

#include "turboopenglwidget.h"

TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent)
    : QOpenGLWidget(parent)
{

}

void TurboOpenGLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    QOpenGLWidget::initializeGL();
}

void TurboOpenGLWidget::paintGL()
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    QOpenGLWidget::paintGL();
}

void TurboOpenGLWidget::resizeGL(int w, int h)
{
    QOpenGLWidget::resizeGL(w, h);
}

你好,三角形

float vertices[] = 
{
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    0.0f, 0.5f, 0.0f,
};

标准化设备坐标:

订单着色器中处理过后,应该就是标准化设备坐标,x、y、z的值在-1.0到1.0的一小段空间(立方体)。落在范围外的坐标都会被裁减。

顶点输入

  • 他会在GPU上创建内存,用于存储我们的顶点数据
    • 通过点缓冲对象(vertex buffer object , VBO)管理
      • 顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER
  • 配置OpenGL如何解释这些内存
    • 通过顶点数组对象(vertex array object , VAO)管理

数组力的每一个项都对应一个属性的解析。

OpenGL允许我们同时绑定多个缓冲,只要他们是不同的缓冲类型(每个缓冲类型类似于前面说的子集,每个VBO是一个小助理)。

VAO并不保存实际数据,而是放顶点结构定义。

// 创建VAO和VBO对象,并且赋予ID
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);

// 绑定VAO和VBO对象
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);

// 为当前绑定到target的缓冲区对象创建一个新的数据存储
// 如果data不是NULL, 则使用来自此指针的数据初始化数据存储
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 告知显卡如何解析缓冲里面的属性值
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0);
// 开启VAO管理的第一个属性的值
glEnableVertexAttribArray(0);

// 释放VAO和VBO对象
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

glBufferData

是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。

  • 第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。

  • 第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。

  • 第三个参数是我们希望发送的实际数据。

  • 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:

    • GL_STATIC_DRAW :数据不会或几乎不会改变。

    • GL_DYNAMIC_DRAW:数据会被改变很多。

    • GL_STREAM_DRAW :数据每次绘制时都会改变。

glVertexAttribPointer

  • 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0
  • 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
  • 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
  • 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
  • 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
  • 最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。

顶点着色器

#version 330 core 
layout (location = 0) in vec3 aPos;

void main()
{
	gl_Position = vec4(aPos, 1.0);
}
// 创建顶点着色器
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader ,1 ,&vertexShaderSource, NULL);
glCompileShader(vertexShader);

glShaderSource

  • 第一个参数是函数把要编译的着色器对象。
  • 第二参数指定了传递的源码字符串数量,这里只有一个。
  • 第三个参数是顶点着色器真正的源码,
  • 第四个参数我们先设置为NULL

片段着色器

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 
// 创建顶片段着色器
unsigned int fragShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragShader ,1 ,&fragShaderSource, NULL);
glCompileShader(fragShader);

链接着色器

unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragShader);
glLinkProgram(shaderProgram);

glDeleteShader(vertexShader);
glDeleteShader(fragShader);

本节代码

turboopenglwidget.cpp

#include "turboopenglwidget.h"

float vertices[] =
{
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    0.0f, 0.5f, 0.0f,
};
const char *vertexShaderSource =
        "#version 330 core \n"
        "layout (location = 0) in vec3 aPos;\n"
        "\n"
        "void main()\n"
        "{\n"
        "\tgl_Position = vec4(aPos, 1.0);\n"
        "}";
const char *fragShaderSource =
        "#version 330 core\n"
        "out vec4 FragColor;\n"
        "\n"
        "void main()\n"
        "{\n"
        "    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
        "} ";

// 创建VAO和VBO对象,并且赋予ID
unsigned int VBO, VAO;
unsigned int shaderProgram;
TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent)
    : QOpenGLWidget(parent)
{

}

void TurboOpenGLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // 绑定VAO和VBO对象
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // 为当前绑定到target的缓冲区对象创建一个新的数据存储
    // 如果data不是NULL, 则使用来自此指针的数据初始化数据存储
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // 告知显卡如何解析缓冲里面的属性值
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0);
    // 开启VAO管理的第一个属性的值
    glEnableVertexAttribArray(0);
    // 释放VAO和VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    // 创建顶点着色器
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader ,1 ,&vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // 创建顶片段着色器
    unsigned int fragShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragShader ,1 ,&fragShaderSource, NULL);
    glCompileShader(fragShader);

    shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragShader);
    glLinkProgram(shaderProgram);

    glDeleteShader(vertexShader);
    glDeleteShader(fragShader);
}

void TurboOpenGLWidget::paintGL()
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(shaderProgram);
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

void TurboOpenGLWidget::resizeGL(int w, int h)
{

}

元素缓冲对象EBO

可以绘制两个三角形来组合成一个矩形,这会生成下面的顶点的集合:

float vertices[] = {
    // 第一个三角形
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, 0.5f, 0.0f,  // 左上角
    // 第二个三角形
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

值得庆幸的是,元素缓冲区对象的工作方式正是如此。 EBO是一个缓冲区,就像一个顶点缓冲区对象一样,它存储 OpenGL 用来决定要绘制哪些顶点的索引。这种所谓的索引绘制(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义(不重复的)顶点,和绘制出矩形所需的索引:

代码展示:

#include "turboopenglwidget.h"

float vertices[] = {
        0.5f, 0.5f, 0.0f,   // 右上角
        0.5f, -0.5f, 0.0f,  // 右下角
        -0.5f, -0.5f, 0.0f, // 左下角
        -0.5f, 0.5f, 0.0f   // 左上角
};

unsigned int indices[] = {
        0, 1, 3, // 第一个三角形
        1, 2, 3  // 第二个三角形
};
const char *vertexShaderSource =
        "#version 330 core \n"
        "layout (location = 0) in vec3 aPos;\n"
        "\n"
        "void main()\n"
        "{\n"
        "\tgl_Position = vec4(aPos, 1.0);\n"
        "}";
const char *fragShaderSource =
        "#version 330 core\n"
        "out vec4 FragColor;\n"
        "\n"
        "void main()\n"
        "{\n"
        "    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
        "} ";

// 创建VAO和VBO对象,并且赋予ID
unsigned int VBO, VAO;
unsigned int EBO;
unsigned int shaderProgram;
TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent)
    : QOpenGLWidget(parent)
{

}

void TurboOpenGLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);
    // 绑定VAO和VBO对象
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // 为当前绑定到target的缓冲区对象创建一个新的数据存储
    // 如果data不是NULL, 则使用来自此指针的数据初始化数据存储
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // 告知显卡如何解析缓冲里面的属性值
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0);
    // 开启VAO管理的第一个属性的值
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // 释放VAO和VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    // 创建顶点着色器
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader ,1 ,&vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // 创建顶片段着色器
    unsigned int fragShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragShader ,1 ,&fragShaderSource, NULL);
    glCompileShader(fragShader);

    shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragShader);
    glLinkProgram(shaderProgram);

    glDeleteShader(vertexShader);
    glDeleteShader(fragShader);

    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
}

void TurboOpenGLWidget::paintGL()
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    
    glUseProgram(shaderProgram);
    glBindVertexArray(VAO);
//    glDrawArrays(GL_TRIANGLES, 0, 6);
//     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}

void TurboOpenGLWidget::resizeGL(int w, int h)
{
	
}

QT交互

  • 如果需要从paintGL()以外的位置触发重新绘制(典型示例是使用计时器设置场景动画), 则应该调用widget的update()函数来安排更新
  • 调用paintGL()、resizeGL()和initializeGL()时,widget的OpenGL呈现上下文将变为当前。如果需要从其他位置(例如,在widget的构造函数或自己的绘制函数中)调用opengl API函数,则必须首先调用makeCurrent()。

代码示例:

turboopenglwidget.h

#ifndef QTOPENGL_TURBOOPENGLWIDGET_H
#define QTOPENGL_TURBOOPENGLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLShaderProgram>
#include <QOpenGLFunctions_4_5_Core>

class TurboOpenGLWidget : public QOpenGLWidget, QOpenGLFunctions_4_5_Core
{
    Q_OBJECT
public:
    enum Shape
    {
        None,
        Rect,
        Circle,
        Triangle,
    };
    explicit TurboOpenGLWidget(QWidget *parent = 0);
    ~TurboOpenGLWidget() override;
    void drawShape(Shape shape);
    void setWireFrame(bool mode);

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

private:
    Shape shape_;
    QOpenGLShaderProgram shader_program_;
};


#endif //QTOPENGL_TURBOOPENGLWIDGET_H

turboopenglwidget.cpp

#include <iostream>
#include "turboopenglwidget.h"

float vertices[] = {
        0.5f, 0.5f, 0.0f,   // 右上角
        0.5f, -0.5f, 0.0f,  // 右下角
        -0.5f, -0.5f, 0.0f, // 左下角
        -0.5f, 0.5f, 0.0f   // 左上角
};

unsigned int indices[] = {
        0, 1, 3, // 第一个三角形
        1, 2, 3  // 第二个三角形
};

// 创建VAO和VBO对象,并且赋予ID
unsigned int VBO, VAO;
unsigned int EBO;
TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent)
    : QOpenGLWidget(parent)
{

}

TurboOpenGLWidget::~TurboOpenGLWidget()
{
    makeCurrent();
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glDeleteVertexArrays(1, &VAO);
    doneCurrent();
}

void TurboOpenGLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);
    // 绑定VAO和VBO对象
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // 为当前绑定到target的缓冲区对象创建一个新的数据存储
    // 如果data不是NULL, 则使用来自此指针的数据初始化数据存储
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // 告知显卡如何解析缓冲里面的属性值
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0);
    // 开启VAO管理的第一个属性的值
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // 释放VAO和VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    bool success = false;
    shader_program_.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/resources/shader.vert");
    shader_program_.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/resources/shader.frag");
    success = shader_program_.link();
    if(!success)
    {
        std::cout << "shader is failed" << std::endl;
    }
    // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
}

void TurboOpenGLWidget::paintGL()
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    shader_program_.bind();
    glBindVertexArray(VAO);
//    glDrawArrays(GL_TRIANGLES, 0, 6);

//     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    switch(shape_)
    {
        case Rect:
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
            break;
    }
    update();
}

void TurboOpenGLWidget::resizeGL(int w, int h)
{

}

void TurboOpenGLWidget::drawShape(TurboOpenGLWidget::Shape shape)
{
    shape_ = shape;
}

void TurboOpenGLWidget::setWireFrame(bool mode)
{
    makeCurrent();
    if(mode)
    {
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    }
    else
    {
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    }
}

mainwidow.h

#ifndef QTOPENGL_MAINWINDOW_H
#define QTOPENGL_MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = nullptr);

    ~MainWindow() override;

protected slots:
    void drawRect();
    void clearPic();
    void lineModel(const bool &mode);

private:
    Ui::MainWindow *ui;
    QToolBar *tool_bar_{nullptr};
};


#endif //QTOPENGL_MAINWINDOW_H

mainwidow.cpp

#include "mainwindow.h"
#include "ui_MainWindow.h"
#include <QToolBar>
#include <QAction>

MainWindow::MainWindow(QWidget *parent) :
        QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    setCentralWidget(ui->openGLWidget);
    tool_bar_ = new QToolBar(this);
    auto *action = new QAction(tr("绘制矩形"), this);
    auto *action2 = new QAction(tr("清空图形"), this);
    auto *action3 = new QAction(tr("线框模式"), this);
    action3->setCheckable(true);
    connect(action, &QAction::triggered, this, &MainWindow::drawRect);
    connect(action2, &QAction::triggered, this, &MainWindow::clearPic);
    connect(action3, &QAction::triggered, this, &MainWindow::lineModel);
    tool_bar_->addAction(action);
    tool_bar_->addAction(action2);
    tool_bar_->addAction(action3);
    addToolBar(tool_bar_);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::drawRect()
{
    ui->openGLWidget->drawShape(TurboOpenGLWidget::Rect);
}

void MainWindow::clearPic()
{
    ui->openGLWidget->drawShape(TurboOpenGLWidget::None);
}

void MainWindow::lineModel(const bool &mode)
{
    ui->openGLWidget->setWireFrame(mode);
}

GLSL

OpenGL Shading Languaage

一个典型的shader程序结构:

#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

int main()
{
  // 处理输入并进行一些图形操作
  ...
  // 输出处理过的结果到输出变量
  out_variable_name = weird_stuff_we_processed;
}

我们能声明的顶点数量是有限的,可以通过下面的代码获取:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件可能会允许更多的顶点属性。

GLSL支持的类型

类型:

GLSL中包含C等其他语言大部分默认的基础数据类型

int float double uint bool

GLSL也有两种容器类型

类型 含义
vecn 包含n个float分量的默认向量
bvecn 包含n个bool分量的向量
ivecn 包含n个int分量的向量
uvecn 包含n个unsigned int分量的向量
dvecn 包含n个double分量的向量

向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(Swizzling)。重组允许这样的语法:

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

输入输出

  • 在发送方着色器声明一个输出
  • 在接收方着色器声明一个类似的输入
  • 当类型和名称都一致的时候OpenGL会把两个变量链接到一起(在链接程序对象时候完成)

顶点着色器

#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0

out vec4 vertexColor; // 为片段着色器指定一个颜色输出

void main()
{
    gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}

片段着色器

#version 330 core
out vec4 FragColor;

in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)

void main()
{
    FragColor = vertexColor;
}

顶点着色器接收的是一种特殊形式的输入,否则就会效率低下

从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据(metadata)指定输入变量,这样我们才可以在CPU上配置顶点属性。例如: layout(location = 0)。 layout这个标识,使得我们能把它链接到顶点数据。

可以忽略layout( location = 0) 标识符,通过在OPenGL代码中使用glGetAttrribLocation查询属性位置(Location),或者是glBindAttribLocation属性位置值(Location),但是推荐在着色器中设置他们,这样会更容易理解而且节省你和(OpenGL)的工作量

#include <iostream>
#include "turboopenglwidget.h"

float vertices[] = {
        0.5f, 0.5f, 0.0f,   // 右上角
        0.5f, -0.5f, 0.0f,  // 右下角
        -0.5f, -0.5f, 0.0f, // 左下角
        -0.5f, 0.5f, 0.0f   // 左上角
};

unsigned int indices[] = {
        0, 1, 3, // 第一个三角形
        1, 2, 3  // 第二个三角形
};

// 创建VAO和VBO对象,并且赋予ID
unsigned int VBO, VAO;
unsigned int EBO;
TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent)
    : QOpenGLWidget(parent)
{

}

TurboOpenGLWidget::~TurboOpenGLWidget()
{
    makeCurrent();
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glDeleteVertexArrays(1, &VAO);
    doneCurrent();
}

void TurboOpenGLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);
    // 绑定VAO和VBO对象
    glBindVertexArray(VAO);

    shader_program_.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/resources/shader.vert");
    shader_program_.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/resources/shader.frag");
    bool success = false;
    success = shader_program_.link();
    if(!success)
    {
        std::cout << "shader is failed" << std::endl;
    }
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // 为当前绑定到target的缓冲区对象创建一个新的数据存储
    // 如果data不是NULL, 则使用来自此指针的数据初始化数据存储
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // 告知显卡如何解析缓冲里面的属性值
    int location = shader_program_.attributeLocation("aPos");
    glVertexAttribPointer(location, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0);
    // 开启VAO管理的第一个属性的值
    glEnableVertexAttribArray(location);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // 释放VAO和VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

void TurboOpenGLWidget::paintGL()
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    shader_program_.bind();
    glBindVertexArray(VAO);
//    glDrawArrays(GL_TRIANGLES, 0, 6);

//     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    switch(shape_)
    {
        case Rect:
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
            break;
    }
    update();
}

void TurboOpenGLWidget::resizeGL(int w, int h)
{

}

void TurboOpenGLWidget::drawShape(TurboOpenGLWidget::Shape shape)
{
    shape_ = shape;
}

void TurboOpenGLWidget::setWireFrame(bool mode)
{
    makeCurrent();
    if(mode)
    {
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    }
    else
    {
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    }
}

Uniform

另一种从CPU的应用,向GPU中的着色器发送数据的方式

uniform是全局的,可以被任意的着色器程序在任一阶段访问

#version 330 core
out vec4 FragColor;

uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量

void main()
{
    FragColor = ourColor;
}

如果声明了一个uniform却没有用过,编译器会默认移除这个变,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,切记

这次我们不去给像素单独传递一个颜色,而是让他随着时间改变颜色。

OpenGL在其核心是一个C库,所以他不支持类型重载,在函数参数类型不同时候就要为其定义新的函数,glUniform是一个典型的例子。这个函数有特定的后缀,用来标识设定的uniform的类型。可能的后缀有:

后缀 含义
f 函数需要一个float作为它的值
i 函数需要一个int作为它的值
ui 函数需要一个unsigned int作为它的值
3f 函数需要3个float作为它的值
fv 函数需要一个float向量/数组作为它的值

QT 为我们封装了这个函数,因此我们可以不用太过关注该函数的详细内容,但是你要是用原生的OpenGL的话,需要关注该函数。

纹理

当我们需要给图形赋予真实的颜色的时候,不大可能使用前面的方法为每一个顶点指定第一个颜色,通常我们会采用纹理贴图。

每个顶点关联一个纹理坐标(Texture Coordinate),之后在图形的其他片段上进行片段插值

我们只需要告诉OpenGL如何对纹理采样即可

顶点着色器

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 1) in vec2 aTexCord;
out vec3 ourColor; // 向片段着色器输出一个颜色
out vec2 texCord; // 向片段着色器输出一个颜色
void main()
{
	gl_Position = vec4(aPos, 1.0);
	ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
	texCord = aTexCord;
}

片段着色器

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

对应的显示代码:

#include <iostream>
#include "turboopenglwidget.h"

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, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f // 左上角
};

unsigned int indices[] = {
        0, 1, 3, // 第一个三角形
        1, 2, 3  // 第二个三角形
};

// 创建VAO和VBO对象,并且赋予ID
unsigned int VBO, VAO;
unsigned int EBO;
TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent)
    : QOpenGLWidget(parent)
{
    connect(&timer, &QTimer::timeout, this, &TurboOpenGLWidget::timeout);
    // timer.start(100);
}

TurboOpenGLWidget::~TurboOpenGLWidget()
{
    if(!isValid()) return;
    makeCurrent();
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glDeleteVertexArrays(1, &VAO);
    doneCurrent();
}

void TurboOpenGLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);
    // 绑定VAO和VBO对象
    glBindVertexArray(VAO);

    shader_program_.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/resources/shader.vert");
    shader_program_.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/resources/shader.frag");
    bool success = false;
    success = shader_program_.link();
    if(!success)
    {
        std::cout << "shader is failed" << std::endl;
    }
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // 为当前绑定到target的缓冲区对象创建一个新的数据存储
    // 如果data不是NULL, 则使用来自此指针的数据初始化数据存储
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // 告知显卡如何解析缓冲里面的属性值
    int location = shader_program_.attributeLocation("aPos");
    glVertexAttribPointer(location, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void *)0);
    // 开启VAO管理的第一个属性的值
    glEnableVertexAttribArray(location);

    int location2 = shader_program_.attributeLocation("aColor");
    glVertexAttribPointer(location2, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void *)(3*sizeof(float)));
    // 开启VAO管理的第一个属性的值
    glEnableVertexAttribArray(location2);

    int location3 = shader_program_.attributeLocation("aTexCord");
    glVertexAttribPointer(location3, 2, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void *)(6*sizeof(float)));
    // 开启VAO管理的第一个属性的值
    glEnableVertexAttribArray(location3);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    texture_wall_ = new QOpenGLTexture(QImage(":/resources/wall.jpg"));

    // 释放VAO和VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);


}

void TurboOpenGLWidget::paintGL()
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    shader_program_.bind();
    glBindVertexArray(VAO);
//    glDrawArrays(GL_TRIANGLES, 0, 6);

//     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    switch(shape_)
    {
        case Rect:
            texture_wall_->bind();
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
            break;
    }
    update();
}

void TurboOpenGLWidget::resizeGL(int w, int h)
{

}

void TurboOpenGLWidget::drawShape(TurboOpenGLWidget::Shape shape)
{
    shape_ = shape;
}

void TurboOpenGLWidget::setWireFrame(bool mode)
{
    makeCurrent();
    if(mode)
    {
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    }
    else
    {
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    }
}
#include <QTime>
void TurboOpenGLWidget::timeout()
{
    if(shape_)
    {
        return;
    }
    makeCurrent();
    int time = QTime::currentTime().second();
    float green = (sin(time) / 2.0f) + 0.5f;
    shader_program_.setUniformValue("ourColor", 0.0f, green, 0.0f, 1.0f);
}

纹理单元

OpenGL保证至少16个纹理单元,也就是说你可以激活从GL_TEXTURE0到GL_TEXTURE15。他们都是按照顺序定义的, GL_TEXTURE0+8可以获得GL_TEXTURE8

以下是QT主要代码,在gitee项目中查看完整代码。

texture_wall_ = new QOpenGLTexture(QImage(":/resources/wall.jpg").mirrored());
texture_le_ = new QOpenGLTexture(QImage(":/resources/awesomeface.png").mirrored());

shader_program_.bind();
shader_program_.setUniformValue("textureWall", 0);
shader_program_.setUniformValue("textureSmile", 1);

纹理环绕

环绕方式 描述
GL_REPEAT 对纹理的默认行为。重复纹理图像。
GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。

相关代码请到gitee查看,这里不复制

纹理过滤

纹理坐标不依赖于分辨率,OpenGL需要知道怎么将纹理像素映射到纹理坐标;

可以想象你打开一张图片,不断放大,会发现它是由无数像素点组成的,这个点就是纹理像素

  • 纹理坐标的精度是无限的,可以是任意浮点值
  • 纹理像素是有限的(图片分辨率)
  • 一个像素需要一个颜色
  • 所谓采样就是通过纹理坐标,问图片要纹理像素的颜色值

大图片贴小面片时:纹理的精度高,相邻纹理像素往往色差不打,无需融合,直接就近选取即可。

主要函数:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

相关代码请到gitee查看,这里不复制

多级渐远纹理

简单来说就是一系列的纹理图像,根据观察者与物体的距离,参考临界值,选择最适合物体的距离的那个纹理

OpenGL有一个glGenerateMipmaps函数,可以生产多级渐远纹理

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

主要函数:

texture_small_->generateMipMaps();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

相关代码请到gitee查看,这里不复制

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

QT+ OpenGL学习 的相关文章

随机推荐

  • 信号和槽

    1 信号和槽是一种高级接口 应用于对象之间的通信 它是QT的核心特性 也是QT区别于其它工具包的重要地方 信号和槽是QT自行定义的一种通信机制 2 moc Meta ObjectCompiler QT工具 该工具是一个C 预处理程序 它为高
  • Charles 安装及配置,详细步骤

    一 安装激活 1 1 下载 https www charlesproxy com download 1 2 激活 打开Charles gt Help gt Register Charles gt 输入 Registered Name htt
  • 2020.11.13 奇偶链表

    2020 11 13 奇偶链表 题目描述 给定一个单链表 把所有的奇数节点和偶数节点分别排在一起 请注意 这里的奇数节点和偶数节点指的是节点编号的奇偶性 而不是节点的值的奇偶性 请尝试使用原地算法完成 你的算法的空间复杂度应为 O 1 时间
  • Java:List<Map>指定Key去重,差集(针对集合里面只有Map非实体对象)

    ListMap去重 差集 针对集合里面只有Map非实体对象 问题 由于业务需求 接口的入参出参都是List格式 对于有些查出来的数据 为了防止重复的数据 需要对集合去重 以下分享一种去重的方法 该方法可根据指定字段对List
  • 最全 VxLAN 知识详解

    什么是VXLAN VXLAN Virtual eXtensible Local Area Network 虚拟扩展局域网 是由IETF定义的NVO3 Network Virtualization over Layer 3 标准技术之一 是对
  • 容器修改完成的镜像打包到自己的docker hub

    容器修改完成的镜像打包到自己的docker hub 一 步骤 首先 我们基于当前的容器进行了修改 比如 我们首先创建了一个Ubuntu的容器 然后在容器当中安装了python3 安装了Django框架 安装NGINX服务器 安装了mysql
  • rt-thread中使用WebClient WebNet总结 http学习

    HTTP学习资料 1 需求背景 WebClient主要用来传输文件 WebNet用来支持cgi接口 需要支持get post put delete方式 2 webnet中使用 2 1 webnet存在问题 2 11 rt thread 使用
  • Web基础 HTML标签 六种超链接标签的使用方式

    超链接标签 重点 1 链接的语法格式 a href 跳转目标链接 target self 文本或图像 a a 标签里的a是单词anchor的的缩写 意为 锚 两个属性的作用如下 属性 作用 href 用于指定链接目标的url地址 必须属性
  • 【物联网毕设基础】NBIOT 窄带物联网

    文章目录 1 简介 2 NBIOT简介 3NB的型号介绍 3 1 BC95 3 2 BC35 3 3 BC28 3 4 BC26 3 5 BC20 3 6 BC30 4 NB物联网卡 5 OpenCPU 6 BC260模块详解 6 1 基本
  • 【周末闲谈】二进制VS三进制

    个人主页 个人主页 系列专栏 周末闲谈 周末闲谈 第一周 二进制VS三进制 文章目录 周末闲谈 前言 一 效率 二 三进制计算机 三进制计算机的最后 总结 前言 作为计算机是20世纪最先进的科学技术发明之一 对人类的生产活动和社会活动产生了
  • yolov7 onnx tensorrt 批量预测 全网首发

    目录 deepstream yolov7 mask yolov5的TensorRT部署 动态batch 开源tensorrt 调研笔记 tensorrt 加载模型batch size为 1的原因
  • JS解混淆-AST还原案例

    目录 一 js混淆了解 1 为什么要混淆 2 常见的混淆模样 ob sojson jsfuck AAencode jjEncode eval 二 AST初步认识 三 解混淆常用的方法 一 js混淆了解 1 为什么要混淆 js混淆的作用 为了
  • 为什么计算机中的整数要用补码表示?补码表示有什么好处?

    为什么计算机中的整数要用补码表示 补码表示有什么好处 在计算机中 补码可谓是十分神奇而又重要的存在 我们知道整数在计算机内部的机器数一般都是补码表示的 这里给出几个这样表示的好处 符号位可以和数值为一起参加运算 比如俩个负数相加 只要结果在
  • 风投平台

    一 天使湾创投 http www tisiwi com 二 天使汇 http angelcrunch com 三 蚂蚁天使 https www mayiangel com index htm 四 梦想小镇孵化器平台 http www dre
  • docker+jenkins+git搭建java自动化部署

    一 杂言 首先今天在写这篇文章的时候 刚好LOL洲际赛 RW赛前不被看好的情况下 为LPL扳回一城 RNG成功的在BO5最后一场拿下AFS LPL成功的拿下了洲际赛的冠军 恭喜LPL 田忌赛马的故事大家都耳熟能详 但是不可避免的也会出现逆转
  • pandas读取和存储CSV文件

    import pandas as pd import numpy as np read csv data pd read csv data temp 0 csv header None data DataFrame object data
  • 前端核心手写面试题(看你的马步扎实不扎实)

    防抖 防抖
  • c++类和对象--封装--属性和行为做整体

    封装的意义 1 将属性和行为当做一个整体来表现对象 类中的属性和行为统称为成员 属性又叫成员属性或成员变量 行为又叫成员函数或成员方法 案例 设计一个圆类 求圆的周长 include
  • 华为OD机试真题 Java 实现【机器人活动区域】【2023Q1 200分】

    一 题目描述 现有一个机器人 可放置于 M N的网格中任意位置 每个网格包含一个非负整数编号 当相邻网格的数字编号差值的绝对值小于等于 1 时 机器人可在网格间移动 问题 求机器人可活动的最大范围对应的网格点数目 说明 1 网格左上角坐标为
  • QT+ OpenGL学习

    文章目录 QT OpenGL QOpenGLWidget 不需要GLFW QOpenGLFunction X X Core 不需要GLAD 你好 三角形 顶点输入 顶点着色器 片段着色器 链接着色器 本节代码 元素缓冲对象EBO QT交互