欢迎来到 Qt 与现代 OpenGL (Core Profile) 的世界!本教程将带领您从零开始搭建一个 Qt 项目,并逐步实现从简单的彩色三角形到更复杂的彩色四边形的绘制。
我们将重点关注 VAO 、VBO 、EBO 以及 着色器 的核心概念。
第一步:项目工程建立与配置
我们使用 Qt Creator 创建一个基于 Qt Widgets Application 的项目。
1. CMake 配置 (CMakeLists.txt)
要使用 OpenGL 绘制,我们需要在 CMakeLists.txt 中添加必要的模块。请确保您的配置文件中包含 OpenGLWidgets:
# 设置 CMake 的最低要求版本
cmake_minimum_required(VERSION 3.16)
# 定义项目名称和版本
project(opengl3D VERSION 0.1 LANGUAGES CXX)
# 设置 C++ 标准为 C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 启用 Qt 自动工具(MOC, UIC, RCC)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
# 查找所需的 Qt 6 模块:Widgets, OpenGL, OpenGLWidgets
find_package(Qt6 REQUIRED COMPONENTS Widgets OpenGL OpenGLWidgets)
# 定义可执行文件及其源文件
qt_add_executable(opengl3D
main.cpp
mainwindow.cpp
mainwindow.h
mainwindow.ui
openglwidget.cpp
openglwidget.h
)
# --- 关键修正:添加当前源目录到头文件搜索路径 (用于解决 ui_mainwindow.h 错误) ---
target_include_directories(opengl3D PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# 链接所需的 Qt 库
target_link_libraries(opengl3D PRIVATE
Qt::Widgets
Qt::OpenGL
Qt::OpenGLWidgets
)
2. 创建 OpenGL 绘制组件
新建一个 C++ 类,命名为 OpenGLWidget,它需要继承自 QOpenGLWidget 并实现 QOpenGLFunctions_3_3_Core 来使用现代 OpenGL 的核心功能。
openglwidget.h
#ifndef OPENGLWIDGET_H
#define OPENGLWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLShaderProgram>
class OpenGLWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core
{
Q_OBJECT
public:
explicit OpenGLWidget(QWidget *parent = nullptr);
~OpenGLWidget();
protected:
void initializeGL() override;
void resizeGL(int w, int h) override;
void paintGL() override;
private:
QOpenGLShaderProgram *program = nullptr;
QOpenGLBuffer vbo;
QOpenGLVertexArrayObject vao;
unsigned int ebo = 0; // EBO 仅用于四边形示例
};
#endif // OPENGLWIDGET_H
【重要提示:更新工程文件】
在 Qt Creator 中手动创建 openglwidget.cpp 和 openglwidget.h 文件,并确保它们已在 CMakeLists.txt 中被引用后,您需要强制 Qt Creator 重新运行 CMake。
如果新文件未显示在项目树中,或编译时出现 No such file or directory 错误,请执行以下步骤:
-
菜单栏选择 构建 (Build) -> 清理项目 (Clean Project)。
-
菜单栏选择 构建 (Build) -> 运行 CMake (Run CMake)(或者直接重新构建项目)。
此操作将强制 CMake 重新扫描文件并生成必要的构建文件,从而将新类添加到您的工程中,并解决头文件路径问题。
第二步:绘制彩色三角形(入门示例)

彩色三角形是 OpenGL 的 Hello World。我们使用交错数组存储位置和颜色数据。
1. 顶点数据与着色器
我们将 3 个位置(XYZ)和 3 个颜色(RGB)交错存储,总共 6 个浮点数/顶点。
// 顶点数据 (Triangle) - 3个顶点,包含位置和颜色
// 采用 glDrawArrays 方式时,不需要索引数组 (EBO)
float vertices[] = {
// Position (Location 0) // Color (Location 1)
0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 顶点 0: 顶部, 红色
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 顶点 1: 左下, 绿色
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶点 2: 右下, 蓝色
};
// 顶点着色器
const char *vertexShaderSource =
"#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aColor;\n"
"out vec3 ourColor;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
" ourColor = aColor;\n"
"}\0";
// 片段着色器
const char *fragmentShaderSource =
"#version 330 core\n"
"in vec3 ourColor;\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(ourColor, 1.0f);\n"
"}\n\0";
2. 初始化核心逻辑 (initializeGL)
这里只创建和配置 VAO 和 VBO。
-
步长 (Stride): 1 个顶点数据占用的总字节数,即
6 * sizeof(float)。 -
颜色偏移量 (Offset): 颜色数据在每个顶点数据块中开始的位置,即跳过 3 个位置 float,为
3 * sizeof(float)。
<!-- end list -->
cpp
// 核心片段:initializeGL()
void OpenGLWidget::initializeGL()
{
qDebug() << "Initialization started.";
initializeOpenGLFunctions();
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
// ... (着色器编译和链接代码) ...
// 1. 设置 VAO
vao.create();
QOpenGLVertexArrayObject::Binder vaoBinder(&vao);
// 2. 设置 VBO (仅 VBO,不使用 EBO)
vbo.create();
vbo.bind();
vbo.allocate(vertices, sizeof(vertices));
// 3. 设置顶点属性
// 步长 (Stride): 1个顶点数据占用的总字节数: 6 * sizeof(float)
GLsizei stride = 6 * sizeof(float);
program->bind();
// 位置属性 (location = 0)
program->enableAttributeArray(0);
program->setAttributeBuffer(0, GL_FLOAT, 0, 3, stride);
// 颜色属性 (location = 1)
program->enableAttributeArray(1);
program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, stride);
// 释放资源
program->release();
vbo.release();
}
3. 绘制 (paintGL)
使用 glDrawArrays 直接从 VBO 绘制。从第 0 个顶点开始,绘制 3 个顶点。
cpp
// 核心片段:paintGL()
void OpenGLWidget::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT);
program->bind();
QOpenGLVertexArrayObject::Binder vaoBinder(&vao);
// 使用 glDrawArrays 绘制:从顶点 0 开始,绘制 3 个顶点 (1个三角形)
glDrawArrays(GL_TRIANGLES, 0, 3);
program->release();
}
完全代码openglwidget.cpp:
#include "openglwidget.h"
#include <QDebug>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
// ==========================================================
// 1. 顶点数据 (Triangle) - 3个顶点,包含位置和颜色
// ==========================================================
float vertices[] = {
// Position (Location 0) // Color (Location 1)
0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // Vertex 0: Top, Red
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // Vertex 1: Bottom-Left, Green
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f // Vertex 2: Bottom-Right, Blue
};
// 注意:使用 glDrawArrays 方式时,不再需要索引数组 (EBO)
// === 2. 顶点着色器 ===
const char *vertexShaderSource =
"#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aColor;\n"
"out vec3 ourColor;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
" ourColor = aColor;\n"
"}\0";
// === 3. 片段着色器 ===
const char *fragmentShaderSource =
"#version 330 core\n"
"in vec3 ourColor;\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(ourColor, 1.0f);\n"
"}\n\0";
// ==========================================================
// 4. 核心类实现
// ==========================================================
OpenGLWidget::OpenGLWidget(QWidget *parent) : QOpenGLWidget(parent) {}
OpenGLWidget::~OpenGLWidget() {
makeCurrent();
vao.destroy();
vbo.destroy();
// 析构函数中移除了 EBO 的清理
delete program;
doneCurrent();
}
void OpenGLWidget::initializeGL()
{
qDebug() << "Triangle initialization started.";
initializeOpenGLFunctions();
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
program = new QOpenGLShaderProgram(this);
if (!program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource) ||
!program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource) ||
!program->link()) {
qDebug() << "Shader linking failed:" << program->log();
return;
}
qDebug() << "Shaders linked successfully.";
// 1. 设置 VAO
vao.create();
QOpenGLVertexArrayObject::Binder vaoBinder(&vao);
// 2. 设置 VBO (仅 VBO,不使用 EBO)
vbo.create();
vbo.bind();
vbo.allocate(vertices, sizeof(vertices));
// 3. 移除了 EBO 的设置
// 4. 设置顶点属性
GLsizei stride = 6 * sizeof(float); // 步长: 3 Pos + 3 Color
program->bind();
// 位置属性 (location 0)
program->enableAttributeArray(0);
program->setAttributeBuffer(0, GL_FLOAT, 0, 3, stride);
// 颜色属性 (location 1)
program->enableAttributeArray(1);
program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, stride);
program->release();
vbo.release();
qDebug() << "Triangle initialization finished.";
}
void OpenGLWidget::paintGL()
{
qDebug() << "Drawing triangle...";
glClear(GL_COLOR_BUFFER_BIT);
program->bind();
QOpenGLVertexArrayObject::Binder vaoBinder(&vao);
// 使用 glDrawArrays 绘制:从顶点 0 开始,绘制 3 个顶点 (1个三角形)
glDrawArrays(GL_TRIANGLES, 0, 3);
program->release();
qDebug() << "Triangle drawn successfully.";
}
void OpenGLWidget::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);
}
第三步:进阶:绘制彩色四边形 (使用 EBO)

