00 QT+OpenGL入门笔记(完结) (2024)

自主学习笔记,若同时也能帮到正在阅读的你,万分荣幸! (2022/05/22 12:13)

一、关于OpenGL的一些重要说明

1. OpenGL是什么?

OpenGL一般它被认为是一个API(Application Programming Interface, 应用程序编程接口),包含了一系列可以操作图形、图像的函数。然而,OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范(Specification)。

OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由编写OpenGL库的人自行决定。

2. 立即渲染模式(Immediate mode) && 核心模式(Core-profile)

早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便。

但OpenGL的大多数功能都被库隐藏起来,开发者很少有控制OpenGL如何进行计算的自由,核心模式由此应运而生。

从OpenGL3.2开始,规范文档开始废弃立即渲染模式。

3. 状态机

OpenGL自身是一个巨大的状态机(State Machine) :也即一系列的变量描述OpenGL此刻应当如何运行。

OpenGL的状态通常被称为OpenGL上下文(Context)。

e.g. 当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL的状态,从而告诉OpenGL如何去绘图。一旦修改OpenGL的状态为绘制线段,下一个命令就会画出线段而不是三角形。

4. 对象

1)OpenGL在开发的时候引入了一些抽象层,对象(Object)就是其中一个。

2)可将OpenGL中的对象看做C语言中的结构体。

e.g. 可以用一个对象来代表绘制窗口的设置,之后就可以设置它的大小、颜色的等。

struct object_name{ float set_window_size; // 这里调用的是OpenGL提供的设置窗口大小的某个方法 float set_window_color; // 这里调用的是OpenGL提供的设置窗口颜色的某个方法 ...}

3)对象和上下文的关系

// OpenGL的状态struct OpenGL_Context { ... object* object_Window_Target; // 某个对象 ... };

4)创建对象的大致流程

e.g. 以创建窗口对象为例

// 1. 创建对象unsigned int objectId = 0; // 用一个无符号int型变量保存创建对象的唯一ID号,该ID对应的对象名为objectIdglGenObject(1, &objectId); // 使用glGenObject方法进行对象的,第一个参数是要生成对象的数量// 2. 绑定对象至上下文glBindObject(GL_WINDOW_TARGET, objectId); // 窗口对象目标的位置被定义成GL_WINDOW_TARGET,可以进行窗口相关设置// 3. 设置当前绑定到 GL_WINDOW_TARGET 的对象的一些选项glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800); // 通过GL_WINDOW_TARGET设定窗口相关属性,窗口宽度glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600); // 通过GL_WINDOW_TARGET设定窗口相关属性,窗口高度// 4. 将上下文对象设回默认glBindObject(GL_WINDOW_TARGET, 0); // 0表示解绑这个对象,但是前面设置的关于窗口的长和宽会被保存在objectId所引用的对象中// 【注】一旦重新绑定这个对象objectId到GL_WINDOW_TARGET,上面的选项会被重新启动,就是前面的设置会生效;// 重新绑定的代码和初次绑定的代码相同:glBindObject(GL_WINDOW_TARGET, objectId);

5)使用对象的好处

在一个程序中,我们不止可以定义一个对象,并设置它们的选项,每个对象都可以是不同的设置。

当我们执行一个使用OpenGL状态的操作的时候,只需要绑定含有需要的设置的对象即可(直接调用该对象在之前设置的属性等)。

e.g. 比如说我们有一些作为3D模型数据(一栋房子或一个人物)的容器对象,在我们想绘制其中任何一个模型的时候,只需绑定一个包含对应模型数据的对象就可以了(当然,我们需要先创建并设置对象的选项)

二、QT中使用OpenGL

1. 使用QOpenGLWidget类的说明

QOpenGLWidget类提供了三个便捷的虚函数,可以重载,用来重新实现典型的OpenGL任务:

1)paintGL () :渲染OpenGL窗口,当窗口widget需要更新时调用;

a)在paintGL()以外的地方调用绘制函数是没有意义的,因为绘制图像最终将被paintGL()覆盖;b)若需要从paintGL()以外的位置触发重新绘制(e.g. 使用计时器设置场景动画),则应调用widget的update()函数来安排更新。

2)resize () :设置OpenGL视口、投影等,当widget调整大小(或首次显示)时调用;

3)initializeGL () : 设置OpenGL资源和状态,最先调用且调用一次。

2. 关于QOpenGLFunctions_X_X_Core的说明

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

其中的initializeOpenGLFunctions用于初始化OpenGL函数,将Qt里的函数指针指向显卡的函数,之后调用的OpenGL函数才是可用的。

3. 实验:实现“Hello World”

1)创建Qt项目

00 QT+OpenGL入门笔记(完结) (1)

2)让其支持OpenGL

step1:创建类TestOpenGLWidget

00 QT+OpenGL入门笔记(完结) (2)

step2:让该类继承自OpenGL相关的类,并初始化需要实现的虚函数:

#ifndef TESTOPENGLWIDGET_H#define TESTOPENGLWIDGET_H// 1.引入相关库#include <QOpenGLWidget>#include <QOpenGLFunctions_3_3_Core>// 2.继承相关类class TestOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core{ Q_OBJECTpublic: explicit TestOpenGLWidget(QWidget *parent = nullptr);protected: // 3.重载相关虚函数 virtual void initializeGL(); virtual void resizeGL(int w, int h); virtual void paintGL();signals:};#endif // TESTOPENGLWIDGET_H

step3:实现上述三个虚函数

#include "testopenglwidget.h"TestOpenGLWidget::TestOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent){}void TestOpenGLWidget::initializeGL(){ // 1.初始化OpenGL函数,否则OpenGL函数不可调用 initializeOpenGLFunctions();}void TestOpenGLWidget::resizeGL(int w, int h){}void TestOpenGLWidget::paintGL(){ // 2.initializeOpenGLFunctions();执行后,下面的函数才有执行的意义 // 设置窗口颜色 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT);}

4)创建一个OpenGL Widget窗口,并将其类提升为新创建的TestOpenGLWidget

