在QT中使用OpenGL

参考资料:

主页 - LearnOpenGL CN

由于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 的联系与区别

联系

  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();
}
相关推荐
yi0113 小时前
LeetCode 219:存在重复元素 II——哈希表记录“最近一次出现的位置”
数据结构·人工智能·笔记·python·算法·leetcode·哈希表
志尊宝4 小时前
Vue3 零基础每日笔记(073):keep-alive 页面缓存实战——列表页返回不丢状态
vue.js·笔记·缓存·#vue #前端 #前端开发·#javascript·#vue.js
李日华大战鸡红5 小时前
FOC状态空间方程模型推导(学习记录)
python·学习·线性代数
weixin_448119946 小时前
Datawhale Easy Data × AI:笔记1 已重做,看新的笔记
笔记
要开心吖ZSH7 小时前
MySQL 慢 SQL 排查操作手册-个人笔记版
java·笔记·sql·mysql·慢查询
kkkkkkkkkk_Z7 小时前
新手自学嵌入式 | 学习日记:异常与中断基础、i.MX6ULL裸机开发环境搭建、GPIO模块与点灯原理
学习
咖啡忍者8 小时前
【SAP】程制造生产计划PP-PI(Production Planning for Process Industries)
笔记
m4Rk_8 小时前
【论文阅读】Agent 记忆机制(94):MemGen——在推理过程中动态生成并织入潜在记忆
论文阅读·人工智能·学习·开源·github
晓德8 小时前
0、LLM大模型安全学习系列(启)
学习·安全
Titan20249 小时前
MySQL索引学习笔记
笔记·学习·mysql