第四章 高级OpenGL
4.6 数据与缓冲
在OpenGL中使用缓冲来储存数据,本节将讨论不同的缓冲函数,以及如何使用纹理对象来储存大量的数据。缓冲是用来管理特定内存块的对象,将它绑定到一个缓冲目标后,才赋予了其意义。例如,将缓冲绑定到GL_ARRAY_BUFFER
或GL_ELEMENT_ARRAY_BUFFER
等缓冲目标上,OpenGL根据目标的不同,以不同的方式处理缓冲。
4.6.1 缓冲函数
1、之前一直调用glBufferData
函数来填充缓冲对象所管理的内存,这个函数会分配一块内存,并将数据添加到这块内存中。如果data
参数设置为NULL
,将只会分配内存,但不进行填充。
glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), cubeVertices, GL_STATIC_DRAW);
2、可使用glBufferSubData
函数填充缓冲的特定区域。参数为:缓冲目标、偏移量、数据的大小和数据本身。偏移量指定从何处开始填充这个缓冲,因此可以插入或者更新缓冲内存的某一部分,但缓冲需要有足够的已分配内存,所以对一个缓冲调用glBufferSubData
之前必须要先调用glBufferData
。
glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), &data); // 范围: [24, 24 + sizeof(data)]
3、将数据导入缓冲的另外一种方法是:通过调用glMapBuffer
函数,返回当前绑定缓冲的内存指针,直接将数据复制到缓冲当中。该函数用于直接映射数据到缓冲,而不事先将其存储到临时内存中,比如从文件中读取数据,并直接将它们复制到缓冲内存中。
float data[] = {
0.5f, 1.0f, -0.35f
...
};
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// 获取指针
void *ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
// 复制数据到内存
memcpy(ptr, data, sizeof(data));
//不再需要这个指针了
glUnmapBuffer(GL_ARRAY_BUFFER);
4.6.2 分批顶点属性
1、之前,通过glVertexAttribPointer
函数指定顶点数组缓冲内容的属性布局。在顶点数组缓冲中,对属性进行了交错处理,将每一个顶点的位置、法线和纹理坐标紧密放置在一起。
2、另一种方式是:将每一种属性类型的向量数据打包为一个大的区块。与交错布局123123123123
不同,采用分批的方式111122223333
。
3、从文件中加载顶点数据时,通常获取到的是位置数组、法线数组和纹理坐标数组。使用glBufferSubData
函数进行缓冲填充:
float positions[] = { ... };
float normals[] = { ... };
float tex[] = { ... };
// 填充缓冲
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals), sizeof(tex), &tex);
更新顶点属性指针,stride参数等于顶点属性的大小
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(sizeof(positions)));
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)(sizeof(positions) + sizeof(normals)));
4.6.3 复制缓冲
1、glCopyBufferSubData
能够从一个缓冲中复制数据到另一个缓冲中,readtarget
和writetarget
参数需为复制源和复制目标的缓冲目标。如将VERTEX_ARRAY_BUFFER
缓冲复制到VERTEX_ELEMENT_ARRAY_BUFFER
缓冲,分别将这些缓冲目标设置为读和写的目标。
void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset,
GLintptr writeoffset, GLsizeiptr size);
2、如果想读写数据的两个不同缓冲都为顶点数组缓冲,就不能同时将两个缓冲绑定到同一个缓冲目标上。因此,使用另外两个缓冲目标,GL_COPY_READ_BUFFER
和GL_COPY_WRITE_BUFFER
,可以将需要的缓冲绑定到这两个缓冲目标上,作为readtarget
和writetarget
参数。glCopyBufferSubData
会从readtarget
中读取size
大小的数据,并将其写入writetarget
缓冲的writeoffset
偏移量处。
float vertexData[] = { ... };
glBindBuffer(GL_COPY_READ_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));
float vertexData[] = { ... };
glBindBuffer(GL_ARRAY_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));
4.7 高级GLSL
4.7.1 顶点着色器变量
1、GLSL
定义了以gl_
为前缀的内建变量,如:顶点着色器的输出向量gl_Position
,它是顶点着色器的裁剪空间输出位置向量。
2、将图元设置为GL_POINTS
时,每一个顶点都会被渲染为一个点,可以通过glPointSize
函数来设置渲染出来的点的大小。也可以通过gl_PointSize
设置点的宽高(像素),能对每个顶点设置不同的值。
- 启用顶点着色器中修改点大小的功能:
glEnable(GL_PROGRAM_POINT_SIZE);
- 将点的大小设置为裁剪空间位置的z值,也就是顶点距观察者的距离。点的大小会随着观察者距顶点距离变远而增大。
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
gl_PointSize = gl_Position.z;
}
3、gl_VertexID
为只读的输入变量,储存了正在绘制顶点的当前ID。当进行索引渲染的时候,这个变量会存储正在绘制顶点的当前索引。当不使用索引进行绘制的时候,这个变量会储存从渲染调用开始的已处理顶点数量。
4.7.2 片段着色器变量
1、gl_FragCoord
的x和y分量是片段的窗口空间坐标,其原点为窗口的左下角,z分量等于对应片段的深度值。可以根据片段的窗口坐标,计算出不同的颜色。gl_FragCoord
的常用于对比不同片段计算的视觉输出效果,如将屏幕分成两部分,在窗口的左侧渲染一种输出,在窗口的右侧渲染另一种输出。
void main()
{
if(gl_FragCoord.x < 400)
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
else
FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
2、gl_FrontFacing
是一个bool变量,如果当前片段是正向面就返回true
,否则就是false
。如果不使用面剔除,能够对正反面使用不同的颜色(纹理)。
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D frontTexture;
uniform sampler2D backTexture;
void main()
{
if(gl_FrontFacing)
FragColor = texture(frontTexture, TexCoords);
else
FragColor = texture(backTexture, TexCoords);
}
3、gl_FragCoord
能获取片段的深度值,但不能修改。而输出变量gl_FragDepth
可用来设置片段的深度值,直接写入一个0.0到1.0之间的float值。如果着色器没有写入值到gl_FragDepth
,它会自动取用gl_FragCoord.z
的值。
-
在片段着色器中对
gl_FragDepth
进行写入,OpenGL就会禁用所有的提前深度测试,因为OpenGL无法在片段着色器运行之前确定片段的深度值。 -
可以在片段着色器的顶部使用深度条件重新声明
gl_FragDepth
变量,可以对两者进行一定的调和。如将深度条件设置为greater
,假设只会写入比当前片段深度值更大的值,当深度值比片段的深度值要小的时候,仍是能够进行提前深度测试的。layout (depth_<condition>) out float gl_FragDepth;
condition | 描述 |
---|---|
any | 提前深度测试是禁用的,你会损失很多性能 |
greater | 只能让深度值比gl_FragCoord.z更大 |
less | 只能让深度值比gl_FragCoord.z更小 |
unchanged | 只能写入gl_FragCoord.z的值 |
#version 420 core // 注意GLSL的版本!
out vec4 FragColor;
layout (depth_greater) out float gl_FragDepth;
void main()
{
FragColor = vec4(1.0);
gl_FragDepth = gl_FragCoord.z + 0.1;
}
4.7.3 接口块
1、当需要从顶点着色器向片段着色器发送数组或结构体时,可以通过GLSL里的接口块组合这些变量。接口块的声明和结构体的声明类似,使用in或out关键字来定义输入输出。
2、声明了一个块名为VS_OUT
,实例名为vs_out
的接口块,将着色器的输入打包为数组。
#version 330 core
layout(location=0) in vec3 aPos;
layout(location=1) in vec2 aTexCooords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out VS_OUT
{
vec2 TexCoords;
}vs_out;
void main()
{
gl_postion=projection*view*model*vec4(aPos,1.0);
vs_out.TexCoords=aTexCoords;
}
3、在片段着色器中定义一个输入接口块,块名和顶点着色器中一样(VS_OUT
),实例名是随意的。
#version 330 core
out vec4 FragColor;
in VS_OUT
{
vec2 TexCoords;
} fs_in;
uniform sampler2D texture;
void main()
{
FragColor = texture(texture, fs_in.TexCoords);
}
4.7.4 Uniform缓冲对象
1、Uniform
缓冲对象是可以在多个着色器中使用的全局Uniform
变量,使用glGenBuffers
来创建,绑定到GL_UNIFORM_BUFFER
缓冲目标,并将所有相关的Uniform
数据存入缓冲。Uniform
缓冲对象只需创建一次,但每个着色器中不同的uniform
需要手动设置。
2、在每个渲染迭代中,对每个着色器设置projection
和view
矩阵,现在只需要存储这些矩阵一次。首先,声明了一个叫做Matrices
的Uniform
块,将projection
和view
矩阵存储到Uniform
块中,块中的变量可以直接访问。然后,在OpenGL中将这些矩阵值存入缓冲中,每个声明了这个Uniform
块的着色器都能够访问这些矩阵。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
uniform mat4 model;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
4、layout (std140)
设置了Uniform
块布局,对它的内容使用一个特定的内存布局。Uniform
块的内容是储存在一个缓冲对象中的,缓冲对象只是一块预留内存,并不会保存数据的类型,因此需要确定内存的哪一部分对应着着色器中的哪一个uniform变量。
layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
mat4 matrix;
float values[3];
bool boolean;
int integer;
};
5、需要确定每个变量的大小和偏移量,才能够按顺序将它们放进缓冲中。变量大小对应C++数据类型,其中向量和矩阵都是大的float数组。变量偏移量可以通过std140
布局确定,std140
布局声明了偏移量的计算规则。
- 每个变量的基准对齐量等于变量在Uniform块中所占据的空间(包括填充量),可以使用
std140
布局的规则计算; - 每个变量的对齐偏移量等于变量从块起始位置的字节偏移量。对齐字节偏移量必须等于基准对齐量的倍数。
- GLSL中的每个变量,比如说int、float和bool,都被定义为4字节量。每4个字节将会用一个N来表示。
类型 | 布局规则 |
---|---|
标量,比如int和bool | 每个标量的基准对齐量为N。 |
向量 | 2N或者4N这意味着vec3的基准对齐量为4N。 |
标量或向量的数组 | 每个元素的基准对齐量与vec4的相同。 |
矩阵 | 储存为列向量的数组,每个向量的基准对齐量与vec4的相同。 |
结构体 | 等于所有元素根据规则计算后的大小,但会填充到vec4大小的倍数。 |
layout (std140) uniform ExampleBlock
{
// 基准对齐量 // 对齐偏移量
float value; // 4 // 0
vec3 vector; // 16 // 16 (必须是16的倍数,所以 4->16)
mat4 matrix; // 16 // 32 (列 0)
// 16 // 48 (列 1)
// 16 // 64 (列 2)
// 16 // 80 (列 3)
float values[3]; // 16 // 96 (values[0])
// 16 // 112 (values[1])
// 16 // 128 (values[2])
bool boolean; // 4 // 144
int integer; // 4 // 148
};
6、根据std140布局的规则,就能使用glBufferSubData
的函数将变量数据按照偏移量填充进缓冲中了。虽然std140布局不是最高效的布局,但它保证了内存布局在每个声明了这个Uniform块的程序中是一致的。
4.7.5 使用Uniform缓冲
1、首先,调用glGenBuffers
创建一个Uniform
缓冲对象,将它绑定到GL_UNIFORM_BUFFER
目标,并调用glBufferData
,分配足够的内存。
unsigned int uboExampleBlock;
glGenBuffers(1,&uboExampleBlock);
glBindBuffer(GL_UNIFORM_BUFFER,uboExampleBlock);
glBufferData(GL_UNIFORM_BUFFER,152,NULL,GL_STATIC_DRAW);//分配152字节的内存
glBindBuffer(GL_UNIFORM_BUFFER,0);
2、当需要对缓冲更新或者插入数据,都会绑定到uboExampleBlock
,并使用glBufferSubData来更新它的内存。只需要更新这个Uniform
缓冲一次,所有使用这个缓冲的着色器就都使用的是更新后的数据了。
3、在OpenGL上下文中,定义了一些绑定点,将一个Uniform
缓冲链接至它,并将着色器中的Uniform
块绑定到相同的绑定点,就可以将Uniform
缓冲对象和Uniform块对应起来。
4、可以绑定多个Uniform
缓冲到不同的绑定点上。因为着色器A和着色器B都有一个链接到绑定点0的Uniform
块,它们的Uniform
块将会共享相同的uniform
数据。
-
调用
glUniformBlockBinding
函数将Uniform块绑定到一个特定的绑定点中,参数是着色器ID、Uniform块索引和绑定点。Uniform块索引是着色器中Uniform块的位置值索引,通过调用glGetUniformBlockIndex
来获取,参数是着色器ID和Uniform块的名称。unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights"); glUniformBlockBinding(shaderA.ID, lights_index, 2);
-
使用
glBindBufferBase
或glBindBufferRange
将Uniform
缓冲对象绑定到相同的绑定点上。glBindbufferBase
需要目标,绑定点索引和Uniform缓冲对象作为它的参数。glBindBufferRange
函数需要附加偏移量和大小参数,可以绑定Uniform缓冲的特定一部分到绑定点中。使用glBindBufferRange
函数,可以让多个不同的Uniform块绑定到同一个Uniform缓冲对象上。glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); // 或 glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
5、使用glBufferSubData
函数,用一个字节数组添加所有的数据,或者更新缓冲的一部分。要想更新uniform变量boolean,可以用以下方式更新Uniform缓冲对象:
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // GLSL中的bool是4字节的,所以我们将它存为一个integer
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
4.7.6 举例
1、在顶点着色器中使用了3个矩阵:投影、观察和模型矩阵,而只有模型矩阵会频繁变动。如果有多个着色器使用了这同一组矩阵,那么使用Uniform缓冲对象会更好,因此将投影和模型矩阵存储到一个叫做Matrices
的Uniform
块中。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
uniform mat4 model;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
2、在例子程序中显示4个立方体,每个立方体使用不同的着色器程序渲染的。将顶点着色器的Uniform块设置为绑定点0。
unsigned int uniformBlockIndexRed = glGetUniformBlockIndex(shaderRed.ID, "Matrices");
unsigned int uniformBlockIndexGreen = glGetUniformBlockIndex(shaderGreen.ID, "Matrices");
unsigned int uniformBlockIndexBlue = glGetUniformBlockIndex(shaderBlue.ID, "Matrices");
unsigned int uniformBlockIndexYellow = glGetUniformBlockIndex(shaderYellow.ID, "Matrices");
glUniformBlockBinding(shaderRed.ID, uniformBlockIndexRed, 0);
glUniformBlockBinding(shaderGreen.ID, uniformBlockIndexGreen, 0);
glUniformBlockBinding(shaderBlue.ID, uniformBlockIndexBlue, 0);
glUniformBlockBinding(shaderYellow.ID, uniformBlockIndexYellow, 0);
3、创建Uniform缓冲对象本身,为缓冲分配了足够的内存,它等于glm::mat4
大小的两倍。并将缓冲中的特定范围(在这里是整个缓冲)链接到绑定点0。
unsigned int uboMatrices
glGenBuffers(1, &uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));
4、填充缓冲。将投影矩阵的视野值保持不变(所以摄像机就没有缩放了),只需要将其在程序中定义一次,使用glBufferSubData
在进入渲染循环之前存储投影矩阵:
glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
5、在每次渲染迭代中绘制物体之前,将观察矩阵更新到缓冲的后半部分:
glm::mat4 view = camera.GetViewMatrix();
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
6、因为修改了模型矩阵,每个立方体都移动到了窗口的一边,并且由于使用了不同的片段着色器,它们的颜色也不同。
glBindVertexArray(cubeVAO);
shaderRed.use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f)); // 移动到左上角
shaderRed.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
// ... 绘制绿色立方体
// ... 绘制蓝色立方体
// ... 绘制黄色立方体
7、Uniform缓冲对象比起独立的uniform有很多好处。第一,一次设置很多uniform会比一个一个设置多个uniform要快很多。第二,比起在多个着色器中修改同样的uniform,在Uniform缓冲中修改一次会更容易一些。第三,如果使用Uniform缓冲对象的话,你可以在着色器中使用更多的uniform。