原教程地址:
https://learnopengl-cn.github.io/01 Getting started/04 Hello Triangle/
程序参考地址:
https://blog.csdn.net/z136411501/article/details/79832561
经过几天的学习,终于可以用OpenGL渲染出一个三角形了。把自己这几天的学习总结下。
一、 图形渲染管线过程概述
图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。
下图是形渲染管线工作流程的简化,其中蓝色的是我们可以配置的着色器(关于着色器请参考原文),本次我们只初步掌握顶点着色器与片段着色器即可。
二、我们需要编程部分的图形渲染管线
所以本节概括起来就是对输入顶点数据的管理——>顶点着色器、片段着色器——>绘制三角形。
下面串接一下上面的概括:
开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据,当然,这些数据是有范围限制的,如范围是-1.0f到1.0f,数据需要是三维的等等。由于从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据,这时我们需要引入顶点缓冲对象(Vertex Buffer Objects, VBO)来管理GPU上的内存。到这时候还不能把数据发送给顶点着色器,我们还要告诉它如何进行解析这些数据,术语叫做链接顶点属性。经过我们手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点的属性后,之后数据就可以传递给顶点着色器了。注意还有一个VAO,它使得顶点属性调用更加方便,而且OpenGL的核心模式要求我们使用VAO。
顶点着色器(Vertex Shader)是几个可编程着色器中的一个。如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器。首先要做的第一件事是用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后编译着色器,之后才能在程序中使用它。在这里先概括下,先使用GLSL语言编写、然后编译,之后还有步骤,暂不叙述。顶点着色器输入是一个3个分量的向量,而输出是一个4个分量的向量,其间经过了“构造”。片段着色器所做的是计算像素最后的颜色输出,为了方便理解程序,该片段着色器一直输出“橘色”。该例子中片段着色器是一个具有4分量输出的向量。着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本,如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
最后绘制三角形。
关于矩形的绘制用到了索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。
三、费曼学习法理解
关于上面的过程我是陆续看了几天才大概理解的,之前对图形学编程基础为0。为了方便理解,我把他们理解为一个各种土豆变成各种口味薯片的工业自动化过程。可能有些地方比喻不恰当,望大家不吝指正,感谢!
首先是对输入的土豆的处理,这对输入的土豆是有一定要求的,不能太大也不能太小,而且还要是3个为一组或是其他的数量为一组,OpenGL中是三个三维顶点的输入;待加工的土豆有了,但是总不能一有土豆就加工,而是我们等待有一定数量土豆再开工,这样批量处理比较科学化,所以就有了VBO对GPU内存的管理;OK,土豆数量够了,接下来把合格的土豆送往各部分土豆切割器,三个土豆变成了N多土豆片,再“编译”土豆片一下,土豆切割部分就完成了;片段着色器就是土豆片口味区分的部分了,芥末味、黄瓜味、酸奶味等等;最后通过着色器程序把不同口味的土豆片变成不同口味的薯片;;关于链接顶点属性,是自动化切割土豆片时需要设置一次来了几个土豆,根据设置调整切割土豆的刀具,我实在快编不下去了......VAO让我们不用每次设置来了几个土豆,一次设置,终生享用,这就是VAO.
四、Qt程序与注释
1. .h部分
代码语言:javascript复制#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
class MyGLWidget : public QOpenGLWidget, protected QOpenGLExtraFunctions
{
Q_OBJECT
public:
MyGLWidget(QWidget *parent = nullptr);
~MyGLWidget();
protected:
virtual void initializeGL();
virtual void resizeGL(int w, int h);
virtual void paintGL();
private:
//顶点缓冲对象 Vertex Buffer Object,VBO,教程中第一个出现的OpenGL对象
GLuint VBO;
GLuint VAO;
GLuint EBO;
GLuint m_shaderProgram; //着色器程序对象
2. .cpp部分
代码语言:javascript复制//着色器语言GLSL(OpenGL Shading Language)编写的顶点着色器
static const char *vertexShaderSource =
"#version 330 coren"
"layout(location = 0) in vec3 aPos;n"
"void main(){n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);n"
"}n ";
//着色器语言GLSL(OpenGL Shading Language)编写的片段着色器
static const char *fragmentShaderSource =
"#version 330 coren"
"out vec4 FragColor;n"
"void main(){n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);n"
"}n ";
#define TRIANGLE
//#define RECTANGLE
MyGLWidget::MyGLWidget(QWidget *parent)
: QOpenGLWidget(parent)
{
}
MyGLWidget::~MyGLWidget()
{
}
void MyGLWidget::initializeGL()
{
this->initializeOpenGLFunctions(); //重要!一定要在调用任何gl函数前调用该函数!!
//着色器部分
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); //创建顶点着色器对象
glShaderSource(GLuint(vertexShader), 1, &vertexShaderSource, nullptr); //着色器源码附加到着色器对象
glCompileShader(GLuint(vertexShader)); //编译
int is_success; //检查是否编译成功,未成功则需返回错误
char infoLog[512];
glGetShaderiv(GLuint(vertexShader), GL_COMPILE_STATUS, &is_success);
if (!is_success)
{
glGetShaderInfoLog(GLuint(vertexShader), 512, nullptr, infoLog);
qDebug() << "ERROR::SHADER::VERTEX::COMPILATION_FAILED" << infoLog ;
}
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //创建片段着色器对象,与顶点着色器相似
glShaderSource(GLuint(fragmentShader), 1, &fragmentShaderSource, nullptr);
glCompileShader(GLuint(fragmentShader));
glGetShaderiv(GLuint(fragmentShader), GL_COMPILE_STATUS, &is_success);
if (!is_success)
{
glGetShaderInfoLog(GLuint(fragmentShader), 512, nullptr, infoLog);
qDebug() << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED" << infoLog;
}
m_shaderProgram = glCreateProgram(); //创建着色器程序对象
glAttachShader(m_shaderProgram, GLuint(vertexShader));
glAttachShader(m_shaderProgram, GLuint(fragmentShader));
glLinkProgram(m_shaderProgram); //之前编译的着色器附加到程序对象上
glGetProgramiv(m_shaderProgram, GL_LINK_STATUS, &is_success);
if (!is_success) {
glGetProgramInfoLog(m_shaderProgram, 512, nullptr, infoLog);
qDebug() << "ERROR::SHADER::PROGRAM::LINKING_FAILEDn" << infoLog ;
}
glDeleteShader(vertexShader); //着色器对象链接到程序对象以后,删除着色器对象
glDeleteShader(fragmentShader);
//VAO,VBO数据部分
#ifdef TRIANGLE
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f, // left
0.5f, -0.5f, 0.0f, // right
0.0f, 0.5f, 0.0f // top
};
#else //绘制矩形
GLfloat 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开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
#endif
//两个参数,第一个为需要创建的缓存数量。第二个为用于存储单一ID或多个ID的GLuint变量或数组的地址
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO); //顶点缓冲对象处理
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
//使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//之前定义的顶点数据复制到缓冲的内存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
#ifdef RECTANGLE
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
#endif
//glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), nullptr);
glEnableVertexAttribArray(0); //以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
void MyGLWidget::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);
}
void MyGLWidget::paintGL()
{
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(m_shaderProgram); //激活程序对象
glBindVertexArray(VAO);
#ifdef TRIANGLE
glDrawArrays(GL_TRIANGLES, 0, 3);
#else //绘制矩形
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
#endif
// glUseProgram(0);
}
3. 程序运行效果
矩形部分老铁们自己理解下EBO吧!
小结:
对于程序中 文字解释的排版欢迎大家多提出宝贵的意见,感谢!