- 基本概念
顶点数组对象:Vertex Array Object,VAO
顶点缓冲对象:Vertex Buffer Object,VBO
索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO - 图形渲染管线(管线):3D空间坐标-->2D屏幕坐标处理,一堆原始图形数据途径一个输送管道,期间经过各种变化处理最终出现在屏幕的过程。图形渲染管线可以被划分为两个主要部分:1)把你的3D坐标转换为2D坐标;2)把2D坐标转变为实际有颜色的像素。每个阶段会把前一个阶段的输出作为输入,每个阶段运行各自的小程序,从而在图形渲染管线中快速处理数据,这些小程序叫做着色器,GPU上运行。
- 图形渲染管线阶段介绍,以制作三角形为例:
1)以数组形式传递3个3D坐标,用来表示一个三角形,这个数组叫做顶点数据;图元:指出的渲染类型
2)顶点着色器阶段:把一个单独的顶点作为输入,每个输入变量也叫顶点属性,目的:把3D坐标转为另一种3D坐标;顶点着色器允许我们对顶点属性进行一些基本处理
3)图元装配阶段:将顶点着色器输出的所有顶点作为输入,所有的点装配成指定图元的形状;例:三角形
4)几何着色器:图元装配阶段的输出会传递给几何着色器,把图元形式的一系列顶点几何作为输入,它可以通过产生新顶点构造出新的图元来生成其他形状。例:生成了另一个三角形
5)光栅化阶段:几何着色器的输出会被传入光栅化阶段,把图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段(OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据)。在片段着色器运行之前会执行裁剪:丢弃超出视图以外的所有像素,用来提升执行效率。
6)片段着色器:计算一个像素的最终颜色,所有OpenGL高级效果产生的部分。通常,片段着色器包含3D场景的数据(光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
7)Alpha测试和混合阶段:检测片段对应的深度值,用来判断这个像素是其他物体前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合。
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)
顶点输入
OpenGL仅当3D坐标在3个轴(x、y、z)上都为-1.0到1.0的范围内时才处理它。深度通常为Z坐标的输入,它代表一个像素空间中和你的距离。以三角形为例:三个float型顶点的3D位置数组如下:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
标准化设备坐标<(-1.0,1.0)区间>接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport函数提供的数据,进行视口变换(Viewport Transform)完成的。所得的屏幕空间坐标又会被变换为片段输入到片段着色器中。
定义这样的顶点数据以后,我们会把它作为输入发送给渲染管线的第一个处理阶段:顶点着色器。
它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且制定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。
我们通过顶点缓冲对象(VBO)管理这个内存,它会在GPU内存(显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次,从CPU把数据发送到显卡相对较慢,所以尽量一次性发送尽可能多的数据。
顶点缓冲对象有一个独一无二的ID,可以使用glGenBuffers函数和一个缓冲ID生成一个VBO对象。
unsigned int VBO;
glGenBuffers(1, &VBO);
OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同事绑定多个缓冲,只要他们是不同的缓冲类型。我们可以使用glbindbuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER上:
glBindBuffer(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 :数据每次绘制时都会改变。
顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
gl_Position设置的值会成为该顶点着色器的输出,必须是将三分量装换为四分量,w分量设置为1.0。
编译着色器
现在,我们暂时将顶点着色器的源代码硬编码在代码文件顶部的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);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
/* glShaderSource把要编译的着色器作为第一个参数,第二参数指定了传递的源码字符串数量,
这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。*/
glCompileShader(vertexShader);
片段着色器
计算像素最后的颜色输出,在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
编译片段着色器的过程与顶点着色器类似,只不过我们使用GL_FRAGMENT_SHADER常量作为着色器类型:
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
着色器程序
多个着色器合并之后并最终链接完成的版本,然后在渲染对象的时候激活这个着色器程序,已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
创建一个程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram();//创建一个程序,并返回新创建程序对象的ID引用。
//现在我们需要把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
得到的结果就是一个程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:
glUseProgram(shaderProgram);
在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。
//删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
链接顶点属性
我们必须在渲染前指定OpenGL该如何解释顶点数据。
说明:
1)位置数据被储存为32位(4字节)浮点值。
2)每个位置包含3个这样的值。
3)在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
4)数据中第一个值在缓冲开始的位置。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer函数的参数非常多,所以我会逐一介绍它们:
1)第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
2)第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
3)第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
4)下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
5)第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
6)最后一个参数的类型是void *,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。
在OpenGL中绘制一个物体,代码会像是这样:
// 0. 复制顶点数组到缓冲中供OpenGL使用
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();
顶点数组对象(VAO)
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。
一个顶点数组对象会储存以下这些内容:
1)glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
2)通过glVertexAttribPointer设置的顶点属性配置。
3)通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
创建一个VAO和创建一个VBO很类似:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑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();
索引缓冲对象EBO:专门存储索引
索引绘制:先要定义(不重复的)顶点,和绘制出矩形所需的索引。例:
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 // 第二个三角形
}
最后的初始化和绘制代码现在看起来像这样:
// ..:: 初始化代码 :: ..
// 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);