step1:右键openGLWidget窗口/提升为,提升后的界面:

00 QT+OpenGL入门笔记(完结) (3)

step2:在MainWindow.cpp中将新创建的openGLWidget设置为centralWidget,让其铺满整个屏幕:

#include "mainwindow.h"#include "ui_mainwindow.h"MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow){ ui->setupUi(this); setCentralWidget(ui->openGLWidget);}MainWindow::~MainWindow(){ delete ui;}

5)执行代码,结果如下:

00 QT+OpenGL入门笔记(完结) (4)

三、初步理解着色器

1. 准备工作:创建一个为后续学习使用的窗口界面

step1:在窗口的空白处右击添加工具栏

00 QT+OpenGL入门笔记(完结) (5)

step2:在Action Editor内创建两个“工具“

00 QT+OpenGL入门笔记(完结) (6)

step3:将他们添加到工具栏

00 QT+OpenGL入门笔记(完结) (7)

step4:执行代码,测试结果:

00 QT+OpenGL入门笔记(完结) (8)

2. 视口

在开始渲染之前必须告诉OpenGL渲染窗口的尺寸大小,即视口(Viewport),这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。

通过调用glViewport函数来设置窗口的维度(Dimension):

glViewport(0, 0, 800, 600); // 前两个参数控制窗口左下角的位置,第三个和第四个参数控制渲染窗口的宽度和高度(像素)

在OpenGL中给的坐标范围只能是[-1,1],最终会被映射为屏幕坐标(与程序员glViewport()中的传参有关)。

e.g. 使用glViewport(0, 0, 800, 600),且OpenGL中的坐标是(-0.5, 0.5)。

最终(-0.5, 0.5)会被映射为屏幕坐标(200,450)。

3. 实验:绘制“三角形”

目标:通过OpenGL(CPU端)给定的数据,在GLSL(GPU端)进行三角形的绘制。

0)基础概述

OpenGL中所有的事物都是在3D空间中,但屏幕和窗口时2D像素数组,这导致OpenGL的大部分工作是把3D坐标转变为适应你屏幕的2D像素。

3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline)管理的。

图形渲染管线的两项主要工作:

a)将3D坐标转换为2D坐标;

b)将2D坐标转变为实际的有颜色的像素。

2D坐标 && 像素:2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到你的屏幕或窗口分辨率的限制。

图形渲染管线的可编程部分:顶点着色器(必选)、几何着色器(可选)、片段着色器(必选)。

00 QT+OpenGL入门笔记(完结) (9)

在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

片段 && 像素:一个片段是OpenGL渲染一个像素所需的所有数据。通常,片段着色器包含3D场景的数据(比如光照、阴影等),这些数据可以被用来计算最终像素的颜色(但由于渲染管线后面还有测试和混合,故片段着色器计算出来的一个像素输出的颜色,并不一定是在窗口中显示的最终的颜色)

1)顶点的输入

OpenGL仅当3D坐标在3个轴x,y,z在[-1,1]范围内才处理它。所有在标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上。

一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了(屏幕坐标的顶点在左上角,这里的顶点是最终渲染图像的中心点)

标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport函数提供的数据,进行视口变换(Viewport Transform)完成的。所得的屏幕空间坐标又会被变换为片段输入到片段着色器中。

顶点缓冲对象(Vertex Buffer Objects, VBO)

OpenGL对象,其会在GPU内存(称为显存)中存储大量顶点(和其他的OpenGL对象一样,有一个独一无二的ID)。

// 1.创建一个VBO对象(OpenGL中)unsigned int VBO;glGenBuffers(1, &VBO); // 使用取地址获得唯一ID// 2.使用glBindBuffer函数把新创建的对象绑定到GL_ARRAY_BUFFER目标上// 【注】此时,任何在GL_ARRAY_BUFFER目标上的缓冲调用都会用来配置当前绑定的缓冲VBOglBindBuffer(GL_ARRAY_BUFFER, VBO); // 3.调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中,VBO就能访问到这些数据了// GL_ARRAY_BUFFER对象// 数据大小// 数据源// GL_STATIC_DRAW:表示数据不会或几乎不会改变glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);// .........................// 至此,已经把顶点数据储存在显卡的内存中,用创建的这个VBO顶点缓冲对象管理

【注】顶点缓冲对象变量需要绑定的目标是GL_ARRAY_BUFFER。

2)顶点着色器

使用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器。

// GLSL版本号要和OpenGL版本号匹配#version 330 core// 定义location为0的输入变量名为aPos:只用location进行关联,aPos和OpenGL中设定的变量名是无关的// in是关键字layout (location = 0) in vec3 aPos;void main(){ // gl_Position内置关键字,为顶点着色器的输出的顶点位置数据 gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);}

3)编译着色器

可以将顶点着色器暂时以字符串的形式进行保存:

// 以常量字符串的形式进行存储const char *vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" "}\0";

为了能够使OpenGL使用它,必须在运行时动态编译它的源代码。

下面创建顶点着色器对象:

// 创建的是顶点着色器对象,故还是使用ID进行引用,变量类型存储为unsigned intunsigned int vertexShader;// 需要创建的着色器类型以参数形式提供给glCreateShader// 由于需要创建一个顶点着色器,故传递的参数是GL_VERTEX_SHADERvertexShader = glCreateShader(GL_VERTEX_SHADER);

把着色器源码附加到着色器对象上,然后编译它:

// 第一个参数:需要编译的着色器对象// 第二个参数:传递的源码字符串数量,这里只传递了一个字符串// 第三个参数:顶点着色器真正的源码glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);// 编译顶点着色器glCompileShader(vertexShader);

检测编译情况的代码:

int success;char infoLog[512];// glGetShaderiv用来检查是否编译成功glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);// 上面的返回值为success,如果不成功,进入下面的分支if(!success){ // 获得错误信息,并将错误信息放在infoLog中进行存储 glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); // 打印错误信息 std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}

4)片段着色器

在OpenGL或GLSL中定义颜色的分量都是被强制归一化到[0.0,1.0]之间的。

