本系列文章主要是记录学习OpenGL的过程,旨在驱动学习理解OpenGL,最终达到能够使用相关接口解决实际项目问题,学习流程参考《LearnOpenGL》。主要展现形式是"代码示例+接口分析"的形式,编码主要是基于Qt封装的OpenGL模块展开,这样对于我来说更加熟悉。
OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。
第一个项目绘制三角形,如下图所示:
#include
#include
class CustomGLWidget : public QOpenGLWidget, QOpenGLFunctions_3_3_Core
{
Q_OBJECT
public:
explicit CustomGLWidget(QWidget *parent = nullptr);
signals:
public slots:
protected:
void initializeGL() override;
void resizeGL(int w, int h) override;
void paintGL() override;
};
static unsigned int VBO, VAO;
static unsigned int shaderProgram;
static float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f,
};
static const char* vertexShaderSource = "#version 330 core \n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
static const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
"FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\0";
CustomGLWidget::CustomGLWidget(QWidget *parent)
: QOpenGLWidget(parent)
{
}
void CustomGLWidget::initializeGL( )
{
initializeOpenGLFunctions();
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// 绑定顶点数组对象
glBindVertexArray(VAO);
// 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 设定顶点属性指针
glVertexAttribPointer(0 ,3, GL_FLOAT, GL_FALSE, 3* sizeof(float), nullptr);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// 编译顶点着色器
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
glCompileShader(vertexShader);
// 编译片段着色器
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);
// 链接
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}
void CustomGLWidget::resizeGL(int w, int h)
{
QOpenGLWidget::resizeGL(w, h);
}
void CustomGLWidget::paintGL()
{
// 背景颜色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 绘制三角形
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
借助Qt使用OpenGL,主要要使用了两个类QOpenGLWidget以及QOpenGLFunctions_3_3_Core。QOpenGLWidget为窗体部件,需要重写initializeGL、resizeGL以及paintGL函数。QOpenGLFunctions_3_3_Core则提供对应版本的OpenGL函数,这里使用的是3.3版本与《LearnOpenGL》相同。
OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由OpenGL库的开发者自行决定(译注:这里开发者是指编写OpenGL库的人)。因为OpenGL规范并没有规定实现的细节,具体的OpenGL库允许使用不同的实现,只要其功能和结果与规范相匹配(亦即,作为用户不会感受到功能上的差异)。
相关的封装使得Qt能够更加方便的调用OpenGL函数,也就是更容易的调用显卡中的函数。
实际的OpenGL库的开发者通常是显卡的生产商。你购买的显卡所支持的OpenGL版本都为这个系列的显卡专门开发的。当你使用Apple系统的时候,OpenGL库是由Apple自身维护的。在Linux下,有显卡生产商提供的OpenGL库,也有一些爱好者改编的版本。这也意味着任何时候OpenGL库表现的行为与规范规定的不一致时,基本都是库的开发者留下的bug。
initializeOpenGLFunctions将Qt相关函数与显卡提供的OpenGL库建立联系,也就是找到显卡中所提供的OpenGL库的对应的函数地址。
通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理顶点数据,在GPU内存(通常被称为显存)中储存大量顶点,借助这些缓冲对象可以大批量的把数据从CPU传递至GPU中,而不是逐个发送。
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中,用于管理VBO中的数据,方便代码中的调用。
顶点数组对象会储存以下这些内容:
glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
通过glVertexAttribPointer设置的顶点属性配置。
通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
glCompileShader编译着色器,glLinkProgram链接着色器程序,也就是通过着色器语言GLSL(OpenGL Shading Language)编写的vertexShaderSource、fragmentShaderSource 编译生成可以在GPU上跑的程序,用于相关元素的渲染,渲染流程如下图所示:
这里在《LearnOpenGL》中有详细解释,实际编码只需关系顶点着色器和片段着色器,大概可以理解为顶点着色器控制形状、片段着色器控制颜色。
glClearColor绘制背景颜色,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
glUseProgram(shaderProgram)使用已经编译好的着色器程序,glBindVertexArray(VAO)绑定顶点数组对象,glDrawArrays(GL_TRIANGLES, 0, 3)绘制图元。