开头
废话不多说,该说的在学习笔记1中已经说明。这篇文章会继续简单认识OpenGL里的一些东西。之前的Demo没有编号,所以从这里开始小标题前面的编号也就对应github上面的project代码
github链接:https://github.com/OnlyCharSaMa/Qt-OpenGL-Demos/tree/main
001.编译链接着色器
引入shader
在OpenGL中使用着色器可以用glUseProgram(<shaderProgram>);来实现
在Qt中,Qt提供了一些工具来让顶点着色器和片段着色器单独存在,使得项目更加简洁。但是这里我们不是做什么大项目,所以我们就直接写:
const char *vertexShaderSource =
"#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main() {\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); \n"
"}\0";
const char *fragmentShaderSource =
"#version 330 core \n"
"out vec4 FragColor;\n"
"void main() { \n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n";
让我们的顶点着色器和片段着色器存储在字符串里
编译并链接shader
void MyGLWidget::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(0,0,0,1);
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
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), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 001 New Code
// 其实和之前一样,先申请一个id,作为“门牌号”来访问shader对象
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
shaderProgram = glCreateProgram(); // shaderProgram 创建于类内,以便之后使用
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader); // 绑定Shader
glLinkProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}
其实新的代码格式上没啥太大变化,就申请id然后创建对象,最后操作罢了。这里详细解释下glShaderSource
void glShaderSource(GLuint shader, GLsizei count, const GLchar **string, const GLint *length);
shader
: 一个表示着色器对象的无符号整数(GLuint)。在这里,vertexShader
是指向顶点着色器对象的引用。count
: 一个整数(GLsizei),表示传递给着色器的源代码字符串数量。在这个例子中,值为1,表示只传递一个源代码字符串string
: 指向指向源代码字符串的指针的指针(const GLchar **)。在这里,&vertexShaderSource
是指向存储顶点着色器源代码的指针的指针length
: 一个指向整数数组的指针(const GLint *),用于指定每个源代码字符串的长度。如果源代码字符串都是以null终止的(以\0
结尾),那么可以将length参数设置为NULL,表示源代码字符串的长度将由OpenGL自动计算
使用shaderProgram
然后我们在paintGL函数里使用它即可:
glUseProgram(shaderProgram); // 使用shaderProgram
002. 使用EBO绘制矩形
啥是EBO
EBO,全名 Element Buffer Object,也叫IBO,Index Buffer Object。中文名索引缓冲对象
为啥用EBO
这里再次提到OpenGL主要处理三角形,以绘制三角形来平凑成其他图形。这意味着我们要画一个矩形就要用两个三角形拼凑出。
但是我们想一想,两个三角形组成了一个矩形,那每个三角形各会有两个顶点重合。那么一共6个顶点有2个顶点的数据与剩下4个中的两个相同,这样子开销会变大,特别是绘制数量变得特别大的时候。
当然这里给一下用开销大的方式的代码
float vertices[18] = {
// first triangle
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, 0.5f, 0.0f // top left
// second triangle
,0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};
void MyGLWidget::paintGL()
{
glDrawArrays(GL_TRIANGLES, 0, 6); // 这里count改为6表示绘制6个顶点
}
EBO大概怎么工作
我们之前不是说有两个顶点重复了吗,那我们就把这两个顶点去掉
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
};
但是这还不够,有这几个顶点,opengl也不知道这几个顶点要以什么方式组成若干个三角形,然后绘制。所以我们要给它描述一下这几个顶点如何构成我们需要的矩形的两个三角形:
uint indices[] = {
0, 1, 3, // first triangle index
1, 2, 3 // second triangle index
};
上面这个indices就代表了第0,1,3个顶点和第1,2,3个顶点分别组成2个三角形
创建EBO
去创建EBO的过程和VBO的过程大同小异,因为都是一个Buffer Object。走流程就是:
- 在类内声明一个id GLuint EBO;
- 生成一个EBO glGenBuffer(1, &EBO);
- 绑定Buffer glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
- 缓存数据 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
initiateGL中应该是这样的:
void MyGLWidget::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(0,0,0,1);
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
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), (void*)0);
glEnableVertexAttribArray(0);
// glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader); // 绑定Shader
glLinkProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// 002 New Code
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glBindVertexArray(0);
}
值得一提的是因为VAO需要记录我们关于顶点的数据,所以我们之前在前面解绑的VAO必须放到后面解绑,要不然会记录不到EBO导致直接崩溃
更值得关注的是VAO对于Buffer Object的记录的重视程度是不一样的:VAO不会存储VBO的glBindBuffer函数调用,但是会存储EBO的glBindBuffer函数调用。这意味着如果你在VAO还在记录Buffer Object状态的时候解绑了EBO那么你使用EBO的时候必须再次绑定才行
绘制EBO
void MyGLWidget::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glColor3f(1,1,1);
// glDrawArrays(GL_TRIANGLES, 0, 6); – old way to draw rect
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}
这里值得注意的是 glDrawElements的最后一个参数,传入0就是让OpenGL使用已经绑定的EBO。当EBO没有绑定时可以直接传入数组的指针,这样OpenGL会使用你传进来的数据(但是如果你绑定了EBO却直接传入数组指针,那可能会导致绘制异常)
003 004
只是练习题而已,看看就行
005. QOpenGLShaderProgram
之前我们用shader的方式都是直接作为字符串写在C++里,但这会为我们修改shader带来极大的不便,而且编译链接的过程十分折磨人。所以Qt为我们封装好了类QOpenGLShaderProgram
但是其实没啥好讲的,熟悉api就行
有了这个类我们就不需要编译顶点着色器和片段着色器了,只需要link。流程:
- 用addShader或者addShaderFromXXX 添加Shader
- link() 链接
- bind() 使用shader
006. GLSL
这个打算单开在学习笔记3里