片段着色器只需要一个输出变量,且这个输出变量是vec4(RGBA),表示当前片段的颜色输出。

// 片段着色器编写的内容#version 330 coreout vec4 FragColor;void main(){ FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);} 

同顶点着色器一样进行创建和编译工作:

// 创建片段着色器unsigned int fragmentShader;fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);// 把着色器源码附加到着色器对象上glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);// 编译glCompileShader(fragmentShader);// .............................// 至此,两个着色器都已编译,剩下的事情是把两个着色器对象连接到一个用来渲染的着色器程序(Shader Program)中。

5)着色器程序

着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。

若要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序

已激活着色器程序的着色器在我们发送渲染调用的时候被使用。

创建一个着色器程序,并链接之前编译好的着色器:

// 创建着色器程序unsigned int shaderProgram;// 【注】这里是glCreateProgram而非glCreateShadershaderProgram = glCreateProgram();// 把着色器附着到程序上glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);// 使用glLinkProgram进行链接glLinkProgram(shaderProgram);// 当在把着色器对象链接到程序对象以后,删除着色器对象glDeleteShader(vertexShader);glDeleteShader(fragmentShader);

检测着色器程序链接是否正常:

// 同着色器检测方式一样glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);if(!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); ...}

若要使用该程序对象,必须激活:

// 激活程序代码:此后每个着色器调用和渲染调用都会只用这个着色器程序内的着色器了glUseProgram(shaderProgram);// ......................................// 至此,已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它。// 但是OpenGL还不知道它该如何解释内存中的顶点数据,以及如何将顶点数据链接到顶点着色器的属性上// (因为顶点缓冲对象只是放了一堆数据,这些数据有什么意义,是表示位置坐标还是颜色等等,OpenGL并不知道)

6)链接顶点属性

在渲染前指定OpenGL该如何解释顶点数据,因为传到顶点着色器的是一坨并没有实际意义的数据。

假设现在顶点数据为:

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

解析数据:

// 第一个参数:这里的0和顶点着色器的location = 0对应// 第二个参数:指定顶点属性的大小,这里说明每个顶点是个vec3的数据// 第三个参数:说明vec3中每个数的类型是GL_FLOAT类型// 第四个参数:表明是否需要将数据进行标准化,若设置为true,则数据会被归一化到[0,1]之间// 第五个参数:步长,表示连续的顶点属性组之间的间隔,表示下一个顶点应该跨越的距离为多少才能取到值// 第六个参数:偏移量,表示在当前这个步长内取值是否需要进行偏移,这里直接不用偏移,直接从第0个位置开始取值glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);// 启用location = 0的顶点属性glEnableVertexAttribArray(0);

7)顶点数组对象

顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会存储在这个VAO中。

好处:当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使得不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。

【注】相同的数据会因为绑定了不同的VAO被解析出不同的意义。

VAO的创建和配置相关:

// 创建一个VAO对象unsigned int VAO1;glGenVertexArrays(1, &VAO1);// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..// 1. 绑定VAOglBindVertexArray(VAO1);// 2. VBO的数据glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);// 3. 开始进行数据解析glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// ..:: 绘制代码(渲染循环中) :: ..// 绘制物体glUseProgram(shaderProgram);// 在使用时重新绑定一下VAO1,也就是使用了刚才的一系列的配置glBindVertexArray(VAO1);// 一些绘制工作相关(绘制完进行VAO的解绑)// ......paint

8)绘制三角形

// 一个绘制函数// 第一个参数:GL_TRIANGLES说明绘制的是三角形图元// 第二个参数:指定了顶点数组的起始索引,这里是从顶点数组的第0个数据开始// 第三个参数:打算绘制几个顶点,这里是绘制三个顶点glDrawArrays(GL_TRIANGLES, 0, 3);

testopenglwidget.h

#ifndef TESTOPENGLWIDGET_H#define TESTOPENGLWIDGET_H#include <QOpenGLWidget>#include <QOpenGLFunctions_3_3_Core>class TestOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core{ Q_OBJECTpublic: explicit TestOpenGLWidget(QWidget *parent = nullptr); // 顶点的数据:没有解析的数据是没有意义的 // 内存中的数据,关键是如何将内存中的数据给显卡 float vertices_data[9] = { // 所有的值是在[-1, 1]之间的 -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f }; // VAO和VBO变量(无符号整型) unsigned int VAO_id,VBO_id; // 着色器变量 unsigned int shaderProgram1_id;protected: virtual void initializeGL(); virtual void resizeGL(int w, int h); virtual void paintGL();signals:};#endif // TESTOPENGLWIDGET_H

testopenglwidget.cpp

#include "testopenglwidget.h"const char *vertexShaderSource = "#version 330 core\n" "layout(location = 0) in vec3 aPos;\n" "void main()\n" "{\n" "gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);\n" "}\n\0";const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" "FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}\n\0";TestOpenGLWidget::TestOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent){}void TestOpenGLWidget::initializeGL(){ initializeOpenGLFunctions(); // ------------------------一、VAO和VBO------------------------ // 1.创建VAO和VBO对象,并赋予ID(使用Gen) glGenVertexArrays(1, &VAO_id); glGenBuffers(1, &VBO_id); // 2.绑定VAO,开始记录属性相关 glBindVertexArray(VAO_id); // 3.绑定VBO(一定是先绑定VAO再绑定VBO) glBindBuffer(GL_ARRAY_BUFFER, VBO_id); // 4.把数据放进VBO glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_data), vertices_data, GL_STATIC_DRAW); // 5.解析数据 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); // 6.开启location = 0的属性解析 glEnableVertexAttribArray(0); // 7.解绑VBO和VAO glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // -----------------------------end--------------------------- // ------------------------二、着色器相关------------------------ // 创建顶点着色器 unsigned int vertexShader_id = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader_id, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader_id); // 创建片段着色器 unsigned int fragmentShader_id = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader_id, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader_id); // -----------------------------end--------------------------- // -----------------------三、着色器程序相关---------------------- shaderProgram1_id = glCreateProgram(); glAttachShader(shaderProgram1_id, vertexShader_id); glAttachShader(shaderProgram1_id, fragmentShader_id); glLinkProgram(shaderProgram1_id); // -----------------------------end--------------------------- // 删除顶点着色器和片段着色器(不能将着色器程序也给delete掉) glDeleteShader(vertexShader_id); glDeleteShader(fragmentShader_id);}void TestOpenGLWidget::resizeGL(int w, int h){}void TestOpenGLWidget::paintGL(){ glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 【画一个图形时需要说使用哪个着色器】 glUseProgram(shaderProgram1_id); // 使用时还需要再绑定一次 glBindVertexArray(VAO_id); // 开始绘制 glDrawArrays(GL_TRIANGLES, 0, 3);}

