让我们继续深入GLSL的各个性质,了解更多功能:
Advanced Data
有关缓冲:
在之前的学习中,我们申请缓冲的函数总是glBufferData。

这个函数固然很好,但是不难发现他针对缓冲区要么是重新创建,要么是重置,我们希望更灵活地去处理缓冲。

简而言之,我们使用glBufferSubData就可以在已有glBufferData的基础上进行更新。
当然了,既然语言是C++,我们当然不能忽略用指针来做,我们可以直接操作指针指向缓冲区来讲数据写入缓冲,这个做法GLSL已经帮我们写成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));
// 记得告诉OpenGL我们不再需要这个指针了
glUnmapBuffer(GL_ARRAY_BUFFER);
可以看到我们利用glMapBuffer就可以直接获取缓冲区的指针,然后memcpy函数就可以。
有关分批处理顶点数据:
默认的顶点数组数据,我们之前默认是交错进行处理,也就是比如前3个数据,第一个代表位置,第二个代表颜色,第三个代表纹理坐标,然后我们的第四个数据就会是第二个顶点的位置,第五个数据就是第二个顶点的颜色...现在让我们换种方式来做,我们将所有的顶点位置数据放置在整个数组的前方,颜色数据放置在整个数组的中部,纹理坐标放置在整个数组的尾部。
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);
当然,这样的话我们也要重新更新顶点获取数据的方式:
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)));
关于交错设置顶点数据和分批设置顶点数据,各自有各自的好处:交错来说内存格式更规整,而分批来说更方便。


有关复制缓冲:
当我们的一个缓冲已经获取到数据之后,我们可能需要将这个缓冲中的内容共享给其他缓冲。
void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset,
GLintptr writeoffset, GLsizeiptr size);
实现这个功能的关键是glCopyBufferSubData,我们可以利用偏移量来讲一个缓冲区中的特定内存复制到另一个缓冲区。
Advanced GLSL
我们现在学会了更灵活的缓冲区处理方式,让我们更近一步:
什么是内建变量?
所谓的内建变量就是GLSL中帮你写好的可以获取外部数据的变量,注意这里的外部数据指的是我们的CPU到GPU绘制中间过程中可能用到的数据,比如我们的顶点坐标,uniform变量,采样器等。我们之前已经学习过gl_position和gl_fragcolor了,现在我们来学点新的内建变量:





我们这里列举了五个新的内建变量:gl_pointsize、gl_vertexID、gl_fragcoord、gl_frontfacing、gl_fragdepth。gl_pointsize用于允许我们直接修改顶点的大小,gl_vertexID则是可以返回一个顶点的索引,gl_fragcoord则是针对我们的窗口坐标进行颜色的更改,gl_frontfacing在我们不开启面剔除时可以返回一个bool变量代表我们看向的面是朝外的面还是朝内的面,gl_fragdepth则是允许我们直接去修改片段的深度值。
有关接口块:
到目前为止,每当我们希望从顶点着色器向片段着色器发送数据时,我们都声明了几个对应的输入/输出变量。将它们一个一个声明是着色器间发送数据最简单的方式了,但当程序变得更大时,你希望发送的可能就不只是几个变量了,它还可能包括数组和结构体。
为了帮助我们管理这些变量,GLSL为我们提供了一个叫做接口块(Interface Block)的东西,来方便我们组合这些变量。接口块的声明和struct的声明有点相像,不同的是,现在根据它是一个输入还是输出块(Block),使用in或out关键字来定义的。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out VS_OUT
{
vec2 TexCoords;
} vs_out;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
vs_out.TexCoords = aTexCoords;
}
以上代码是我们一个接口块的定义方式,可以看到我们用in或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);
}
有关Uniform缓冲对象:
我们已经使用OpenGL很长时间了,学会了一些很酷的技巧,但也遇到了一些很麻烦的地方。比如说,当使用多于一个的着色器时,尽管大部分的uniform变量都是相同的,我们还是需要不断地设置它们,所以为什么要这么麻烦地重复设置它们呢?
OpenGL为我们提供了一个叫做Uniform缓冲对象(Uniform Buffer Object)的工具,它允许我们定义一系列在多个着色器程序中相同的全局Uniform变量。当使用Uniform缓冲对象的时候,我们只需要设置相关的uniform一次。当然,我们仍需要手动设置每个着色器中不同的uniform。并且创建和配置Uniform缓冲对象会有一点繁琐。因为Uniform缓冲对象仍是一个缓冲,我们可以使glGenBuffers来创建它,将它绑定到GL_UNIFORM_BUFFER缓冲目标,并将所有相关的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);
}
这是一个全局的uniform变量的示例,我们在这个顶点着色器中p矩阵和v矩阵放入了一个叫做Matrices的Uniform块,它储存了两个4x4矩阵。Uniform块中的变量可以直接访问,不需要加块名作为前缀。接下来,我们在OpenGL代码中将这些矩阵值存入缓冲中,每个声明了这个Uniform块的着色器都能够访问这些矩阵。你现在可能会在想layout (std140)这个语句是什么意思。它的意思是说,当前定义的Uniform块对它的内容使用一个特定的内存布局。这个语句设置了Uniform块布局(Uniform Block Layout)。