四边形本质上是两个三角形。为了避免重复存储顶点数据(例如,右上角和左下角顶点在两个三角形中都用到),我们引入 索引 (Indices) 和 EBO (Element Buffer Object)。这是 EBO 的典型使用场景。
1. 修改顶点数据和索引
在四边形中,我们只有 4 个顶点,但有 6 个索引来定义两个三角形。
cpp
// 顶点数据 (Quad) - 4个顶点,包含位置 (XYZ) 和颜色 (RGB)
float vertices[] = {
// Position (Location 0) // Color (Location 1)
-0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 顶点 0: 左上
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 顶点 1: 左下
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // 顶点 2: 右下
0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f // 顶点 3: 右上
};
// 索引数据 (6个索引绘制2个三角形)
unsigned int indices[] = {
0, 1, 2, // 第一个三角形: 0 (左上), 1 (左下), 2 (右下)
2, 3, 0 // 第二个三角形: 2 (右下), 3 (右上), 0 (左上)
};
2. 核心 C++ 代码:彩色四边形
此时,initializeGL() 和 paintGL() 需要引入 EBO 的设置和 glDrawElements 调用(完整的四边形代码请参考 openglwidget.cpp 文件)。
cpp
#include "openglwidget.h"
#include <QDebug>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
// ==========================================================
// 1. 顶点数据 (Quad) - 4个顶点,包含位置 (XYZ) 和颜色 (RGB)
// ==========================================================
float vertices[] = {
// Position (Location 0) // Color (Location 1)
-0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 顶点 0: 左上,红色
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 顶点 1: 左下,绿色
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // 顶点 2: 右下,蓝色
0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f // 顶点 3: 右上,黄色
};
// ==========================================================
// 2. 索引数据 (EBO) - 6个索引,通过两个三角形构成四边形
// ==========================================================
unsigned int indices[] = {
0, 1, 2, // Triangle 1
2, 3, 0 // Triangle 2
};
// === 3. 顶点着色器 (传递位置和颜色) ===
const char *vertexShaderSource =
"#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aColor;\n"
"out vec3 ourColor;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
" ourColor = aColor;\n"
"}\0";
// === 4. 片段着色器 (接收并使用内插后的颜色) ===
const char *fragmentShaderSource =
"#version 330 core\n"
"in vec3 ourColor;\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(ourColor, 1.0f);\n"
"}\n\0";
// ==========================================================
// 5. 构造函数和析构函数
// ==========================================================
OpenGLWidget::OpenGLWidget(QWidget *parent)
: QOpenGLWidget(parent)
{
}
OpenGLWidget::~OpenGLWidget()
{
makeCurrent();
vao.destroy();
vbo.destroy();
// 使用原生 OpenGL API 释放 EBO
if (ebo != 0) {
glDeleteBuffers(1, &ebo);
}
delete program;
doneCurrent();
}
// ==========================================================
// 6. 初始化函数
// ==========================================================
void OpenGLWidget::initializeGL()
{
qDebug() << "Initialization started.";
initializeOpenGLFunctions();
// 设置背景色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
program = new QOpenGLShaderProgram(this);
// 编译和链接着色器
if (!program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource) ||
!program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource) ||
!program->link())
{
qDebug() << "Shader error:" << program->log();
return;
}
qDebug() << "Shaders linked successfully.";
// 1. 设置 VAO
vao.create();
QOpenGLVertexArrayObject::Binder vaoBinder(&vao);
// 2. 设置 VBO (顶点数据)
vbo.create();
vbo.bind();
vbo.allocate(vertices, sizeof(vertices));
// 3. 设置 EBO (索引数据)
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设置顶点属性
GLsizei stride = 6 * sizeof(float); // 步长: 3个位置float + 3个颜色float
program->bind();
// 位置属性 (location = 0)
program->enableAttributeArray(0);
program->setAttributeBuffer(0, GL_FLOAT, 0, 3, stride);
// 颜色属性 (location = 1)
program->enableAttributeArray(1);
// 偏移量: 跳过前面的 3 * sizeof(float) 位置数据
program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, stride);
// 释放资源
program->release();
vbo.release();
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // 释放 EBO 绑定
qDebug() << "Initialization finished.";
}
// ==========================================================
// 7. 绘制函数 (PaintGL)
// ==========================================================
void OpenGLWidget::paintGL()
{
// 清除背景
glClear(GL_COLOR_BUFFER_BIT);
program->bind();
QOpenGLVertexArrayObject::Binder vaoBinder(&vao);
// 关键修复: 再次显式绑定 EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
// 绘制 6 个索引 (2个三角形 = 1个四边形)
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // 解绑 EBO
program->release();
}
void OpenGLWidget::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);
}