程序执行结果:

00 QT+OpenGL入门笔记(完结) (10)

四、进一步理解着色器

1)现在需要绘制一个四边形(由于OpenGL的绘制面的图元是三角形,所以需要绘制两个三角形拼成一个四边形),则更改前面的代码。

// 顶点数据部分更改为float vertices_data[18] = { // 所有的值是在[-1, 1]之间的 // 第一个三角形 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 };// 绘制图形部分更改为// 第三个参数改为6,因为现在是绘制6个点glDrawArrays(GL_TRIANGLES, 0, 6);

但现在其实是在做冗余的工作:

00 QT+OpenGL入门笔记(完结) (11)

所以,其实只需要开辟4个点的内存(对待顶点数更多的模型来说会浪费更多的资源)。解决思路是只存储不同的顶点,使用一种标记来指定这些顶点的绘制绘制顺序。

需要引入一个OpenGL对象:EBO。

索引缓冲对象(Element Buffer Object, EBO):和顶点缓冲对象一样,EBO也是一个缓冲,它专门存储索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点。

创建索引缓冲对象:

// 创建EBOunsigned int EBO;glGenBuffers(1, &EBO);// 【VAO没有取消绑定时:会记录正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象】// ==>这样在绑定VAO的同时也会自动绑定EBO// 绑定EBOglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);// 把索引负值到缓冲里,传递GL_ELEMENT_ARRAY_BUFFER作为缓冲目标glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);// ...// 绘制时:// glDrawElements使用当前绑定的索引缓冲对象中的索引进行绘制// 第一个参数:绘制模式// 第二个参数:打算绘制的顶点个数// 第三个参数:索引的类型// 第四个参数:指定EBO中的偏移量glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

【注】EBO的绑定一定要在VAO解绑之前绑定!

故,最终的代码应当如下:

#include "testopenglwidget.h"const char *vertexShaderSource = "#version 330 core\n" "layout(location = 0) in vec3 aPos;\n" "void main()\n" "{\n" "gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);\n" "}\n\0";const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" "FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}\n\0";TestOpenGLWidget::TestOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent){}void TestOpenGLWidget::initializeGL(){ initializeOpenGLFunctions(); // ------------------------一、VAO、VBO和EBO------------------------ // 1.创建相关对象 glGenVertexArrays(1, &VAO_id); glGenBuffers(1, &VBO_id); glGenBuffers(1, &EBO_id); // 2.绑定VAO glBindVertexArray(VAO_id); // 3.配置VBO相关 glBindBuffer(GL_ARRAY_BUFFER, VBO_id); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_data), vertices_data, GL_STATIC_DRAW); // 4.配置EBO相关 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_id); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 5.解析数据并启动解析 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 解绑VBO和VAO(注:不能解绑EBO) glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // ------------------------二、着色器相关------------------------ // 创建顶点着色器 unsigned int vertexShader_id = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader_id, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader_id); // 创建片段着色器 unsigned int fragmentShader_id = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader_id, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader_id); // -----------------------------end--------------------------- // -----------------------三、着色器程序相关---------------------- shaderProgram1_id = glCreateProgram(); glAttachShader(shaderProgram1_id, vertexShader_id); glAttachShader(shaderProgram1_id, fragmentShader_id); glLinkProgram(shaderProgram1_id); // -----------------------------end--------------------------- // 删除顶点着色器和片段着色器(不能将着色器程序也给delete掉) glDeleteShader(vertexShader_id); glDeleteShader(fragmentShader_id);}void TestOpenGLWidget::resizeGL(int w, int h){}void TestOpenGLWidget::paintGL(){ glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 【画一个图形时需要说使用哪个着色器】 glUseProgram(shaderProgram1_id); // 使用时还需要再绑定一次 glBindVertexArray(VAO_id); // 开始绘制 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);}

运行结果截图:

00 QT+OpenGL入门笔记(完结) (12)

【注】glBindXX的参数是0表示对某个OpenGL对象进行解绑的工作。

2)线框模式和填充模式

为了使这个四边形看起来更直观,引入线框模式和填充模式(默认是填充模式)。

将下面的代码添加到初始化的地方即可:

// 函数配置OpenGL如何绘制图元。// 第一个参数:将其应用到所有的三角形的正面和背面;// 第二个参数:用线来绘制。// 之后的绘制调用会一直以线框模式绘制三角形,直到我们用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)将其设置回默认模式。glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
00 QT+OpenGL入门笔记(完结) (13)

五、一些练习巩固知识

1. 实验:绘制两个彼此相连的三角形

需求:添加更多顶点到数据中,使用glDrawArrays,绘制两个彼此相连的三角形。

1).h文件的内容:

#ifndef TESTOPENGLWIDGET_H#define TESTOPENGLWIDGET_H#include <QOpenGLWidget>#include <QOpenGLFunctions_3_3_Core>class TestOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core{ Q_OBJECTpublic: explicit TestOpenGLWidget(QWidget *parent = nullptr); // 顶点的数据:没有解析的数据是没有意义的 // 内存中的数据,关键是如何将内存中的数据给显卡 float vertices_data[18] = { // 所有的值是在[-1, 1]之间的 // 第一个三角形的顶点数据 -0.9f, -0.5f, 0.0f, 0.0f, -0.5f, 0.0f, -0.45f, 0.5f, 0.0f, // 第二个三角形的顶点数据 0.0f, -0.5f, 0.0f, 0.9f, -0.5f, 0.0f, 0.45f, 0.5f, 0.0f }; // VAO和VBO变量(无符号整型) unsigned int VAO_id,VBO_id, EBO_id; // 着色器变量 unsigned int shaderProgram1_id;protected: virtual void initializeGL(); virtual void resizeGL(int w, int h); virtual void paintGL();signals:};#endif // TESTOPENGLWIDGET_H