具体的内存对齐规则我就不展开了,因为我放在这过几天我也就都忘了,反正我们记住当我们在着色器中定义了Uniform块的时候我们就需要在前面添加layout(std140)即可,而这个std140本质上来说一种内存对齐的规则。
我们在顶点定义好我们的uniform块之后,我们要像使用之前的缓冲对象一样去使用这个uniform缓冲对象:
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);
我们现在有uniform缓冲对象也有uniform块了,问题是如何建立这个连接关系。

unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");
glUniformBlockBinding(shaderA.ID, lights_index, 2);
这个函数的原型是这样的:
void glUniformBlockBinding(
GLuint program, // 着色器程序对象
GLuint uniformBlockIndex, // Uniform 块的索引
GLuint uniformBlockBinding // 要绑定的全局绑定点索引
);
注意这里我们是需要去指明具体的着色器对象的,因为不同的着色器中可能有相同索引的uniform块。

简单地说,我们先在着色器内部定义好一个uniform块,然后在程序中定义好uniform缓冲对象,我们将这两个东西通过绑定点来建立连接。
这里让我们介绍一下glBindBufferRange函数:
void glBindBufferRange(
GLenum target, // 缓冲目标类型
GLuint index, // 全局绑定点索引
GLuint buffer, // 缓冲对象 ID
GLintptr offset, // 绑定起始偏移量(字节)
GLsizeiptr size // 绑定范围大小(字节)
);
以下是一个具体的实例:
// configure a uniform buffer object
// ---------------------------------
// first. We get the relevant block indices
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");
// then we link each shader's uniform block to this uniform binding point
glUniformBlockBinding(shaderRed.ID, uniformBlockIndexRed, 0);
glUniformBlockBinding(shaderGreen.ID, uniformBlockIndexGreen, 0);
glUniformBlockBinding(shaderBlue.ID, uniformBlockIndexBlue, 0);
glUniformBlockBinding(shaderYellow.ID, uniformBlockIndexYellow, 0);
// Now actually create the buffer
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);
// define the range of the buffer that links to a uniform binding point
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));
我们首先去获取各个着色器的ID中的名为Matrices的uniform块的index,然后我们利用glUniformBlockBinding函数来将各个着色器中的指定索引的Uniform块与绑定点绑定;后续我们生成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);
}
片元着色器则是简单的设置片元颜色为红、绿、蓝、黄。
效果如图:

Geometry Shader
还记得渲染流程中我们提到过的几何着色器吗?我们说在核心模式下我们可以对顶点着色器、几何着色器和片元着色器进行修改,但是一般情况下我们都只是去修改顶点着色器和片元着色器而忽略几何着色器的操作。
在顶点和片段着色器之间有一个可选的几何着色器(Geometry Shader),几何着色器的输入是一个图元(如点或三角形)的一组顶点。几何着色器可以在顶点发送到下一着色器阶段之前对它们随意变换。然而,几何着色器最有趣的地方在于,它能够将(这一组)顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。
#version 330 core
layout (points) in;
layout (line_strip, max_vertices = 2) out;
void main() {
gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
EmitVertex();
EndPrimitive();
}
这是一个几何着色器的具体定义,可以看到与我们的顶点着色器相比还是相当不同的。

最开始的两个布局修饰符修饰了输入和输出的类型,可以看到输入要求是一堆点而输出则是一条最多包含两个点的线段,在main函数中我们使用了两个内建变量:gl_position和gl_in。

