文章目录
参考:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
一.BO(Buffer Object,缓冲对象)
缓冲对象BO(Buffer Object)是OpenGL管理的一段内存,为了与我们CPU的内存区分开,一般称OpenGL管理的内存为:显存。显存,也就是显卡里的内存。显卡访问显存比较快,而BO(Buffer Object),就是由OpenGL维护的一块显存区域。比如说在一块显存为2G的显卡里,分配了128K大小的内存区域给OpenGL使用,这个128K大小的内存区域,就叫一个BO(Buffer Object)。由于显卡访问显存,比访问内存(CPU里的内存区域)要快很多。而且显卡做运算,一般都是访问显存的数据,然后运算得到结果,并把结果也都保存在显存中。所以一般,需要先把数据,从内存传输到显存中去。
显卡里申请的这片显存区域,存放顶点数据,就叫VBO(Vertex Buffer Object),存放图像数据,就叫PBO(Picture Buffer Object),根据它存放的数据的不同,有不同的叫法。
二.VBO(Vertex Buffer Object,顶点缓冲对象)
OpenGL是一个3D图形库,所以在OpenGL中我们指定的所有坐标都是3D坐标(x、y和z),在开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。这些数据一开始存在C++语言创建的CPU的内存中,比如指定三个顶点的坐标,存在float数组内。
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
OpenGL仅当3D坐标在3个轴(x、y和z)上-1.0到1.0的范围内时才处理它。所有在这个范围内的坐标叫做标准化设备坐标(Normalized Device Coordinates),此范围内的坐标最终显示在屏幕上(在这个范围以外的坐标则不会显示)。而后需要把这个顶点数据作为输入发送给OpenGL的图形渲染管线的第一个阶段:顶点着色器。顶点着色器会在GPU上创建显存用于储存这些顶点数据,同时我们还需要告诉OpenGL如何解释这些显存(比如告诉OpenGL,顶点数据前三个是物体的三维坐标,后三个是顶点法线,再后两个是纹理坐标)。
顶点缓冲对象(Vertex Buffer Objects, VBO)的作用就是管理这个在GPU上创建的显存。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器访问顶点是个非常快的过程的。
(1) 顶点缓冲对象的生成
- 顶点缓冲对象就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用
glGenBuffers
函数和一个缓冲ID生成一个VBO对象:unsigned int VBO; glGenBuffers(1, &VBO);
- OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是
GL_ARRAY_BUFFER
。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用glBindBuffer
函数把新创建的缓冲绑定到GL_ARRAY_BUFFER
目标上:glBindBuffer(GL_ARRAY_BUFFER, VBO);
- 从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用
glBufferData
函数,它会把之前定义的顶点数据复制到缓冲的内存中:glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData
是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。- 它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到
GL_ARRAY_BUFFER
目标上。 - 第二个参数指定传输数据的大小(以字节为单位);用一个简单的
sizeof
计算出顶点数据大小就行。 - 第三个参数是我们希望发送的实际数据。
- 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
GL_STATIC_DRAW
:数据不会或几乎不会改变。
GL_DYNAMIC_DRAW
:数据会被改变很多。
GL_STREAM_DRAW
:数据每次绘制时都会改变。
三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW
。如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW
或GL_STREAM_DRAW
,这样就能确保显卡把数据放在能够高速写入的内存部分。现在我们已经把顶点数据储存在显卡的内存中,用VBO这个顶点缓冲对象管理。
- 它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到
(2) 顶点着色器的编译生成
顶点着色器(Vertex Shader)是几个可编程着色器中的一个。如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器。我们需要做的第一件事是用着色器语言GLSL(OpenGL Shading Language)
编写顶点着色器,然后编译这个着色器,这样我们就可以在程序中使用它了。
-
下面你会看到一个非常基础的GLSL顶点着色器的源代码:
#version 330 core layout (location = 0) in vec3 aPos; void main() { gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); }
可以看到,GLSL看起来很像C语言。每个着色器都起始于一个版本声明。OpenGL 3.3以及和更高版本中,GLSL版本号和OpenGL的版本是匹配的(比如说GLSL 420版本对应于OpenGL 4.2)。我们同样明确表示我们会使用核心模式。
下一步,使用in关键字,在顶点着色器中声明所有的输入顶点属性(
Input Vertex Attribute
)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。GLSL有一个向量数据类型,它包含1到4个float分量,包含的数量可以从它的后缀数字看出来。由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量aPos。我们同样也通过layout (location = 0)设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。 -
现在,我们暂时将顶点着色器的源代码硬编码在代码文件顶部的C风格字符串中:
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";
为了能够让OpenGL使用它,我们必须在运行时动态编译它的源代码。
-
我们首先要做的是创建一个着色器对象,注意还是用ID来引用的。所以我们储存这个顶点着色器为unsigned int,然后用
glCreateShader
创建这个着色器:unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER);
我们把需要创建的着色器类型以参数形式提供给
glCreateShader
。由于我们正在创建一个顶点着色器,传递的参数是GL_VERTEX_SHADER
。 -
下一步我们把这个着色器源码附加到着色器对象上,然后编译它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader);
glShaderSource
函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。
(3) 片段着色器的编译生成
片段着色器(Fragment Shader)是第二个也是最后一个我们打算创建的用于渲染三角形的着色器。片段着色器所做的是计算像素最后的颜色输出。为了让事情更简单,我们的片段着色器将会一直输出橘黄色。
-
以下为片段着色器的GLSL代码:
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }
-
现在,我们暂时将顶点着色器的源代码硬编码在代码文件顶部的C风格字符串中:
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" "}\n\0";
-
编译片段着色器的过程与顶点着色器类似,只不过我们使用
GL_FRAGMENT_SHADER
常量作为着色器类型:unsigned int fragmentShader; fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader);
现在,两个着色器现在都编译了,剩下的事情是把两个着色器对象链接到一个用来渲染的着色器程序(Shader Program)中。
(4) 着色器的生效
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
- 创建一个程序对象很简单:
unsigned int shaderProgram; shaderProgram = glCreateProgram();
glCreateProgram
函数创建一个程序,并返回新创建程序对象的ID引用。 - 现在我们需要把之前编译的着色器附加到程序对象上,然后用
glLinkProgram
链接它们:glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram);
- 得到的结果就是一个程序对象,我们可以调用
glUseProgram
函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:
在glUseProgram(shaderProgram);
glUseProgram
函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。 - 在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:
glDeleteShader(vertexShader); glDeleteShader(fragmentShader);