2).cpp文件的内容

#include "testopenglwidget.h"const char *vertexShaderSource = "#version 330 core\n" "layout(location = 0) in vec3 aPos;\n" "void main()\n" "{\n" "gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);\n" "}\n\0";const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" "FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}\n\0";TestOpenGLWidget::TestOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent){}void TestOpenGLWidget::initializeGL(){ initializeOpenGLFunctions(); // ------------------------一、VAO、VBO和EBO------------------------ // 1.创建相关对象 glGenVertexArrays(1, &VAO_id); glGenBuffers(1, &VBO_id); // 2.绑定VAO glBindVertexArray(VAO_id); // 3.配置VBO相关 glBindBuffer(GL_ARRAY_BUFFER, VBO_id); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_data), vertices_data, GL_STATIC_DRAW); // 5.解析数据并启动解析 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 解绑VBO和VAO(注:不能解绑EBO) glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // ------------------------二、着色器相关------------------------ // 创建顶点着色器 unsigned int vertexShader_id = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader_id, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader_id); // 创建片段着色器 unsigned int fragmentShader_id = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader_id, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader_id); // -----------------------------end--------------------------- // -----------------------三、着色器程序相关---------------------- shaderProgram1_id = glCreateProgram(); glAttachShader(shaderProgram1_id, vertexShader_id); glAttachShader(shaderProgram1_id, fragmentShader_id); glLinkProgram(shaderProgram1_id); // -----------------------------end--------------------------- // 删除顶点着色器和片段着色器(不能将着色器程序也给delete掉) glDeleteShader(vertexShader_id); glDeleteShader(fragmentShader_id); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);}void TestOpenGLWidget::resizeGL(int w, int h){}void TestOpenGLWidget::paintGL(){ glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 【画一个图形时需要说使用哪个着色器】 glUseProgram(shaderProgram1_id); // 使用时还需要再绑定一次 glBindVertexArray(VAO_id); // 开始绘制 glDrawArrays(GL_TRIANGLES, 0, 6);}

实验结果:

00 QT+OpenGL入门笔记(完结) (14)

2. 实验:使用两个VAO和VBO创建两个三角形

需求:使用一个VAO1和VBO1构建一个三角形;使用另一个VAO2和VBO2创建另一个三角形。

思路:使用VAOs_id[0]记录VBOs_id[0]的相关信息,将数据放在location = 0的位置;

使用VAOs_id[1]记录VBOs_id[1]的相关信息,将数据放在location = 0的位置。

在顶点着色器中,在location = 0的位置去取数据。

1).h文件

#ifndef TESTOPENGLWIDGET_H#define TESTOPENGLWIDGET_H#include <QOpenGLWidget>#include <QOpenGLFunctions_3_3_Core>class TestOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core{ Q_OBJECTpublic: explicit TestOpenGLWidget(QWidget *parent = nullptr); // 顶点的数据:没有解析的数据是没有意义的 // 内存中的数据,关键是如何将内存中的数据给显卡 float firstTriangle[9] = { // 第一个三角形的顶点数据 -0.9f, -0.5f, 0.0f, 0.0f, -0.5f, 0.0f, -0.45f, 0.5f, 0.0f, }; float secondTriangle[9] = { // 第二个三角形的顶点数据 0.0f, -0.5f, 0.0f, 0.9f, -0.5f, 0.0f, 0.45f, 0.5f, 0.0f }; // VAO和VBO变量变成数组了 unsigned int VAOs_id[2],VBOs_id[2]; // 着色器变量 unsigned int shaderProgram1_id;protected: virtual void initializeGL(); virtual void resizeGL(int w, int h); virtual void paintGL();signals:};#endif // TESTOPENGLWIDGET_H

2).cpp文件

#include "testopenglwidget.h"const char *vertexShaderSource = "#version 330 core\n" "layout(location = 0) in vec3 aPos;\n" "void main()\n" "{\n" "gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);\n" "}\n\0";const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" "FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}\n\0";TestOpenGLWidget::TestOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent){}void TestOpenGLWidget::initializeGL(){ initializeOpenGLFunctions(); // ------------------------一、VAO、VBO和EBO------------------------ // 1.创建相关对象(创建2个对象,对分配ID号) // 因为是数组,数组名就是地址 glGenVertexArrays(2, VAOs_id); glGenBuffers(2, VBOs_id); // 2.绑定第一个三角形相关 glBindVertexArray(VAOs_id[0]); glBindBuffer(GL_ARRAY_BUFFER, VBOs_id[0]); glBufferData(GL_ARRAY_BUFFER, sizeof(firstTriangle), firstTriangle, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // 3.绑定第二个三角形相关 glBindVertexArray(VAOs_id[1]); glBindBuffer(GL_ARRAY_BUFFER, VBOs_id[1]); glBufferData(GL_ARRAY_BUFFER, sizeof(secondTriangle), secondTriangle, GL_STATIC_DRAW); // 还是放在location = 0的位置 // 第一个参数:顶点属性的位置值 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // ------------------------二、着色器相关------------------------ // 创建顶点着色器 unsigned int vertexShader_id = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader_id, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader_id); // 创建片段着色器 unsigned int fragmentShader_id = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader_id, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader_id); // -----------------------------end--------------------------- // -----------------------三、着色器程序相关---------------------- shaderProgram1_id = glCreateProgram(); glAttachShader(shaderProgram1_id, vertexShader_id); glAttachShader(shaderProgram1_id, fragmentShader_id); glLinkProgram(shaderProgram1_id); // -----------------------------end--------------------------- // 删除顶点着色器和片段着色器(不能将着色器程序也给delete掉) glDeleteShader(vertexShader_id); glDeleteShader(fragmentShader_id); // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);}void TestOpenGLWidget::resizeGL(int w, int h){}void TestOpenGLWidget::paintGL(){ glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 【画一个图形时需要说使用哪个着色器】 glUseProgram(shaderProgram1_id); // 数据都是在location = 0的位置 // 画第一个三角形 glBindVertexArray(VAOs_id[0]); glDrawArrays(GL_TRIANGLES, 0, 3); // 画第二个三角形 glBindVertexArray(VAOs_id[1]); glDrawArrays(GL_TRIANGLES, 0, 3);}

