在QT中使用OpenGL

参考资料:

主页 - LearnOpenGL CN

由于OpenGL的大多数实现都是由显卡厂商编写的,当产生一个bug时通常可以通过升级显卡驱动来解决。

OpenGL中的名词解释

OpenGL 上下文(Context)

  • 作用

    • OpenGL上下文 就像是一个画家的工具箱 ,它包含了所有当前可用的工具(如画笔、颜料、画布等),它存储了OpenGL的所有状态(如当前颜色、线条粗细、是否启用混合等)。

    • 每个窗口/线程通常有一个独立的上下文,每个上下文是独立的,就像一个"沙盒",不同上下文的资源默认不共享(除非显式设置共享)。

    • 上下文必须绑定到某个"窗口"或"绘图表面" (如HWNDQOpenGLWidget等),才能进行渲染。

为什么需要多个上下文?

多线程渲染(每个线程通常需要一个独立的上下文)。

不同窗口可能需要不同的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 (如VAOVBOTexture的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 的联系与区别

联系

  1. 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 值对片段着色器输出的颜色进行筛选或混合,最终确定屏幕上显示的像素颜色。
图形渲染管线核心流程拆解
  1. 3D 到 2D 坐标转换阶段

    • 顶点着色器(Vertex Shader):输入单个顶点,将 3D 坐标转换为特定空间坐标(如裁剪空间),并处理顶点属性(如位置、颜色)。
    • 几何着色器(Geometry Shader)(可选):输入顶点组(图元),通过生成新顶点扩展或修改图元形状(如从单个三角形生成两个三角形)。
  2. 图元装配与光栅化阶段

    • 图元装配(Primitive Assembly):将顶点按指定图元类型(如三角形、点)装配为几何形状,确定渲染基本单元。
    • 光栅化(Rasterization):将图元映射为屏幕像素,生成片段(Fragment);裁切(Clipping)丢弃视图外像素,提升效率。
  3. 片段处理与最终输出阶段

    • 片段着色器(Fragment Shader):计算像素最终颜色,结合 3D 场景数据(光照、阴影、材质等)生成颜色值。
    • Alpha 测试与混合(Alpha Test & Blending):通过深度 / 模板测试判断像素可见性,基于 Alpha 值实现透明度混合,确定最终渲染结果。
管线核心逻辑总结

"状态机管理对象状态 → 管线各阶段逐级处理顶点与图元 → 从 3D 坐标映射至 2D 像素并渲染",这一流程构成了 OpenGL 将几何数据转化为屏幕图像的完整技术链路。

完整OpenGL绘图流程

OpenGL步骤 Qt (QOpenGLWidget) 画家类比
1. 初始化OpenGL上下文 QOpenGLWidget 构造函数自动创建OpenGL上下文 initializeOpenGLFunctions()加载OpenGL函数,初始化Qt的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;  // 初始化OpenGL
    void 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  // 顶部(蓝)
    };

    // 创建并绑定VBO
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // 创建并绑定VAO
    glGenVertexArrays(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();
}
相关推荐
xiaoxiaoxiaolll2 分钟前
可编程光子处理器新范式:《APL Photonics》封面级成果展示多功能集成突破
学习
电院工程师28 分钟前
ChipWhisperer教程(三)
笔记·python·嵌入式硬件·安全·fpga开发·安全架构
LuH112436 分钟前
【论文阅读笔记】高光反射实时渲染新突破:3D Gaussian Splatting with Deferred Reflection 技术解析
论文阅读·笔记·3d
程序员Xu1 小时前
大厂机试题解法笔记大纲+按知识点分类+算法编码训练
笔记·算法
张哈大1 小时前
【 java 虚拟机知识 第二篇 】
java·开发语言·jvm·笔记
凉、介1 小时前
Linux 下 pcie 初始化设备枚举流程代码分析
linux·运维·服务器·学习·嵌入式·c·pcie
虾球xz2 小时前
游戏引擎学习第315天:取消排序键的反向顺序
开发语言·c++·学习·游戏引擎
玻璃瓶和纸飞机3 小时前
Java常用类库大全(学习笔记)持续更新中
java·笔记·学习
菜一头包3 小时前
QT5中的QGraphics图形视图框架学习笔记(Item、Scene和View)
笔记·qt·学习
蓝婷儿3 小时前
Python 爬虫入门 Day 1 - 网络请求与网页结构基础
开发语言·python·学习