在QT中使用OpenGL
参考资料:
主页 - LearnOpenGL CN
https://blog.csdn.net/qq_40120946/category_12566573.html
由于OpenGL的大多数实现都是由显卡厂商编写的,当产生一个bug时通常可以通过升级显卡驱动来解决。
OpenGL中的名词解释
OpenGL 上下文(Context)
-
作用:
-
OpenGL上下文就像是一个画家的工具箱,它包含了所有当前可用的工具(如画笔、颜料、画布等),它存储了OpenGL的所有状态(如当前颜色、线条粗细、是否启用混合等)。
-
每个窗口/线程通常有一个独立的上下文,每个上下文是独立的,就像一个“沙盒”,不同上下文的资源默认不共享(除非显式设置共享)。
-
上下文必须绑定到某个“窗口”或“绘图表面”(如
HWND
、QOpenGLWidget
等),才能进行渲染。
-
为什么需要多个上下文?
多线程渲染(每个线程通常需要一个独立的上下文)。
不同窗口可能需要不同的OpenGL版本(如一个用OpenGL 2.1,另一个用OpenGL 4.6)。
-
例子:
-
如果你有两个画家(两个OpenGL上下文),他们各自有自己的画笔和颜料,互不影响。
-
当你切换上下文(比如从窗口A切换到窗口B),就像换了一个画家,他用的工具和之前的不一样。
-
OpenGL 状态机(State Machine)
OpenGL状态机决定了当前如何绘制图形,它由一系列可开关的设置组成,就像画家的当前工作模式。
-
作用:
-
控制OpenGL的绘制行为,比如:
-
当前颜色(
glColor3f(1.0, 0.0, 0.0)
→ 红色) -
是否启用深度测试(
glEnable(GL_DEPTH_TEST)
) -
当前绑定的纹理(
glBindTexture(GL_TEXTURE_2D, textureId)
)
-
-
这些状态会一直保持,直到你手动改变它们。
-
-
例子:
-
画家当前选择的是红色颜料(
glColor3f(1.0, 0.0, 0.0)
),那么接下来他画的所有东西都是红色,直到他换颜色。 -
画家决定是否使用尺子(
glEnable(GL_LINE_SMOOTH)
),如果启用,画线会更平滑。
-
OpenGL (ObjectID)
对象ID(如VAO
、VBO
、Texture
的ID)就像是工具箱里的每件工具的编号。
-
作用:
-
OpenGL管理的资源(如缓冲区、纹理、着色器)都有一个唯一的
GLuint
类型的ID。 -
你需要绑定(Bind)这些ID到OpenGL的某个目标(Target),才能使用它们。
-
-
例子:
-
画家有3支画笔(3个
VBO
),编号分别是1、2、3。 -
他必须拿起某支笔(
glBindBuffer(GL_ARRAY_BUFFER, vboId)
)才能用它画画。 -
如果他不绑定任何笔(
glBindBuffer(GL_ARRAY_BUFFER, 0)
),就没法画。
-
着色器 —— 着色器 - LearnOpenGL CN
顶点着色器
核心作用:处理每个顶点的坐标变换和属性计算。
- 坐标变换:将输入的顶点坐标(如模型空间坐标)转换为裁剪空间坐标(Clip Space),这是透视投影和视口变换的基础。
- 属性传递:处理顶点属性(如位置、法线、纹理坐标、颜色),并将处理后的数据传递给后续阶段(如几何着色器或片段着色器)。
- 应用场景:
- 实现模型的位移、旋转、缩放(通过矩阵变换)。
- 计算顶点光照效果的初始值(如法线变换)。
- 实现顶点动画(如骨骼动画、波浪效果)。
特点:
- 对每个顶点执行一次,并行计算效率高。
- 必须输出裁剪空间坐标(
gl_Position
)。
// 顶点着色器示例(简化)
#version 330 core
layout (location = 0) in vec3 aPosition; // 顶点位置
layout (location = 1) in vec3 aColor; // 顶点颜色
out vec3 vColor; // 传递给片段着色器的颜色uniform mat4 model; // 模型矩阵
uniform mat4 view; // 视图矩阵
uniform mat4 projection; // 投影矩阵void main() {
// 坐标变换:模型空间 → 世界空间 → 视图空间 → 裁剪空间
gl_Position = projection * view * model * vec4(aPosition, 1.0);
vColor = aColor; // 传递颜色属性
}
片段着色器
核心作用:计算每个片段(Fragment,可理解为潜在像素)的最终颜色。
- 颜色计算:基于插值后的顶点属性(如颜色、纹理坐标)和外部数据(如光照、材质、纹理),计算像素的 RGBA 值。
- 高级效果:实现光照模型(如 Phong、PBR)、阴影、纹理采样、透明度、后处理(如模糊、色调调整)。
- 应用场景:
- 渲染物体材质和纹理。
- 实现光照、反射、折射效果。
- 创建特效(如雾效、发光、粒子)。
特点:
- 对每个光栅化后的片段执行一次,计算量通常比顶点着色器大。
- 必须输出一个颜色值(
out vec4 FragColor
)。
#version 330 core
in vec3 vColor; // 从顶点着色器接收的颜色
out vec4 FragColor; // 输出的最终颜色void main() {
// 直接使用插值后的颜色
FragColor = vec4(vColor, 1.0);
}
VBO(Vertex Buffer Object)—— 顶点缓冲区对象
VBO 是OpenGL中用于存储顶点数据(如位置、颜色、纹理坐标等)的缓冲区对象。它直接在GPU内存中分配空间,避免CPU到GPU的重复数据传输,提高渲染效率。
作用
-
存储顶点属性数据(例如:
float vertices[] = {x1,y1,z1, x2,y2,z2, ...}
)。 -
通过
glBindBuffer
绑定后,OpenGL才知道从哪里读取数据。
类比
-
VBO = 颜料管
-
每个颜料管(VBO)存储一种原始数据(如红色颜料管、蓝色颜料管)。
-
颜料管本身不知道如何被使用,只是数据的容器。
-
VAO(Vertex Array Object)—— 顶点数组对象
VAO 是一个“配置容器”,用于记录:
-
当前绑定的VBO。
-
顶点属性的解析方式(如位置是3个float,颜色是4个float等)。
-
顶点属性的启用状态(通过
glEnableVertexAttribArray
)。
作用
-
避免重复配置:每次绘制时无需重新绑定VBO和设置属性格式。
-
提高性能:OpenGL可以直接读取预定义的顶点数据布局。
类比
-
VAO = 调色板(或画笔套装)
-
调色板(VAO)记录了:
-
哪些颜料管(VBO)被挤到调色板上。
-
每种颜料的用途(如红色用于轮廓,蓝色用于填充)。
-
-
画家(OpenGL)拿起调色板后,直接知道如何作画。
-
VBO 和 VAO 的联系与区别
联系
-
VAO 依赖 VBO
-
VAO本身不存储顶点数据,它只是记录“如何从VBO中读取数据”。
-
必须先绑定VBO,再配置VAO(就像先挤颜料到调色板,再定义用途)。
-
区别
特性 | VBO | VAO |
---|---|---|
存储内容 | 原始顶点数据(如位置、颜色) | 顶点属性的配置(如何读数据) |
是否直接参与绘制 | 是(数据源) | 否(只是数据格式的封装) |
绑定目标 | GL_ARRAY_BUFFER | 无目标(直接绑定) |
性能优化 | 减少CPU-GPU数据传输 | 减少绘制时的状态切换开销 |
一个VAO绑定多个VBO吗?——可以
VAO可以关联多个VBO(例如:一个VBO存位置,另一个存颜色)。
只需在绑定VAO后,依次绑定不同VBO并配置属性:glBindVertexArray(vaoId);
// 绑定位置VBO
glBindBuffer(GL_ARRAY_BUFFER, vboPos);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
// 绑定颜色VBO
glBindBuffer(GL_ARRAY_BUFFER, vboColor);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, NULL);
基础光照
环境光照(Ambient Lighting):
即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
漫反射光照(Diffuse Lighting):
模拟光源对物体的方向性影响(Directional Impact)。它是风氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
镜面光照(Specular Lighting):
模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
法向量
法向量是一个垂直于顶点表面的(单位)向量。由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面。我们能够使用一个小技巧,使用叉乘对立方体所有的顶点计算法向量,但是由于3D立方体不是一个复杂的形状,所以我们可以简单地把法线数据手工添加到顶点数据中。更新后的顶点数据数组可以在这里找到。试着去想象一下,这些法向量真的是垂直于立方体各个平面的表面的(一个立方体由6个平面组成)。
计算漫反射光照需要什么?
- 法向量:一个垂直于顶点表面的向量。
- 定向的光线:作为光源的位置与片段的位置之间向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。
OpenGL 图形渲染管线全流程解析
OpenGL 状态机与对象管理基础
- 状态机特性:OpenGL 通过上下文(Context)维护运行状态,通过设置选项、操作缓冲修改状态,以当前上下文执行渲染。
- 对象引用机制:对象通过整数 ID(objectId)绑定至上下文目标位置,存储选项配置,避免重复设置(如模型数据绑定)。
阶段 | 像素位置相关 | 像素颜色相关 |
---|---|---|
顶点着色器 | 转换 3D 坐标至裁剪空间,为投影做准备 | 处理顶点颜色等属性(可传递给片段着色器) |
几何着色器(可选) | 修改图元形状(如生成更多顶点) | 可输出顶点颜色属性 |
图元装配 | 定义几何形状的轮廓 | 无直接颜色处理 |
光栅化 | 确定像素的屏幕坐标(x, y) | 传递片段位置给片段着色器 |
片段着色器 | 无直接位置处理 | 计算像素的 RGBA 颜色值 |
Alpha 测试与混合 | 基于深度判断像素可见性 | 混合颜色值,确定最终显示的颜色 |
数据流:
一、像素位置的确定:3D 到 2D 坐标转换与光栅化阶段
1. 顶点着色器(Vertex Shader):初步坐标变换
- 作用:将输入的 3D 顶点坐标(如模型本地坐标)转换为裁剪空间坐标(Clip Space),并完成顶点属性(如位置、法线)的基础处理。
- 与像素位置的关系:此阶段将顶点从模型空间映射到裁剪空间,为后续投影到 2D 屏幕做准备,但尚未直接确定像素位置。
2. 图元装配(Primitive Assembly):几何形状定义
- 作用:将顶点按指定图元类型(如三角形、线段)装配成几何形状,确定渲染的基本单元(如一个三角形由 3 个顶点组成)。
- 与像素位置的关系:明确了 3D 空间中几何图形的轮廓,但仍处于抽象的几何阶段。
3. 光栅化(Rasterization):像素位置生成
- 作用:将图元(如三角形)映射到屏幕的 2D 像素网格,计算每个图元覆盖的像素坐标,并生成对应的片段(Fragment)。
- 关键细节:
- 此阶段通过插值计算图元在屏幕上的覆盖范围,确定每个像素的位置(x, y 坐标)。
- 裁切(Clipping)会丢弃视图外的像素,减少无效计算。
- 结论:光栅化阶段直接确定了像素的屏幕位置。
二、像素颜色的计算:片段处理与最终输出阶段
1. 片段着色器(Fragment Shader):颜色值计算
- 作用:接收光栅化生成的片段(包含位置、纹理坐标等信息),结合 3D 场景数据(如光照、阴影、材质属性、纹理采样),计算每个像素的最终颜色值(RGBA)。
- 关键细节:
- 可通过光照模型(如兰伯特、Phong)计算光照对颜色的影响。
- 支持纹理采样,将纹理贴图的颜色映射到像素。
- 结论:片段着色器是像素颜色计算的核心阶段。
2. Alpha 测试与混合(Alpha Test & Blending):颜色最终确定
- 作用:
- 深度测试:比较当前片段与已渲染像素的深度值,决定是否保留当前像素(近物覆盖远物)。
- Alpha 混合:根据透明度(Alpha 值)混合当前像素与背景像素的颜色,实现半透明效果(如玻璃、烟雾)。
- 与颜色的关系:此阶段不直接计算颜色,而是基于深度和 Alpha 值对片段着色器输出的颜色进行筛选或混合,最终确定屏幕上显示的像素颜色。
图形渲染管线核心流程拆解
-
3D 到 2D 坐标转换阶段
- 顶点着色器(Vertex Shader):输入单个顶点,将 3D 坐标转换为特定空间坐标(如裁剪空间),并处理顶点属性(如位置、颜色)。
- 几何着色器(Geometry Shader)(可选):输入顶点组(图元),通过生成新顶点扩展或修改图元形状(如从单个三角形生成两个三角形)。
-
图元装配与光栅化阶段
- 图元装配(Primitive Assembly):将顶点按指定图元类型(如三角形、点)装配为几何形状,确定渲染基本单元。
- 光栅化(Rasterization):将图元映射为屏幕像素,生成片段(Fragment);裁切(Clipping)丢弃视图外像素,提升效率。
-
片段处理与最终输出阶段
- 片段着色器(Fragment Shader):计算像素最终颜色,结合 3D 场景数据(光照、阴影、材质等)生成颜色值。
- Alpha 测试与混合(Alpha Test & Blending):通过深度 / 模板测试判断像素可见性,基于 Alpha 值实现透明度混合,确定最终渲染结果。
管线核心逻辑总结
“状态机管理对象状态 → 管线各阶段逐级处理顶点与图元 → 从 3D 坐标映射至 2D 像素并渲染”,这一流程构成了 OpenGL 将几何数据转化为屏幕图像的完整技术链路。
完整OpenGL绘图流程
OpenGL步骤 | Qt (QOpenGLWidget) | 画家类比 |
---|---|---|
1. 初始化OpenGL上下文 |
| 画家准备好画布、工具箱(上下文=工具箱+画布)。 |
2. 定义顶点数据 | 在initializeGL() 中定义数组(如float vertices[] ) | 画家决定画什么(如“画一个三角形,左下红、右下绿、顶部蓝”)。 |
3. 创建VBO | glGenBuffers() + glBufferData() 在initializeGL() 中完成 | 把颜料挤到调色盘上(VBO=颜料管,存储原始颜色和位置数据)。 |
4. 创建VAO | glGenVertexArrays() + 配置属性(glVertexAttribPointer )在initializeGL() 中完成 | 调色盘(VAO)记录: - 红色颜料用于轮廓 - 蓝色颜料用于填充。 |
5. 清空屏幕 | 在paintGL() 中调用 glClear() | 画家擦干净画布 |
6. 绑定VAO | 在paintGL() 中调用 glBindVertexArray(VAO) | 画家拿起调色盘(绑定VAO后,OpenGL知道如何读取VBO数据)。 |
7. 绘制调用 | 在paintGL() 中调用 glDrawArrays() | 画家用调色盘上的颜料开始作画 |
8. 解绑VAO | 可调用 glBindVertexArray(0) ,但Qt通常省略 | 画家放下调色盘 |
9.调整画布大小 | 重写 resizeGL(int w, int h) ,自动调用 | 画家根据画布大小调整画笔范围。 |
10.清理资源 | QOpenGLWidget 析构函数自动释放VAO/VBO | 画家收工,清理调色盘和颜料。 |
原生OpenGL对比Qt绘图流程关键区别总结
特性 | Qt (QOpenGLWidget ) | 原生OpenGL (GLFW) |
---|---|---|
上下文管理 | 自动创建和管理OpenGL上下文 | 需手动初始化(如GLFW/GLEW) |
窗口集成 | 直接嵌入Qt窗口体系 | 需手动处理窗口事件(如GLFW) |
函数调用 | 通过 QOpenGLFunctions 调用OpenGL API | 直接调用OpenGL API(如 glDrawArrays ) |
生命周期 | 自动调用 initializeGL() 、paintGL() | 需手动编写渲染循环 |
适用场景 | Qt应用程序中嵌入3D绘图 | 跨平台游戏/独立图形应用 |
Qt 的 QOpenGLWidget
绘图完整代码——制一个三色三角形
1.1 创建自定义 QOpenGLWidget
子类
// MyGLWidget.h #include <QOpenGLWidget> #include <QOpenGLFunctions>class MyGLWidget : public QOpenGLWidget, protected QOpenGLFunctions { public:MyGLWidget(QWidget* parent = nullptr) : QOpenGLWidget(parent) {}protected:void initializeGL() override; // 初始化OpenGLvoid paintGL() override; // 绘制void resizeGL(int w, int h) override; // 窗口大小变化时调整private:GLuint VAO, VBO; // OpenGL对象 };
1.2 实现 initializeGL
(初始化阶段)
// MyGLWidget.cpp void MyGLWidget::initializeGL() {initializeOpenGLFunctions(); // 初始化Qt的OpenGL函数绑定// 定义顶点数据(位置 + 颜色)float vertices[] = {// 位置 // 颜色-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下(红)0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 右下(绿)0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部(蓝)};// 创建并绑定VBOglGenBuffers(1, &VBO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);// 创建并绑定VAOglGenVertexArrays(1, &VAO);glBindVertexArray(VAO);// 配置位置属性(前3个float)glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// 配置颜色属性(后3个float,偏移量=3*sizeof(float))glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(1);// 解绑(非必须,但安全)glBindVertexArray(0);glBindBuffer(GL_ARRAY_BUFFER, 0); }
1.3 实现 paintGL
(渲染阶段)
void MyGLWidget::paintGL() {glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置清屏颜色(深绿色)glClear(GL_COLOR_BUFFER_BIT); // 清空颜色缓冲glBindVertexArray(VAO); // 绑定VAO(自动关联VBO和属性)glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形 }
1.4 实现 resizeGL
(窗口调整)
void MyGLWidget::resizeGL(int w, int h) {glViewport(0, 0, w, h); // 调整视口大小 }
1.5 在Qt窗口中使用 MyGLWidget
// main.cpp #include <QApplication> #include "MyGLWidget.h"int main(int argc, char** argv) {QApplication app(argc, argv);MyGLWidget widget;widget.show();return app.exec(); }