程序执行结果截图:

00 QT+OpenGL入门笔记(完结) (15)

3. 实验:创建两个着色器程序绘制两个三角形

需求:创建两个着色器程序,其中他们共用一个顶点着色器(即顶点数据现在都存放在location = 0的位置),使用不同的片段着色器,绘制两个不同的三角形。

1).h文件

#ifndef TESTOPENGLWIDGET_H#define TESTOPENGLWIDGET_H#include <QOpenGLWidget>#include <QOpenGLFunctions_3_3_Core>class TestOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core{ Q_OBJECTpublic: explicit TestOpenGLWidget(QWidget *parent = nullptr); // 顶点的数据:没有解析的数据是没有意义的 // 内存中的数据,关键是如何将内存中的数据给显卡 float firstTriangle[9] = { // 第一个三角形的顶点数据 -0.9f, -0.5f, 0.0f, 0.0f, -0.5f, 0.0f, -0.45f, 0.5f, 0.0f, }; float secondTriangle[9] = { // 第二个三角形的顶点数据 0.0f, -0.5f, 0.0f, 0.9f, -0.5f, 0.0f, 0.45f, 0.5f, 0.0f }; unsigned int VAOs_id[2],VBOs_id[2]; // 着色器变量 unsigned int shaderProgram1_id; // 着色器程序2 unsigned int shaderProgram1_id_2;protected: virtual void initializeGL(); virtual void resizeGL(int w, int h); virtual void paintGL();signals:};#endif // TESTOPENGLWIDGET_H

2).cpp文件

#include "testopenglwidget.h"const char *vertexShaderSource = "#version 330 core\n" "layout(location = 0) in vec3 aPos;\n" "void main()\n" "{\n" "gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);\n" "}\n\0";const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" "FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}\n\0";const char *fragmentShaderSource_2 = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" "FragColor = vec4(1.0f, 1.0f, 0.0f, 1.0f);\n" "}\n\0";TestOpenGLWidget::TestOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent){}void TestOpenGLWidget::initializeGL(){ initializeOpenGLFunctions(); // ------------------------一、VAO、VBO和EBO------------------------ glGenVertexArrays(2, VAOs_id); glGenBuffers(2, VBOs_id); // 2.绑定第一个三角形相关 glBindVertexArray(VAOs_id[0]); glBindBuffer(GL_ARRAY_BUFFER, VBOs_id[0]); glBufferData(GL_ARRAY_BUFFER, sizeof(firstTriangle), firstTriangle, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // 3.绑定第二个三角形相关 glBindVertexArray(VAOs_id[1]); glBindBuffer(GL_ARRAY_BUFFER, VBOs_id[1]); glBufferData(GL_ARRAY_BUFFER, sizeof(secondTriangle), secondTriangle, GL_STATIC_DRAW); // 还是放在location = 0的位置 // 第一个参数:顶点属性的位置值 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // ------------------------二、着色器相关------------------------ // 创建顶点着色器 unsigned int vertexShader_id = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader_id, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader_id); // 创建片段着色器1 unsigned int fragmentShader_id = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader_id, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader_id); // 创建片段着色器2 unsigned int fragmentShader_id_2 = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader_id_2, 1, &fragmentShaderSource_2, NULL); glCompileShader(fragmentShader_id_2); // -----------------------------end--------------------------- // -----------------------三、着色器程序相关---------------------- // 链接第一个着色器程序 shaderProgram1_id = glCreateProgram(); glAttachShader(shaderProgram1_id, vertexShader_id); glAttachShader(shaderProgram1_id, fragmentShader_id); glLinkProgram(shaderProgram1_id); // 链接第二个着色器程序 shaderProgram1_id_2 = glCreateProgram(); glAttachShader(shaderProgram1_id_2, vertexShader_id); glAttachShader(shaderProgram1_id_2, fragmentShader_id_2); glLinkProgram(shaderProgram1_id_2); // -----------------------------end--------------------------- // 删除顶点着色器和片段着色器(不能将着色器程序也给delete掉) glDeleteShader(vertexShader_id); glDeleteShader(fragmentShader_id); glDeleteShader(fragmentShader_id_2); // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);}void TestOpenGLWidget::resizeGL(int w, int h){}void TestOpenGLWidget::paintGL(){ glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 1.使用第一个着色器程序绘制第一个三角形 glUseProgram(shaderProgram1_id); glBindVertexArray(VAOs_id[0]); glDrawArrays(GL_TRIANGLES, 0, 3); // 2.使用第二个着色器程序绘制第二个三角形 glUseProgram(shaderProgram1_id_2); glBindVertexArray(VAOs_id[1]); glDrawArrays(GL_TRIANGLES, 0, 3);}

程序执行结果:

00 QT+OpenGL入门笔记(完结) (16)

六、在QT窗口进行简单的交互工作

需求:点击QT窗口的按钮实现简单的交互工作。

00 QT+OpenGL入门笔记(完结) (17)

响应窗口按键的实现流程:

1)添加功能函数:

testopenglwidget.h

