一、图形渲染管线
在 OpenGL 中,任何事物都在 3D 空间中,而屏幕和窗口却是 2D 的空间,这导致 OpenGL 的大部分工作都是关于把 3D 坐标转变为适应你屏幕的 2D 坐标。3D 坐标转为 2D 坐标的处理过程是由 OpenGL 的图形渲染管线管理的。(Graphics Pipeline,大多译为管线,实际上指的是 一堆原始图形数据途经一个输送管道,期间经过各种变化处理,最终出现在屏幕的过程 )
为什么叫 “管线” 呢? 因为图形的渲染过程就像是:管的一端输入,管的另一端输出。输入是一些 “顶点坐标、画图规则”,而输出就是 “实际图画”。而中间那条 “管子” ,就像是一个通用的 “容器”,什么样的 “输入” 都能接受。就像生活中的水管,即能传输水,也能传输油,还能传输天然气
它们具有并行执行的特性,也就是说,就像自来水厂向你家输水一样,可以多条水管一起输过来。而当今大多数大脑的显卡上,都有成千上万的小处理核心(这个就像是那成千上万的“水管”),它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)
图形渲染管线可以被划分为两个主要部分:第一部分把 3D 坐标转换为 2D 坐标,第二部分把 2D 坐标转变为实际有颜色的像素。
图形渲染管线的阶段流程图:
顶点数据Vertex Data[] -> 顶点着色器VertexShader -> 图元装配Shape Assembly -> 几何着色器Geometry Shader -> 光栅化 -> 片段着色器Fragment Shader -> 测试与混合
① 图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标。
② 图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入,并将所有的顶点装配成指定图元的形状。
③ 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它会通过新产生的顶点构造出新的图元来生成其他形状(但变形不变样)。
④ 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。
⑤ 片段着色器(也称为片元着色器)的主要目的是计算每一个像素的最终颜色。
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。
二、Qt QOpenGLWidget实战-绘制一个三角形
✨2.1 引入库
1、 .pro工程文件添加
QT += core gui opengl widgets
QT += openglwidgets
2、 新建一个类TriangleWidget public继承QOpenGLWidget protected继承QOpenGLFunctions_3_3_Core
QOpenGLFunctions_3_3_Core是 Qt 在 QtGui
模块中提供的一个 “函数集合类”,它把 OpenGL 3.3 Core Profile 的全部(官方规范中规定的)函数指针一次性打包好,供我们在 Qt 程序中直接调用。
✨2.2 编写顶点数据
我们要渲染一个三角形,一共要指定三个顶点,每个顶点都有一个 3D 位置:
OpenGL 是一个 3D 图形库,所以我们在 OpenGL 中指定的所有坐标都是 3D 坐标(x、y和z)。仅当 3D 坐标在3个轴 (x、y和z) 上都在 -1.0 到 1.0 的范围内时才处理它。这个是 标准化设备坐标 (Normalized Device Coordinates, NDC)。
static GLfloat vertices_1[] = {
0.0f, 0.5f, 0.0f, // 上顶点
-0.5f, -0.5f, 0.0f, // 左顶点
0.5f, -0.5f, 0.0f // 右顶点
};
解析:这里的GLfloat 本质是 C/C++ 里的 float
,只是 OpenGL 为了跨平台统一名字而起的别名。OpenGL 的 API(例如 glBufferData
)参数类型是 GLfloat*
,用官方 typedef 可以避免不同平台 float
与 double
的字节对齐/符号扩展问题。在这里 z = 0.0f
表示“没有深度分量”,从而把三维空间“压扁”到二维平面(即 xy
平面)。
OpenGL 默认按 (x, y, z) 三元组来理解顶点。这个数组长度必须是 3 的倍数:
[x0, y0, z0, // 顶点 0
x1, y1, z1, // 顶点 1
x2, y2, z2] // 顶点 2
与 GLSL 对应:
顶点着色器里声明
layout(location = 0) in vec3 position;
告诉管线:每个顶点着色器实例将一次性读取 3 个 GLfloat
,组成 vec3
。
逐点解读:
顶点索引 | (x, y, z) | 屏幕实际位置(像素无关) |
---|---|---|
0 | ( 0, 0.5, 0) | 中心正上方(y 轴 0.5) |
1 | (-0.5, -0.5, 0) | 左下角 |
2 | ( 0.5, -0.5, 0) | 右下角 |
画出的形状:
三个点顺次连接得到 一个以窗口中心为顶点的等腰三角形,底边水平,位于窗口下半部。
z = 0 表示三角形在 NDC(标准化设备坐标) 的“中平面”,没有深度偏移。
定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。
✨2.3 编写顶点着色器
// 编写顶点着色器
const char *vertexCode_1 = "#version 330 core\n" // 3.30版本(版本申明)
"layout(location = 0) in vec3 position_1;\n" // 三个浮点数 vector 向量表示位置。position是变量,并储存了这三个向量
"void main()\n"
"{\n"
" gl_Position = vec4(position_1, 1.0);\n" // 核心函数(位置信息赋值)
"}\n";
编写顶点着色器需要用到 “着色器语言GLSL(OpenGL Shading Language) ”,后面编译这个着色器,这样我们就可以在程序中使用它了。
解析上面的代码:
设置顶点着色器的输出的时候,我们必须把位置数据赋值给预定义的 gl_Position变量 ,这个是一定要设置的,对应代码里的gl_Position = vec4(position_1, 1.0);
gl_Position变量 是 vec4 类型的,而我们的输入是一个 3 分量的向量,我们必须把它转换为 4 分量的。我们可以把 vec3 的数据作为 vec4 构造器的参数,同时把 “透视值分量” 设置为1.0f。vec4(position, 1.0)
把三维点变成齐次坐标,w = 1.0 保证透视除法后坐标不变。
有了顶点着色器这个 “模子”,我们在主函数里面就可以创建 顶点着色器对象 了
在创建着色器对象时,注意要用 ID 来引用。所以我们让这个顶点着色器为 unsigned int 数据类型(注:在OpenGL里面,“GLuint ” 实际上就是 “unsigned int”),然后用 glCreateShader 创建这个着色器。
GLuint vertexShader_1 = glCreateShader(GL_VERTEX_SHADER);
这里指定顶点着色器的标识符GL_VERTEX_SHADER
下一步我们把这个着色器源码附加到着色器对象上,然后编译(Compile)它
glShaderSource(vertexShader_1, 1, &vertexCode_1, nullptr); // 将顶点着色器的内容传进来
glCompileShader(vertexShader_1); // 编译着色器
解析glShaderSource函数
● glShaderSource 函数:
▼ 第一个参数:要编译的着色器对象。
▼ 第二个参数:指定传递的源码字符串数量,这里只有一个。
▼ 第三个参数:顶点着色器真正的源码。我们传入的是地址。
▼ 第四个参数:读数据时的起始位。
检测 glCompileShader 编译是否成功了:
GLint flag; // 用于判断编译是否成功的标识符
glGetShaderiv(vertexShader_1, GL_COMPILE_STATUS, &flag); // 获取编译状态
if (!flag) {
GLchar infoLog[512];
glGetShaderInfoLog(vertexShader_1, 512, nullptr, infoLog); // 如果出错,用 glGetShaderInfoLog函数 来获取错误消息
qDebug() << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog; // 打印错误信息
}
✨2.4 编写片元着色器(片段着色器)
片元着色器的作用是处理由光栅化阶段生成的每个片元,最终计算出每个像素的最终颜色。
和编写顶点着色器的过程相似。片段着色器只需要一个输出变量,这个变量是一个 4 分量向量,它表示的是最终的输出颜色,这里我们命名为 FragColor_1。然后,声明输出变量可以使用 out 关键字。我们设置 Alpha 值为1.0(1.0代表完全不透明)的墨绿色的 vec4 赋值给颜色输出。
// 编写片元着色器
const char *fragmentCode_1 = "#version 330 core\n" // 版本信息3.3
"out vec4 FragColor_1;\n" // 输出是四个浮点数构成的一个向量
"void main()\n"
"{\n"
" FragColor_1 = vec4(0.5f, 0.75f, 0.25f, 1.0f);\n" // 核心函数(颜色信息赋值)
"}\n";
后面的流程和顶点着色器一样,创建片元着色器,将内容绑定,然后编译,以及做判断编译状态的处理
// 创建并编译片元着色器
GLuint fragmentShader_1 = glCreateShader(GL_FRAGMENT_SHADER); // 创建片元着色器对象 GL_FRAGMENT_SHADER
glShaderSource(fragmentShader_1, 1, &fragmentCode_1, nullptr); // 将片元着色器的内容传进来
glCompileShader(fragmentShader_1); // 编译片元着色器
glGetShaderiv(fragmentShader_1, GL_COMPILE_STATUS, &flag); // 获取编译状态
if (!flag) {
GLchar infoLog[512];
glGetShaderInfoLog(fragmentShader_1, 512, nullptr, infoLog); // 如果出错,用 glGetShaderInfoLog函数 来获取错误消息
qDebug() << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog;
}
✨2.5 编写着色器程序program
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的东西。 它的名字里最突出的就是 “程序” 两个字,它是一个流程,它是把先前创建的 “着色器模子” 拼接在一起的 “超级模子”,可以进行 “一条龙服务”。
如果要使用刚才编译的着色器,那么我们必须把它们 连接(Link) 为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
当连接着色器至一个程序的时候,它会把每个着色器的输出 连接到 下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
关于这里的每个着色器的输出 连接到 下个着色器的输入,可以得知顶点着色器的输出会成为下一个阶段(图元装配→光栅化)产生的片段着色器的输入:
顶点着色器输出变量 →
图元装配(Primitive Assembly) →
光栅化(Rasterization) →
片段着色器的输入变量(通过插值得到)
这期间并不是“直接”相连,中间还有几个 GPU 内部阶段做“接力”。可以看成一条“数据传递链”
// 创建着色器程序
shaderProgram = glCreateProgram(); // glCreateProgram 函数会创建一个着色器程序,并返回新创建程序对象的 ID 引用。
glAttachShader(shaderProgram, vertexShader_1); // 用 glAttachShader把之前编译的 顶点着色器 附加到程序对象上
glAttachShader(shaderProgram, fragmentShader_1); // 用 glAttachShader把之前编译的 片元着色器 附加到程序对象上
glLinkProgram(shaderProgram); // 最后用 glLinkProgram 链接
shaderProgram是一个GLuint类型的成员变量,保存创建程序对象后生成的 ID
就像编译着色器的一样,我们也可以检测链接着色器程序是否成功,并获取相应的日志:
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &flag);
if (!flag) {
GLchar infoLog[512];
glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
qDebug() << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog;
}
在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:(这可以释放资源,节省空间)
glDeleteShader(vertexShader_1);
glDeleteShader(fragmentShader_1);
在我们需要用编写好的 着色器程序 时,只需要调用 glUseProgram 函数,用刚创建的着色器程序对象作为它的参数,来激活这个程序对象即可,glUseProgram 可以放到paintGL函数里去调用
glUseProgram(shaderProgram);
✨2.6设置链接顶点属性
顶点着色器允许我们输入任何形式的顶点属性。这具有很强的灵活性,但它也意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定 OpenGL 该如何 “解释” 输入给它的顶点数据
我们可以使用 glVertexAttribPointer 函数告诉 OpenGL 该如何解析顶点数据(应用到逐个顶点属性上):
// 顶点属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void*)0);
glEnableVertexAttribArray(0); // 启用顶点属性(注:顶点属性默认是禁用的)
1、第一个参数:顶点属性。这个是 我们在最初,顶点着色器中使用 “layout(location = 0)” 定义 position 顶点属性的位置值(Location)。
2、 第二个参数:顶点属性的大小。顶点属性是一个 vec3 ,它由 3 个值组成,所以大小是3。
3、第三个参数:数据的类型。这里是 GL_FLOAT (注:GLSL中 vec* 都是由浮点数值组成的)。
4、第四个参数:是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到 0 到 1 之间(注:对于有符号型signed数据是-1)。我们把它设置为GL_FALSE。
5、第五个参数:设置步长(Stride)。它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在 3 个 float 之后,我们把步长设置为 “3 * sizeof(float)”。
6、第六个参数:起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。因为最后一个参数的类型是 “void*”,所以我们需要进行这个奇怪的强制类型转换。
✨2.7 设置顶点缓冲对象(VBO)
我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这含有这三个顶点的内存
使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。
就像 OpenGL 中的其它对象一样,这个缓冲对象也有一个独一无二的 ID ,所以我们可以使用 glGenBuffers 函数和一个缓冲 ID 生成一个 VBO 对象:
GLuint VBO{}; // 初始化VBO对象
glGenBuffers(1, &VBO); // 生成 1 个 VBO的ID,存到 VBO对象
OpenGL 有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。我们可以使用 glBindBuffer 函数把新创建的顶点缓冲对象绑定到 GL_ARRAY_BUFFER 目标上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的顶点缓冲对象(VBO)。然后我们可以调用 glBufferData 函数,它会把之前定义的顶点数据复制到缓冲的内存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW); // GL_STATIC_DRAW:静态的画图(频繁地读)
glBufferData 是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。
▼ 第一个参数:目标缓冲的类型。将当前的顶点缓冲对象绑定到GL_ARRAY_BUFFER目标上。
▼ 第二个参数:传输数据的大小。用一个简单的 sizeof 计算出顶点数据大小就行。
▼ 第三个参数:实际数据。
▼ 第四个参数:显卡管理数据的形式。它有三种形式:
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。
因为三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。
✨2.8 设置顶点数组对象(VAO) (顶点阵列对象)
这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的 VAO 就行了。这使得在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的 VAO 就行了。刚刚设置的所有状态都将存储在 VAO 中。(就好像,我们已经把菜谱写好了,想炒什么菜,直接去找到菜谱,拿给小厨子去炒,就不用我们站在旁边一句话一个指点地教他炒了)
可以将VAO和VBO进行一个比较
对象 | 职责 | 生成函数 | 绑定函数 | 删除函数 |
---|---|---|---|---|
VBO | 真正存顶点数据的显存块 | glGenBuffers | glBindBuffer | glDeleteBuffers |
VAO | 记录“如何从 VBO 取数据”的格式表 | glGenVertexArrays | glBindVertexArray | glDeleteVertexArrays |
GLuint VAO{}; // 顶点阵列对象
glGenVertexArrays(1, &VAO); // VAO 对应的一种 关联信息(顶点着色器和位置相映射) 绑定 VAO
要想使用 VAO,要做的只是使用 glBindVertexArray 绑定 VAO 。从绑定之后起,我们应该绑定 与相应配置对应的 VBO 和属性指针,之后解绑 VAO 供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把 VAO 绑定到希望使用的设定上就行了:
glBindVertexArray(VAO); // 绑定 VAO
解绑:
// 解绑
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
✨2.9 绘制三角形
OpenGL给我们提供了 glDrawArrays 函数,它使用当前已激活的着色器,包括之前定义的顶点属性配置,和 VBO 的顶点数据(通过 VAO 间接绑定)来绘制图元。
glDrawArrays(GL_TRIANGLES, 0, 3); // 画三角形 从第0个顶点开始 一共画3次
▼ 第一个参数:绘制OpenGL图元的类型。我们希望绘制的是一个三角形,这里传递GL_TRIANGLES给它。
▼ 第二个参数:顶点数组的起始索引。我们这里填0。
▼ 第三个参数:绘制的顶点个数。这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点)。
✨2.10 完整代码
TriangleWidget.h
#ifndef TRIANGLEWIDGET_H
#define TRIANGLEWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
class TriangleWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core
{
Q_OBJECT
public:
explicit TriangleWidget(QWidget *parent = nullptr);
protected:
void initializeGL() override; // 对应 GLFW 的初始化
void paintGL() override; // 对应渲染循环
void resizeGL(int w, int h) override;
private:
GLuint VAO{}, VBO{}, shaderProgram;
};
#endif // TRIANGLEWIDGET_H
TriangleWidget.cpp
#include "TriangleWidget.h"
#include <QDebug>
// 准备顶点数据
static GLfloat vertices_1[] = {
0.0f, 0.5f, 0.0f, // 上顶点
-0.5f, -0.5f, 0.0f, // 左顶点
0.5f, -0.5f, 0.0f // 右顶点
};
// 编写顶点着色器
const char *vertexCode_1 = "#version 330 core\n" // 3.30版本(版本申明)
"layout(location = 0) in vec3 position_1;\n" // 三个浮点数 vector 向量表示位置。position是变量,并储存了这三个向量
"void main()\n"
"{\n"
" gl_Position = vec4(position_1, 1.0);\n" // 核心函数(位置信息赋值)
"}\n";
// 编写片元着色器
const char *fragmentCode_1 = "#version 330 core\n" // 版本信息3.3
"out vec4 FragColor_1;\n" // 输出是四个浮点数构成的一个向量
"void main()\n"
"{\n"
" FragColor_1 = vec4(0.5f, 0.75f, 0.25f, 1.0f);\n" // 核心函数(颜色信息赋值)
"}\n";
TriangleWidget::TriangleWidget(QWidget *parent)
: QOpenGLWidget(parent)
{
// 强制使用 Core Profile,等价于 glfwWindowHint
QSurfaceFormat fmt;
fmt.setVersion(3, 3);
fmt.setProfile(QSurfaceFormat::CoreProfile);
setFormat(fmt);
}
void TriangleWidget::initializeGL()
{
initializeOpenGLFunctions(); // 等价于 glewInit()
// ------------------------------------------------------------
// 创建并编译顶点着色器
GLuint vertexShader_1 = glCreateShader(GL_VERTEX_SHADER); // 创建顶点着色器 GL_VERTEX_SHADER
glShaderSource(vertexShader_1, 1, &vertexCode_1, nullptr); // 将顶点着色器的内容传进来
glCompileShader(vertexShader_1); // 编译着色器
GLint flag; // 用于判断编译是否成功的标识符
glGetShaderiv(vertexShader_1, GL_COMPILE_STATUS, &flag); // 获取编译状态
if (!flag) {
GLchar infoLog[512];
glGetShaderInfoLog(vertexShader_1, 512, nullptr, infoLog); // 如果出错,用 glGetShaderInfoLog函数 来获取错误消息
qDebug() << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog; // 打印错误信息
}
// 创建并编译片元着色器
GLuint fragmentShader_1 = glCreateShader(GL_FRAGMENT_SHADER); // 创建片元着色器对象 GL_FRAGMENT_SHADER
glShaderSource(fragmentShader_1, 1, &fragmentCode_1, nullptr); // 将片元着色器的内容传进来
glCompileShader(fragmentShader_1); // 编译片元着色器
glGetShaderiv(fragmentShader_1, GL_COMPILE_STATUS, &flag); // 获取编译状态
if (!flag) {
GLchar infoLog[512];
glGetShaderInfoLog(fragmentShader_1, 512, nullptr, infoLog); // 如果出错,用 glGetShaderInfoLog函数 来获取错误消息
qDebug() << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog;
}
// 创建着色器程序
shaderProgram = glCreateProgram(); // glCreateProgram 函数会创建一个着色器程序,并返回新创建程序对象的 ID 引用。
glAttachShader(shaderProgram, vertexShader_1); // 用 glLinkProgram 把之前编译的 顶点着色器 附加到程序对象上
glAttachShader(shaderProgram, fragmentShader_1); // 用 glLinkProgram 把之前编译的 片元着色器 附加到程序对象上
glLinkProgram(shaderProgram); // 最后用 glLinkProgram 链接
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &flag);
if (!flag) {
GLchar infoLog[512];
glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
qDebug() << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog;
}
// 删除着色器对象 释放资源
glDeleteShader(vertexShader_1);
glDeleteShader(fragmentShader_1);
// VBO VAO
glGenVertexArrays(1, &VAO); // 生成 1 个 VAO的ID,存到 VAO对象
glGenBuffers(1, &VBO); // 生成 1 个 VBO的ID,存到 VBO对象
glBindVertexArray(VAO); // 绑定 VAO
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 把新创建的顶点缓冲对象绑定到 GL_ARRAY_BUFFER 目标上
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);
// 顶点属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void*)0);
glEnableVertexAttribArray(0); // 启用顶点属性(注:顶点属性默认是禁用的)
// 解绑
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
void TriangleWidget::paintGL()
{
// 绘制三角形
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
void TriangleWidget::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);
}
最后在窗口创建一个TriangleWidget对象设置为centralWidget就能显示了
TriangleWidget *w = new TriangleWidget;
setCentralWidget(w);
三、总结
整个流程:
数据→着色器→编译→程序→VAO/VBO→绑→配→解→画
(下面是时序图,如果太暗的话可以点击页面右下角的灯泡换为日间模式)
sequenceDiagram participant CPU as CPU / Qt 代码 participant GL as OpenGL 驱动 / GPU %% 1️⃣ 准备顶点数据 CPU->>CPU: 1️⃣ 定义顶点数组 vertices_1[] %% 2️⃣ 编写着色器源码 CPU->>CPU: 2️⃣ 编写 vertexCode_1 & fragmentCode_1 %% 3️⃣ 创建 & 编译着色器 CPU->>GL: 3️⃣ glCreateShader(GL_VERTEX_SHADER) CPU->>GL: glShaderSource(...) CPU->>GL: glCompileShader(...) GL-->>CPU: 返回编译结果 / 日志 CPU->>GL: 3️⃣ glCreateShader(GL_FRAGMENT_SHADER) CPU->>GL: glShaderSource(...) CPU->>GL: glCompileShader(...) GL-->>CPU: 返回编译结果 / 日志 %% 4️⃣ 链接成着色器程序 CPU->>GL: 4️⃣ glCreateProgram() CPU->>GL: glAttachShader(vertex + fragment) CPU->>GL: glLinkProgram() GL-->>CPU: 返回链接结果 / 日志 CPU->>GL: glDeleteShader(x2) 释放中间对象 %% 5️⃣ 生成 VAO / VBO 名字 CPU->>GL: 5️⃣ glGenVertexArrays(1, &VAO) CPU->>GL: glGenBuffers(1, &VBO) %% 6️⃣ 绑定 VAO & VBO CPU->>GL: 6️⃣ glBindVertexArray(VAO) CPU->>GL: glBindBuffer(GL_ARRAY_BUFFER, VBO) %% 7️⃣ 上传数据 & 设置属性指针 CPU->>GL: 7️⃣ glBufferData(GL_ARRAY_BUFFER, ...) CPU->>GL: glVertexAttribPointer(0, ...) CPU->>GL: glEnableVertexAttribArray(0) %% 8️⃣ 解绑 CPU->>GL: 8️⃣ glBindBuffer(GL_ARRAY_BUFFER, 0) CPU->>GL: glBindVertexArray(0) %% 9️⃣ 每帧绘制 loop 渲染循环 CPU->>GL: 9️⃣ glClear(...) CPU->>GL: glUseProgram(shaderProgram) CPU->>GL: glBindVertexArray(VAO) CPU->>GL: glDrawArrays(GL_TRIANGLES, 0, 3) GL-->>CPU: 三角形成像到帧缓冲 end