还有就是我们在这里看到了两个新的函数:EmitVertex()和EndPrimitive()。


空说无益,我们直接上实例,分析代码。
我们首先得去修改一下着色器的定义,因为之前只有顶点着色器和片元着色器,而现在还多了一个片元着色器。
Shader(const char* vertexPath, const char* fragmentPath, const char* geometryPath = nullptr)
{
// 1. retrieve the vertex/fragment source code from filePath
std::string vertexCode;
std::string fragmentCode;
std::string geometryCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
std::ifstream gShaderFile;
// ensure ifstream objects can throw exceptions:
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
gShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
// open files
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// read file's buffer contents into streams
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// close file handlers
vShaderFile.close();
fShaderFile.close();
// convert stream into string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
// if geometry shader path is present, also load a geometry shader
if (geometryPath != nullptr)
{
gShaderFile.open(geometryPath);
std::stringstream gShaderStream;
gShaderStream << gShaderFile.rdbuf();
gShaderFile.close();
geometryCode = gShaderStream.str();
}
}
catch (std::ifstream::failure& e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;
}
可以看到我们多出了一整个着色器的流程,如果几何着色器的路径不为空,我们打开路径并获取相关代码。
if (geometryPath != nullptr)
{
const char* gShaderCode = geometryCode.c_str();
geometry = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometry, 1, &gShaderCode, NULL);
glCompileShader(geometry);
checkCompileErrors(geometry, "GEOMETRY");
}
没有什么不同,生成资源,编译,检查。
if (geometryPath != nullptr)
glAttachShader(ID, geometry);
如果有几何着色器就一起link到主程序中。
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;
in VS_OUT {
vec3 color;
} gs_in[];
out vec3 fColor;
void build_house(vec4 position)
{
fColor = gs_in[0].color; // gs_in[0] since there's only one input vertex
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1:bottom-left
EmitVertex();
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0); // 2:bottom-right
EmitVertex();
gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3:top-left
EmitVertex();
gl_Position = position + vec4( 0.2, 0.2, 0.0, 0.0); // 4:top-right
EmitVertex();
gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0); // 5:top
fColor = vec3(1.0, 1.0, 1.0);
EmitVertex();
EndPrimitive();
}
void main() {
build_house(gl_in[0].gl_Position);
}
这是我们的几何着色器的内容,结果如图。

在课程中还多实现了两种效果:爆破效果与法向量可视化。
爆破:
我们现在不妨来想象一下具体的爆破效果是如何实现的,其实非常简单:我们只需要让我们的片元(每一个三角形)沿着其法线方向移动一小段距离即可。
让我们从几何着色器的定义中来看如何实现:
#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
in VS_OUT {
vec2 texCoords;
} gs_in[];
out vec2 TexCoords;
uniform float time;
vec4 explode(vec4 position, vec3 normal)
{
float magnitude = 2.0;
vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude;
return position + vec4(direction, 0.0);
}
vec3 GetNormal()
{
vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
return normalize(cross(a, b));
}
void main() {
vec3 normal = GetNormal();
gl_Position = explode(gl_in[0].gl_Position, normal);
TexCoords = gs_in[0].texCoords;
EmitVertex();
gl_Position = explode(gl_in[1].gl_Position, normal);
TexCoords = gs_in[1].texCoords;
EmitVertex();
gl_Position = explode(gl_in[2].gl_Position, normal);
TexCoords = gs_in[2].texCoords;
EmitVertex();
EndPrimitive();
}
可以看到我们利用了三角形片元内两个向量叉乘得到了片元法向量,然后我们还写了一个爆炸函数:修改片元的位置,让片元沿着法线向量随着时间移动。效果如下:

我们的法向量可视化则常常用于如生成毛发等功能。
#version 330 core
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;
in VS_OUT {
vec3 normal;
} gs_in[];
const float MAGNITUDE = 0.2;
uniform mat4 projection;
void GenerateLine(int index)
{
gl_Position = projection * gl_in[index].gl_Position;
EmitVertex();
gl_Position = projection * (gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0) * MAGNITUDE);
EmitVertex();
EndPrimitive();
}
void main()
{
GenerateLine(0); // first vertex normal
GenerateLine(1); // second vertex normal
GenerateLine(2); // third vertex normal
}
效果如下:


1816

被折叠的 条评论
为什么被折叠?