#ifndef TESTOPENGLWIDGET_H#define TESTOPENGLWIDGET_H#include <QOpenGLWidget>#include <QOpenGLFunctions_3_3_Core>class TestOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core{ Q_OBJECTpublic: // 设置枚举量 enum Shape{None, Rect, Circle, Triangle}; // 定义一个公共的方法传递一个形状去决定画什么 void drawShape(Shape shape); void setWirefame(bool wireframe); explicit TestOpenGLWidget(QWidget *parent = nullptr); // 添加析构函数,进行一些资源的回收工作 ~TestOpenGLWidget(); // 顶点的数据:没有解析的数据是没有意义的 // 内存中的数据,关键是如何将内存中的数据给显卡 float vertices_data[18] = { // 所有的值是在[-1, 1]之间的 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[6] = { 0, 1, 3, // 第一个三角形 0, 2, 3 // 第二个三角形 }; // VAO和VBO变量(无符号整型) unsigned int VAO_id,VBO_id, EBO_id; // 着色器变量 unsigned int shaderProgram1_id;protected: virtual void initializeGL(); virtual void resizeGL(int w, int h); virtual void paintGL();private: // 定义一个变量去决定画什么 Shape m_shape;signals:};#endif // TESTOPENGLWIDGET_H

testopenglwidget.cpp

#include "testopenglwidget.h"const char *vertexShaderSource = "#version 330 core\n" "layout(location = 0) in vec3 aPos;\n" "void main()\n" "{\n" "gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);\n" "}\n\0";const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" "FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}\n\0";// 添加接口void TestOpenGLWidget::drawShape(TestOpenGLWidget::Shape shape){ m_shape = shape; // 重新调用绘制 update();}void TestOpenGLWidget::setWirefame(bool wireframe){ makeCurrent(); if(wireframe) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // 否则不能重新绘制 update(); doneCurrent();}TestOpenGLWidget::TestOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent){}TestOpenGLWidget::~TestOpenGLWidget(){ // 调出当前的状态 makeCurrent(); // 第一个参数:要删除的对象的个数为1 glDeleteBuffers(1, &VBO_id); glDeleteVertexArrays(1, &VAO_id); glDeleteProgram(shaderProgram1_id); doneCurrent();}void TestOpenGLWidget::initializeGL(){ initializeOpenGLFunctions(); // ------------------------一、VAO、VBO和EBO------------------------ // 1.创建相关对象 glGenVertexArrays(1, &VAO_id); glGenBuffers(1, &VBO_id); glGenBuffers(1, &EBO_id); // 2.绑定VAO glBindVertexArray(VAO_id); // 3.配置VBO相关 glBindBuffer(GL_ARRAY_BUFFER, VBO_id); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_data), vertices_data, GL_STATIC_DRAW); // 4.配置EBO相关 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_id); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 5.解析数据并启动解析 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 解绑VBO和VAO(注:不能解绑EBO) glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // ------------------------二、着色器相关------------------------ // 创建顶点着色器 unsigned int vertexShader_id = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader_id, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader_id); // 创建片段着色器 unsigned int fragmentShader_id = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader_id, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader_id); // -----------------------------end--------------------------- // -----------------------三、着色器程序相关---------------------- shaderProgram1_id = glCreateProgram(); glAttachShader(shaderProgram1_id, vertexShader_id); glAttachShader(shaderProgram1_id, fragmentShader_id); glLinkProgram(shaderProgram1_id); // -----------------------------end--------------------------- // 删除顶点着色器和片段着色器(不能将着色器程序也给delete掉) glDeleteShader(vertexShader_id); glDeleteShader(fragmentShader_id); // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);}void TestOpenGLWidget::resizeGL(int w, int h){}void TestOpenGLWidget::paintGL(){ glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 【画一个图形时需要说使用哪个着色器】 glUseProgram(shaderProgram1_id); // 使用时还需要再绑定一次 glBindVertexArray(VAO_id); // 在某个条件被触发时才会被绘制 switch (m_shape){ case Rect: glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // 否则,不进行绘制 default: break; }}

2)在.ui的窗口添加一些响应事件:

00 QT+OpenGL入门笔记(完结) (18)

3)添加槽函数:

00 QT+OpenGL入门笔记(完结) (19)

在mainwindow.h

#ifndef MAINWINDOW_H#define MAINWINDOW_H#include <QMainWindow>QT_BEGIN_NAMESPACEnamespace Ui { class MainWindow; }QT_END_NAMESPACEclass MainWindow : public QMainWindow{ Q_OBJECTpublic: MainWindow(QWidget *parent = nullptr); ~MainWindow();private slots: void on_actionactDrawRect_triggered(); void on_actClearRect_triggered(); void on_actWireFrame1_triggered();private: Ui::MainWindow *ui;};#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"#include "ui_mainwindow.h"// 窗口绘制相关MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow){ ui->setupUi(this); setCentralWidget(ui->openGLWidget);}MainWindow::~MainWindow(){ delete ui;}void MainWindow::on_actionactDrawRect_triggered(){ // 点击时调用方法 ui->openGLWidget->drawShape(TestOpenGLWidget::Rect);}void MainWindow::on_actClearRect_triggered(){ // 点击时调用方法 ui->openGLWidget->drawShape(TestOpenGLWidget::None);}void MainWindow::on_actWireFrame1_triggered(){ // 使用该方法需要将该控件设置为checkable ui->openGLWidget->setWirefame(ui->actWireFrame1->isChecked());}

实现结果录屏:

https://www.zhihu.com/video/1512522907246505984

七、构建着色器类

1. 使用QT自带的QOpenGLShaderProgram(基础版)

说明:该部分仍不将顶点着色器和片段着色器的数据剥离开!

实现步骤:

testopenglwidget.h

00 QT+OpenGL入门笔记(完结) (21)

testopenglwidget.cpp

00 QT+OpenGL入门笔记(完结) (22)

这样最终的结果和之前的是一样的。

但是这仍然可用性是不强的,因为顶点着色器和片段着色器仍然是以字符串的形式进行存储,这样是极其不方便的,因为我们希望不管是顶点着色器还是片段着色器如果出现错误,它可以有相关的提示。

所以我们首先需要将“顶点着色器”和“片段着色器”从代码中剥离开!下面用另外一种方式实现!

2. 将顶点着色器和片段着色器从源代码中剥离

1)构建相关“顶点着色器”和“片段着色器”文件

00 QT+OpenGL入门笔记(完结) (23)

