来自:QT 5.4+ OpenGL编程 - brain2004的专栏 - CSDN博客.html(https://blog.csdn.net/brain2004/article/details/70768411?utm_source=blogxgwz8)
1.OpenGL可编程管线
1.1 VAO,VBO,EBO编程路线
VAO顶点数组对象
顶点数组对象(Vertex Array Object, VAO) 可以像 顶点缓冲对象 那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。
一个顶点数组对象会储存以下这些内容:
glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
通过glVertexAttribPointer设置的顶点属性配置。
通过glVertexAttribPointer调用进行的顶点缓冲对象与顶点属性链接。
要想使用VAO,要做的只是使用生成并绑定VAO。从绑定之后起,我们应该配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了
1 GLuint VAO; 2 glGenVertexArrays(1, &VAO);
1 // ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: .. 2 // 1. 绑定VAO 3 glBindVertexArray(VAO); 4 // 2. 把顶点数组复制到缓冲中供OpenGL使用 5 glBindBuffer(GL_ARRAY_BUFFER, VBO); 6 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 7 // 3. 设置顶点属性指针 8 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); 9 glEnableVertexAttribArray(0); 10 //4. 解绑VAO 11 glBindVertexArray(0); 12 13 [...] 14 15 // ..:: 绘制代(游戏循环中) :: .. 16 // 5. 绘制物体 17 glUseProgram(shaderProgram); 18 glBindVertexArray(VAO); 19 someOpenGLFunctionThatDrawsOurTriangle(); 20 glBindVertexArray(0);
VBO 顶点缓冲数据属性(位置)
- 位置数据被储存为32-bit(4字节)浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列。
- 数据中第一个值在缓冲开始的位置。
有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer函数的参数非常多,所以我会逐一介绍它们:
- 第一个参数指定我们要配置的顶点属性。我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location),它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。(ZC:可以看 官方3.x的Tutorials里面的项目"tutorial02_red_triangle"里面的"SimpleVertexShader.vertexshader"就是这么做的)
- 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
- 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
- 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
- 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个GLfloat之后,我们把步长设置为3
- sizeof(GLfloat)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注:这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
- 最后一个参数的类型是GLvoid*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVetexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVetexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。
现在我们已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。多属性数据详见下。
EBO索引缓冲对象
索引绘制(Indexed Drawing)允许我们在已经存储的顶点中选取不同的顶点来绘制不同的图形,而不需要对对同一个顶点进行重复定义。进行索引绘制需要借助索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。和顶点缓冲对象一样,EBO也是一个缓冲,它专门储存索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点。首先,我们先要定义(独一无二的)顶点,和绘制出矩形所需的索引:
1 GLfloat vertices[] = { 2 0.5f, 0.5f, 0.0f, // 右上角 3 0.5f, -0.5f, 0.0f, // 右下角 4 -0.5f, -0.5f, 0.0f, // 左下角 5 -0.5f, 0.5f, 0.0f // 左上角 6 }; 7 8 GLuint indices[] = { // 注意索引从0开始! 9 0, 1, 3, // 第一个三角形 10 1, 2, 3 // 第二个三角形 11 }
创建索引缓冲对象
1 GLuint EBO; 2 glGenBuffers(1, &EBO);
与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。
1 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); 2 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
要注意的是,我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,来指明我们从索引缓冲渲染。使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制:
1 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); 2 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。这意味着我们必须在每次要用索引渲染一个物体时绑定相应的EBO,这还是有点麻烦。不过顶点数组对象同样可以保存索引缓冲对象的绑定状态。VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象。绑定VAO的同时也会自动绑定EBO。
当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。
VAO,VBO,EBO编程路线如下:
1 // ..:: 初始化代码 :: .. 2 // 1. 绑定顶点数组对象 3 glBindVertexArray(VAO); 4 // 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用 5 glBindBuffer(GL_ARRAY_BUFFER, VBO); 6 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 7 // 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用 8 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); 9 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); 10 // 3. 设定顶点属性指针 11 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); 12 glEnableVertexAttribArray(0); 13 // 4. 解绑VAO(不是EBO!) 14 glBindVertexArray(0); 15 16 [...] 17 18 // ..:: 绘制代码(循环中) :: .. 19 20 glUseProgram(shaderProgram); 21 glBindVertexArray(VAO); 22 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0) 23 glBindVertexArray(0); 24
1.2 顶点位置、颜色
1.3 顶点、片元着色器
1 #version version_number 2 3 in type in_variable_name; 4 in type in_variable_name; 5 6 out type out_variable_name; 7 8 uniform type uniform_name; 9 10 int main() 11 { 12 // 处理输入并进行一些图形操作 13 ... 14 // 输出处理过的结果到输出变量 15 out_variable_name = weird_stuff_we_processed; 16 }
OpenGL确保顶点着色器的输入变量(顶点属性)至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限:
1 GLint Attributes; 2 glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &Attributes); 3 std::cout << "Maximum nr of vertex attributes supported: " << Attributes << std::endl;
输入与输出
GLSL定义了in和out关键字专门来实现输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。
顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。
你也可以忽略layout (location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation 查询属性位置值(Location),但是我更喜欢在着色器中设置它们,这样会更容易理解而且节省你(和OpenGL)的工作量。
另一个例外是片段着色器,它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
顶点着色器
1 #version 330 core 2 layout(location = 0) in vec3 position; // position变量的属性位置值为0 3 4 out vec4 vertexColor; // 为片段着色器指定一个颜色输出 5 6 void main() 7 { 8 gl_Position = vec4(position, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数 9 vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出变量设置为暗红色 10 }
片段着色器
1 in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) 2 3 out vec4 color; // 片段着色器输出的变量名可以任意命名,类型必须是vec4 4 5 void main() 6 { 7 color = vertexColor; 8 }
Uniform
Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!
片段着色器
1 #version 330 core 2 shader: 3 out vec4 color; 4 uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量 5 void main() 6 { 7 color = ourColor; 8 }
应用程序查询uniform变量位置并赋值
1 GLfloat timeValue = glfwGetTime(); 2 GLfloat greenValue = (sin(timeValue) / 2) + 0.5; 3 GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); 4 glUseProgram(shaderProgram); 5 glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
为顶点添加更多属性
把颜色数据加进顶点数据中。可以把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色:
1 GLfloat vertices[] = { 2 // 位置 // 颜色 3 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下 4 -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下 5 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部 6 };
由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用layout标识符来把color属性的位置值设置为1:
1 #version 330 core 2 layout(location = 0) in vec3 position; // 位置变量的属性位置值为 0 3 layout(location = 1) in vec3 color; // 颜色变量的属性位置值为 1 4 5 out vec3 ourColor; // 向片段着色器输出一个颜色 6 7 void main() 8 { 9 gl_Position = vec4(position, 1.0); 10 ourColor = color; // 将ourColor设置为我们从顶点数据那里得到的输入颜色 11 }
更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:
ZC:2018_7_21--OpenGL学习笔记(二) Triangle_Shader - G_Wen的博客 - CSDN博客.html(https://blog.csdn.net/qq_35605411/article/details/81143060)
知道了现在使用的布局,我们就可以使用glVertexAttribPointer函数更新顶点格式,