最近在系统的学习有关OpenGL的内容,参考的主要学习资料是Joey de Vries的教程在Github上的中文翻译:LearnOpenGL CN
这些笔记主要是汇总整理一些其中的想法,并结合Qt的内容给出自己的理解,对于每篇文章的实现的Qt代码,贴在文后:
顶点输入
在用OpenGL画出图形之前,要首先输入一些数据。OpenGL不会把所有输入进来的3D坐标都变换为屏幕上的2D像素,只有3D坐标在三个轴上都在-1.0到1.0之间时才会处理它。所有标准化设备坐标Normalized Device Coordinates范围内的坐标才会最终呈现在屏幕上。
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
}
一旦顶点坐标已经在顶点着色器中处理过,那么他们就应该已经是标准化设备坐标了,标准化设备坐标会接着变换为屏幕空间坐标,这是通过glViewpot函数提供的数据进行视口变换完成的。所得的屏幕空间坐标优惠被变换为片段输入到片段着色器中。
定义这样的顶点数据以后,我们会把它作为输入发送给顶点着色器。它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。
我们通过顶点缓冲对象管理这个内存,它会在GPU内存中存储大量顶点。使用这些缓冲对象的好处时我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU发送数据相对较慢,所以我们要一次尽可能多的发送数据。
unsigned int VBO;
glGenBuffers(1, &VBO);
这个缓冲有一个独一无二的ID,但是还没确定这个缓冲对象类型,OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们的缓冲类型不同。
glBindBuffer(GL_ARRAY_BUFFER, VBO);
这一刻,这个VBO就正式的确定为ID为1的顶点缓冲对象,我们使用任何GL_ARRAY_BUFFER目标上的缓冲调用都会用来配置当前绑定的缓冲。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
使用这个函数就可以把用户定义的数据复制到当前绑定缓冲。第一个参数是目标缓冲类型第二个参数是传输数据的大小;第三个是实际希望发出的数据大小;第四个是我们希望显卡如何管理给定数据。
经过了这几个步骤,我们定义出的三角形已经存储在显卡的内存里了,并且可以用VBO进行管理。下面建立顶点和片段着色器来处理这个数据。
顶点着色器
顶点着色器是我们需要自己设置的第一个可编程着色器,如果打算做渲染,我们至少需要设置一个顶点和一个片段着色器。
#version 330 core
layout (location = 0) in vec3 aPos;
void main(){
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
能够看出来,GLSL看起来很像C++,每个着色器都起始于一个版本声明。接下来是使用关键字in,在顶点着色器中声明所有的输入顶点属性。由于现在只涉及到位置数据,所以就只需要一个顶点属性。GLSL有一个向量数据类型,包含着1到4维的向量,从他的后缀就可以看出来。由于只输入3D坐标,我们就创建输入变量aPos,通过layout (location = 0) 设定了输入变量的位置值。
为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型,这样就完成了数据的输入;但在真实情况中一般都不是标准化设备坐标,所以首先必须把它们转换至OpenGL的可视区域内。
编译着色器
之前编写的着色器源码已经存储在一个*.vert文件中,为了能让OpenGL使用它,我们必须在运行时动态编译源码。
在示例中,是将这份代码存放在一个字符串vertexShaderSource中,但是这并不影响它的原理,
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
这里不对这段代码做太多解释,原因是在Qt中还有更为简洁的编写方式,这里只是为了了解流程。
片段着色器
片段着色器Fragment Shader也是第二个用于渲染三角形的着色器,片段着色器所做的是计算像素最后的颜色输出。
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
编译片段着色器和顶点着色器类似:
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader);
同样,在Qt中也有读入*.frag文件的简便形式。
着色器程序
着色器程序对象Shader Program Object是多个着色器合并之后最终连接完成的版本。如果要使用刚才编译的着色器就必须要把它们链接Link为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,就会得到一个错误。
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
然后就可以激活程序对象开始使用,在链接完之后,着色器对象就可以不要了,要删除它们
glUseProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
以上这些都只是示例,在Qt中有更为简洁的办法。现在,数据已经发送到了GPU,也指定了GPU要在顶点和片段着色器中处理它们,但是OpenGL还不知道怎么解释这些数据,还不知道怎么链接这些属性。
链接顶点属性
顶点着色器允许任意形式的顶点属性作为输入,这很灵活,但是同时我们也需要自己手动确认哪一部分对应着哪一属性:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
关于glVertexAttribPointer函数的参数:
- 第一个参数指定我们要配置的顶点属性。在顶点着色器中使用了layout(location = 0)定义了position顶点属性,它可以把顶点属性的位置值设置为0。
- 第二个参数制定了顶点属性的大小。vec3是3个值所以大小是3。
- 第三个指定数据类型,这里是GL_FLOAT
- 第四个是是否希望数据被标准化。如果设置为GL_TRUE,就会都被映射到0到1之间。
- 第五个叫做步长,告诉我们在连续的顶点属性组之间的间隔
- 第六个是表示数据在缓冲中的偏移量
然后使用glEnableVertexAttribArray启用顶点属性
// 0.复制顶点数组到缓冲中
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1.设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2.渲染一个物体要使用着色器程序
glUseProgram(shaderProgram);
// 3.绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
在Qt中相关步骤与以上步骤雷同,但也有不一样的地方。
顶点数组对象
顶点数组对象Vertex Array Object可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会存储在这个VAO中。当配置顶点属性指针式,只需要将那些调用执行一次,之后再绘制物体的时候只需要邦迪相应的VAO就可以了。
unsigned int VAO;
glGenVertexArrays(1, &VAO);
这样就创建了一个VAO,要想使用VAO,就只需要绑定VAO,然后再配置属性指针,之后解绑VAO供以后使用,然后再打算绘制一个物体的时候就再物体前把VAO绑定到希望用的设定上就行:
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
最后的三角形
在前面的代码中总会有someOpenGLFunctionThatDrawsOurTriangle()出现,这个就是为了代指一个绘制函数,而实际中用到的绘制函数则是:
glDrawArrays(GL_TRIANGLES, 0, 3);
第一个参数式打算绘制的图元类型;第二个参数指定了顶点数组的起始索引,这里是0;最后一个参数指定了我们打算绘制多少个顶点,这里是3。
索引缓冲对象
在渲染顶点还有最后一个话题——索引缓冲对象Index Buffer Object,IBO。假设我们化的图形不是一个三角形而是一个矩形。我们可以用两个三角形组成一个矩形(在OpenGL中只要处理三角形),就会有下面的顶点集合:
float vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二个三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
可以看出,有两个点出现了两次,这就带来了额外的开销,有效的索引顺序就是我们可以简化的模式。
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = { // 注意索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
这一次定义的时候,就只有四个点,而且每个顶点都不重复。下一步和顶点缓冲对像的套路一样,我们需要创建索引缓冲对象:
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
最后的绘制函数有所变化,第一个参数仍然是绘制图元模式;第二个是要绘制多少个顶点;第三个是索引的类型;第四个是EBO的偏移量。
最后所有代码总结到一起:
// ..:: 初始化代码 :: ..
// 1. 绑定顶点数组对象
glBindVertexArray(VAO);
// 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
以上就是有关三角形带来的知识结构。
Qt工程
openglwidget.h
#ifndef OPENGLWIDGET_H
#define OPENGLWIDGET_H
#include <QMainWindow>
#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
class OpenGLWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core
{
Q_OBJECT
public:
explicit OpenGLWidget(QWidget *parent = nullptr);
~OpenGLWidget();
protected:
virtual void initializeGL() override;
virtual void resizeGL(int w, int h) override;
virtual void paintGL() override;
private:
QOpenGLShaderProgram shaderProgram;
// 这就是Qt中比较简便的形式,可以直接创建一个着色器程序。
signals:
};
#endif // OPENGLWIDGET_H
openglwidget.cpp
#include "openglwidget.h"
#include <QDebug>
#include <QFile>
static GLuint VBO, VAO, EBO;
OpenGLWidget::OpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
}
OpenGLWidget::~OpenGLWidget()
{
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
}
void OpenGLWidget::initializeGL(){
// 初始化函数
initializeOpenGLFunctions();
// 读入顶点着色器
bool success = shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/vertex.vert");
if(!success){
qDebug()<<"failed!"<<shaderProgram.log();
return;
}
// 读入片段着色器
success = shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/frag.frag");
if(!success){
qDebug()<<"failed!"<<shaderProgram.log();
return;
}
// 链接着色器
success = shaderProgram.link();
if(!success){
qDebug()<<"failed!"<<shaderProgram.log();
return;
}
// 顶点坐标
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};
// 索引坐标
unsigned int indices[] = {
0, 1, 3, // first Triangle
1, 2, 3 // second Triangle
};
glGenVertexArrays(1, &VAO); // 定义顶点坐标数组
glGenBuffers(1, &VBO); // 定义缓冲数组
glGenBuffers(1, &EBO); // 定义索引缓冲数组
glBindVertexArray(VAO); //首先绑定顶点坐标数组,在后面画矩形的时候只需要启用一次顶点数组
//绑定缓冲数组和顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof (vertices), vertices, GL_STATIC_DRAW);
//绑定索引数组和索引数据
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof (indices), indices, GL_STATIC_DRAW);
//确认顶点属性和启用属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof (GLfloat), (void*)0);
glEnableVertexAttribArray(0);
//解绑顶点缓冲对象
glBindBuffer(GL_ARRAY_BUFFER, 0);
//解绑顶点数组对象
glBindVertexArray(0);
}
void OpenGLWidget::resizeGL(int w, int h){
glViewport(0, 0, w, h);
}
void OpenGLWidget::paintGL(){
//清理画布
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//绑定启用着色器
shaderProgram.bind();
//启用顶点数组
glBindVertexArray(VAO);
//画图
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
//释放着色器
shaderProgram.release();
}
可以看出在Qt中将编译着色器程序这一步骤简化到了着色器程序这一类里,不需要在逐一去编译,个人理解极大的简化了初始化设置的步骤。