2)在QT中添加资源文件

00 QT+OpenGL入门笔记(完结) (24)
00 QT+OpenGL入门笔记(完结) (25)

3)将“顶点着色器”和“片段着色器”导入进来

00 QT+OpenGL入门笔记(完结) (26)

4)保存后再次打开资源文件可以看见刚才导入的文件如下

00 QT+OpenGL入门笔记(完结) (27)

5)将顶点着色器和片段着色器代码存放在.vert和.frag文件中

// .vert#version 330 corelayout(location = 0) in vec3 aPos;void main(){ gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);}// .frag#version 330 coreout vec4 FragColor;void main(){ FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);}

6)将addShaderFromSourceCode改为addShaderFromSourceFile,并修改相关参数:

shaderProgramObject.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/shapes.vert");shaderProgramObject.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/shapes.frag");

3. GLSL相关补充

【注】1)着色器是使用一种叫GLSL的类C语言写成的;着色器的开头总是要声明版本;2)每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中;3)顶点着色器每个输入变量叫顶点属性(Vertex Attribute);4)虽然着色器(顶点着色器、片段着色器...)是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,希望每个着色器都有输入和输出,这样才能进行数据交流和传递;5)光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段,同时会插值所有片段着色器的输入变量。

1)输入和输出(in && out)

在两个着色器之间进行数据交流:若打算从一个着色器向另一个着色器发送数据,必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。

// 1. 顶点着色器#version 330 corelayout(location = 0) in vec3 aPos;// 从顶点着色器传出去的colorout vec4 vertexColor;void main(){ gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f); vertexColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);}// 2. 片段着色器#version 330 coreout vec4 FragColor;// 从顶点着色器传过来的颜色值in vec4 vertexColor;void main(){ FragColor = vertexColor;}

2)layout

注:layout的location默认值是0。

00 QT+OpenGL入门笔记(完结) (28)
00 QT+OpenGL入门笔记(完结) (29)

若在属性设定时将location设置为1:

// 1.在属性设定时将location设置为1glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);glEnableVertexAttribArray(1);// 2.在相应着色器中location也要设置为1#version 330 corelayout(location = 1) in vec3 aPos;// 从顶点着色器传出去的colorout vec4 vertexColor;void main(){ gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f); vertexColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);}

3)Uniform

CPU端和GPU端是一致的。

实验:不给像素传递单独一个颜色,而是让它随着时间改变颜色。

// 1..h#ifndef TESTOPENGLWIDGET_H#define TESTOPENGLWIDGET_H#include <QOpenGLWidget>#include <QOpenGLFunctions_3_3_Core>#include <QOpenGLShaderProgram>#include <QTimer>class TestOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core{ Q_OBJECTpublic: // 设置枚举量 enum Shape{None, Rect, Circle, Triangle}; // 定义一个公共的方法传递一个形状去决定画什么 void drawShape(Shape shape); void setWirefame(bool wireframe); explicit TestOpenGLWidget(QWidget *parent = nullptr); // 添加析构函数,进行一些资源的回收工作 ~TestOpenGLWidget(); // 顶点的数据:没有解析的数据是没有意义的 // 内存中的数据,关键是如何将内存中的数据给显卡 float vertices_data[18] = { // 所有的值是在[-1, 1]之间的 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[6] = { 0, 1, 3, // 第一个三角形 0, 2, 3 // 第二个三角形 }; // VAO和VBO变量(无符号整型) unsigned int VAO_id,VBO_id, EBO_id; // 着色器变量 unsigned int shaderProgram1_id;protected: virtual void initializeGL(); virtual void resizeGL(int w, int h); virtual void paintGL();public slots: void on_timeout();private: // 定义一个变量去决定画什么 Shape m_shape; // 创建QOpenGLShaderProgram对象 QOpenGLShaderProgram shaderProgramObject; QTimer timer;signals:};#endif // TESTOPENGLWIDGET_H// 2..cpp// ...TestOpenGLWidget::~TestOpenGLWidget(){ // 在VAO和VBO没有创建下可能会进行报错,故添加如下代码: if(!isValid()) return; // 调出当前的状态 makeCurrent(); // 第一个参数:要删除的对象的个数为1 glDeleteBuffers(1, &VBO_id); glDeleteVertexArrays(1, &VAO_id); // glDeleteProgram(shaderProgram1_id); doneCurrent();}// ...#include <QTime>void TestOpenGLWidget::on_timeout(){ // 此时什么也不执行 if(m_shape == None) return; // 此时才会进行响应的函数的调用 makeCurrent(); // 拿出秒的值 int timeValue = QTime::currentTime().second(); float greenValue = (sin(timeValue)/2.0f) + 0.5f; // 往shader里面传递的数据 shaderProgramObject.setUniformValue("ourColor", 0.0f, greenValue, 0.0f, 1.0f); doneCurrent(); update();}// 3..frag在片段着色器中进行数据的接收#version 330 coreout vec4 FragColor;uniform vec4 ourColor;// 从顶点着色器传过来的颜色值in vec4 vertexColor;void main(){ FragColor = ourColor;}

【参考资料】1)OpenGL,Qt实现:1入门篇(已更完)_哔哩哔哩_bilibili

2)简介 - LearnOpenGL CN (learnopengl-cn.github.io)

00 QT+OpenGL入门笔记(完结) (2024)
Top Articles
Latest Posts
Article information

Author: Geoffrey Lueilwitz

Last Updated:

Views: 5934

Rating: 5 / 5 (80 voted)

Reviews: 87% of readers found this page helpful

Author information

Name: Geoffrey Lueilwitz

Birthday: 1997-03-23

Address: 74183 Thomas Course, Port Micheal, OK 55446-1529

Phone: +13408645881558

Job: Global Representative

Hobby: Sailing, Vehicle restoration, Rowing, Ghost hunting, Scrapbooking, Rugby, Board sports

Introduction: My name is Geoffrey Lueilwitz, I am a zealous, encouraging, sparkling, enchanting, graceful, faithful, nice person who loves writing and wants to share my knowledge and understanding with you.