【OpenGL】绘制第一个三角形
本文介绍了在QT5环境下使用OpenGL绘制第一个三角形的步骤。主要内容包括: 环境配置:通过CMake配置QT5和OpenGL库,链接必要的库文件。 窗口创建:继承QOpenGLWidget实现自定义OpenGL窗口,重写initializeGL、paintGL和resizeGL三个关键函数,完成OpenGL初始化、绘制和窗口尺寸调整。 绘制三角形: 使用顶点数组对象(VAO)和顶点缓冲对象(V
参考资料:https://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-2-the-first-triangle/
一、环境配置
这里使用的QT5的opengl环境,配合CMake在MacOS下进行的,因此需要在CMakeLists.txt
中进行配置:
- 主要是进行链接库文件,一个是QT的openGL库,还有一个就是操作系统本身的GL库
- 关于其他QT的配置,这里不再列举出来,实际上只需要配置好QT的环境后,查找opengl的库文件即可
# 查找 Qt5 和系统 OpenGL 库
find_package(Qt5 COMPONENTS Core Gui Widgets OpenGL REQUIRED)
find_package(OpenGL REQUIRED)
target_link_libraries(opengl1-1
Qt5::Core
Qt5::Gui
Qt5::Widgets
Qt5::OpenGL
OpenGL::GL
)
二、创建一个窗口
我们的实现环境是QT,因此很多时候我们不需要自己实现窗口,也不需要自己实现一个时间循环,我们唯一需要实现的就是:实现对应的initializeGL
和paintGL
两个函数
initilizeGL
主要是初始化OpenGL的函数,并且这一步骤在绘制之前将会调用paintGL
主要是利用窗口的重绘,实现openGL的状态机的每一帧的改变,也就是在这一个函数内部我们需要实现openGL的状态保持与改变
2.1 实现my_glwidget类
- 为了实现自己自定义的一个opengl窗口,在QT中比较常用的操作是使用继承的操作
- 一方面,使用public继承QOpenGLWidget窗口(为了能够在外部调用它)
- 另一方面使用protected继承QOpenGLFunction类(为了只在内部实现它的接口,比如paintGL)
通常的写法如下:
头文件
- 通常需要重写这三个虚函数:
initializeGL
、paintGL
、resizeGL
class MyGLWidget : public QOpenGLWidget, protected QOpenGLFunctions {
public:
MyGLWidget(QWidget* parent = nullptr);
~MyGLWidget();
protected:
void initializeGL() override;
void paintGL() override;
void resizeGL(int w, int h) override;
};
源文件
-
在
initilizeGL
中需要初始化opengl的内部函数,否则会导致空指针,使用initializeOpenGLFunctions
函数调用即可。这里也可以设置一下屏幕的清理颜色,我就使用黑色清理了(rgba(0,0,0,1)) -
paintGL
主要对opengl窗口进行绘制,这里简单每一帧清除一下屏幕的颜色即可 -
resizeGL
主要是实现窗口拉伸时的视口的变化,因为opengl的视口需要手动修改,这里我们假设无论怎么拉伸外部窗口,内部opengl的视口总是和窗口保持一致,因此直接设置glViewport(0, 0, w, h)
,其中w
和h
是当前拉伸后的窗口宽高
#include "my_glwidget.h"
MyGLWidget::MyGLWidget(QWidget *parent): QOpenGLWidget(parent)
{
}
MyGLWidget::~MyGLWidget() {
}
void MyGLWidget::initializeGL() {
initializeOpenGLFunctions();
glClearColor(0.0f,0.0f,0.0f,1.0f);
}
void MyGLWidget::paintGL() {
glClear(GL_COLOR_BUFFER_BIT);
}
void MyGLWidget::resizeGL(int w, int h) {
glViewport(0, 0, w, h);
}
通常,运行后的结果如下:
二、顶点数组对象(VAO)
绘制三角形之前,需要创建并绑定一个VAO,比如下面的代码:
GLuint VertexArrayID;
glGenVertexArrays(1, &VertexArrayID);
glBindVertexArray(VertexArrayID);
在窗口创建完毕后,需要进行上述的操作,绑定了VAO后,之后所有的操作都是基于当前绑定的VAO进行,比如后续的VBO、FBO等等操作
2.1 屏幕坐标系
三点确定一个三角形。我们常常用“顶点”(Vertex,复数vertices)来指代3D图形学中的点。一个顶点有三个坐标:X,Y和Z。您可以这样想象这三根坐标轴:
- X轴朝右
- Y轴朝上
- Z轴指向您后面(没错,是后面,不是前面)
还有更形象的方法:使用右手定则
- 拇指代表X轴
- 食指代表Y轴
- 中指代表Z轴。如果您的拇指指向右边,食指指向天空,那么中指将指向您的后面。
为什么Z轴方向这么奇怪呢?简言之:因为基于右手定则的坐标系统被广泛使用了100多年,这一系统涵盖了许多实用工具,唯一的缺点就是Z方向比较别扭。
补充说明一下,您的手可以自由移动,X轴,Y轴和Z轴同样也能移动(详见后文)。
我们需要三个三维点来组成一个三角形;下面来定义一个三角形:
static const GLfloat g_vertex_buffer_data[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
0.0f, 1.0f, 0.0f
};
第一个顶点是(-1, -1, 0)。 这意味着如果不变换该顶点,它就将显示在屏幕的(-1, -1)位置。这是什么意思呢?屏幕的原点在中间,X轴朝右,Y轴朝上。屏幕坐标如下图:
这是显卡内置的坐标系,无法改变。(-1, -1)是屏幕的左下角,(1, -1)是右下角,(0, 1)位于中上部。这个三角形占据了大部分屏幕。
2.2 绘制三角形
下一步,通过缓冲把三角形传给OpenGL:
这步操作仅需执行一次,也就是说,这个步骤实际上是在initializeGL
函数中实现
//生成一个VBO
glGenBuffers(1, &vertexBuffer);
//绑定VBO
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
//分配VBO内存:GL_STATIC_DRAW 不经常修改缓冲区
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);
在initializedGL
最后别忘记了解绑VAO,虽然在只有一个VAO的情况下这不是必备的
//解绑vao
glBindVertexArray(0); // 解绑VAO
在paintGL
中重新绑定VAO,绑定VBO,并且描述如何极细VBO,同时启动我们的layout = 0
,这里与着色器有关,后续章节介绍
//opengl是一个状态机,每一帧都需要解绑vao再绑定vao
glBindVertexArray(VertexArrayID);
//描述顶点信息,绑定的layout = 0,float类型,不需要归一化 stride = 0(只有唯一的位置信息)偏移0(只有唯一的位置信息)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
//启动layout = 0
glEnableVertexAttribArray(0);
描述完毕后,我们绘制三角形:
glDrawArrays(GL_TRIANGLES, 0, 3);
运行结果通常如下:
三、完整代码
完整代码如下:
my_glwidget.h
//
// Created by liuhang on 2025/9/16.
//
#include <QOpenGLWidget>
#include<QOpenGLFunctions>
#ifndef UNTITLED_MYGLWIDGET_H
#define UNTITLED_MYGLWIDGET_H
class MyGLWidget : public QOpenGLWidget, protected QOpenGLFunctions {
public:
MyGLWidget(QWidget* parent = nullptr);
~MyGLWidget();
protected:
void initializeGL() override;
void paintGL() override;
void resizeGL(int w, int h) override;
private:
GLuint VertexArrayID; //VAO
GLuint vertexBuffer; //VBO
};
#endif //UNTITLED_MYGLWIDGET_H
my_glwidget.cpp
#include "my_glwidget.h"
#include<iostream>
//顶点数组
static const GLfloat g_vertex_buffer_data[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
0.0f, 1.0f, 0.0f
};
MyGLWidget::MyGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
}
void MyGLWidget::initializeGL() {
//初始化所有opengl函数
initializeOpenGLFunctions();
//清理画布:颜色 RGB glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
//生成一个VAO
glGenVertexArrays(1, &VertexArrayID);
//绑定VAO
glBindVertexArray(VertexArrayID);
//生成一个VBO
glGenBuffers(1, &vertexBuffer);
//绑定VBO
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
//分配VBO内存:GL_STATIC_DRAW 不经常修改缓冲区
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);
//解绑vao
glBindVertexArray(0); // 解绑VAO
}
void MyGLWidget::paintGL() {
glClear(GL_COLOR_BUFFER_BIT);
//opengl是一个状态机,每一帧都需要解绑vao再绑定vao
glBindVertexArray(VertexArrayID);
//描述顶点信息,绑定的layout = 0,float类型,不需要归一化 stride = 0(只有唯一的位置信息)偏移0(只有唯一的位置信息)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
//启动layout = 0
glEnableVertexAttribArray(0);
std::cout << "vao = " << VertexArrayID << std::endl; //vao = 1804515168
std::cout << "vbo = " << vertexBuffer << std::endl; //vbo = 1
//绘制三角形(GL_TRIANGLES),顶点数组的开始索引,绘制的顶点个数
glDrawArrays(GL_TRIANGLES, 0, 3);
}
void MyGLWidget::resizeGL(int w, int h) {
glViewport(0, 0, w, h);
}
MyGLWidget::~MyGLWidget() {
//切换到opengl上下文
makeCurrent();
//删除VAO和VBO
glDeleteVertexArrays(1, &VertexArrayID);
glDeleteBuffers(1, &vertexBuffer);
doneCurrent();
}
更多推荐
所有评论(0)