高级数据
我们之前定义顶点属性时都是一次性写入顶点数据,其实,我们还可以使用先分配内存再填数据的方式来写入顶点数据。
glBufferData(GL_ARRAY_BUFFER, size, NULL, GL_STATIC_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(data), &data);
但是在使用glBufferSubData写入数据之前,仍然需要先使用glBufferData,但是参数中数据指针为空,这样就可以只分配内存空间而不填入数据。
另外我们还可以使用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);
分批顶点属性
我们之前是将顶点坐标、颜色、纹理坐标看成一个顶点数据,所有顶点数据依次排列得到所有数据。这样相邻顶点间的相同类型数据的间隔是相同的。我们还可以使用另外一种方式设置数据,将所有的顶点坐标放在一起,所有的颜色放在一起,也即将所有相同类型的数据放在一起。这时我们就需要用到glBufferSubData的偏移了,同理在定义顶点属性时,stride和最后一个偏移参数也会有所不同。
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)));
复制缓冲
我们可以使用glCopyBufferSubData复制任意两个缓冲的数据,前提是我们需要填入readtarget和writetarget,如果两者类型不一样,我们可以轻易知道怎么复制,但是如果两者类型一样呢,这是会因为找不到具体要从哪个缓冲对象复制到哪个缓冲对象而产生矛盾。
void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset, GLintptr writeoffset, GLsizeiptr size);
我们可以分别将两个缓冲对象绑定到GL_COPY_READ_BUFFER和GL_COPY_WRITE_BUFFER类型上,这时复制时就能够明确是要操作哪两个对象了。同样,只要绑定了GL_COPY_READ_BUFFER和GL_COPY_WRITE_BUFFER,我们可以任意使用glCopyBufferSubData,只要保证readtarget和writetarget不同时为GL_COPY_READ_BUFFER或GL_COPY_WRITE_BUFFER即可。
高级GLSL
GLSL内建变量
GLSL里有很多内建变量,我们可以直接使用它们。我们之前已经用到了gl_Position,他是顶点着色器的输出,表示经过顶点着色器的顶点位置。
顶点着色器变量
- gl_Position:vec4,点的位置
- gl_PointSize:float,点的大小,当我们绘制的图元是GL_POINTS时,我们改变这个值才有效。默认不允许改变,使用glEnable(GL_PROGRAM_POINT_SIZE);//允许顶点着色器修改。
- gl_VertexID:int,当前点的索引,输入变量,只有当使用glDrawElements绘制时才有效。使用glDrawArrays绘制时存储的值为已处理顶点数量。
片段着色器变量
- gl_FragCoord:vec3,表示片元在屏幕空间的坐标,包含深度。
- gl_FrontFacing:bool,当前片元是否正面。可以输入正反面纹理,通过这个变量显示正反面不同纹理。
- gl_FragDepth:float,当前片元深度值,可写;gl_FragCoord中的深度值只读。如果在片段着色器中对该变量进行写入,那么提前深度测试(在片元着色器之前执行)将不可用,因为我们不能确定片元着色器会写入什么值。但有一种方法可以确定深度值的范围。
layout (depth_<condition>) out float gl_FragDepth;
我们可以通过限定gl_FragDepth可能变化的趋势,从而保证提前深度测试可用。例如,如果我们的深度函数是Less,那么当我们的限定条件是greater时,那么就说明,只要提前深度测试通过了,在限定条件下,不管深度值怎么变,都是不影响深度测试结果的,那么提前深度测试是可用的。我们可以使用的条件有any、greater、less、unchanged。但这个特性只有OpenGL4.2以上才支持。
#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; }
接口块
我们经常会遇到在顶点着色器和片元着色器间传数据时,需要定义很多in、out变量,当我们需要发送结构更加复杂的数组或者结构体时,代码就变得复杂起来。所以,OpenGL提供了接口块的方式来链接两个着色器间的变量。
#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;
}
#version 330 core
out vec4 FragColor;
in VS_OUT
{
vec2 TexCoords;
} fs_in;
uniform sampler2D texture;
void main()
{
FragColor = texture(texture, fs_in.TexCoords);
}
在上面的例程中定义的接口块名称为VS_OUT,区别在于是in还是out。在顶点着色器中有个别名vs_out(暂且叫别名或者实例名),片元着色器中有个别名fs_in,他们通过VS_OUT这个类型名连接起来,有了别名,我们在不同的着色器里使用时也避免了歧义。
Uniform缓冲对象
我们也可以为uniform创建缓冲对象,这样,当我们需要往不同的shader内注入大量相同的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块布局方式
前面例程定义shader内的uniform变量,但是程序如何将该变量传递进去呢?首先,我们需要考虑Matrices内的多个变量在内存中的布局方式。需要注意的是,GLSL的变量bool、float、int都是4字节。例程中的布局方式是std140,这是一种确定变量大小的布局方式,和文件的对齐方式类似。这种布局方式就为确定每个变量的位置提供了条件,我们可以手动计算出每个变量的偏移。
每个变量都有一个基准对齐量(Base Alignment),他是指这个变量所占的内存空间(和实际大小不一样,例如bool只有一位但是占用了4字节)。我们将每个变量按照定义的顺序排列好,每个变量占用空间就是基准对齐量的最小整数倍,那么就可以算出每个变量的偏移了。我们使用N来表示4个字节,也即一个简单类型所占的空间大小。更多规则。
类型 | 布局规则 |
---|---|
标量,比如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
};
这里需要注意的是,虽然vec3只用到了3 * 4字节,但是所有向量的基准对齐量都是2N或者4N,所以vec3只能为4N。
上面例程的vecs对齐偏移量为什么是16 ,因为向量的偏移必须是4N的倍数?
另外我们还可以使用一个叫做共享(Shared)布局的Uniform内存布局方式。他的思想是动态调整每个变量,这样的话就可以减少内存浪费。但是这样我们就不能知道每个变量在哪里,所以每次使用前都必须通过glGetUniformIndices这样的函数来查询变量的位置。
使用Uniform缓冲
上面已经介绍完了Uniform缓冲在shader内部的定义以及数据的内存布局,接下来就该讲如何通过程序,将每个变量根据之前定义的布局放置到正确的位置。下面的代码创建一个Uniform缓冲,并分配好内存,但是未写入数据,写输入需要使用glBufferSubData。
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);
到这里,我们还是不能将缓冲对象与shader里的uniform块绑定起来,所以下面需要使用OpenGL的绑定点(Binding Point),我们可以把它理解成一个纽带,一头绑Shade的Uniform块,一头绑Uniform缓冲对象,他们是多对多的关系。
ShaderA 和ShaderB的Matrices,他们都需要uboMatrices的数据。下面的例程定义的是将ShadeA的Lights Uniform块绑定到绑定点,然后将uboLights Unform缓冲对象绑定到绑定点,这样就实现了shader和UBO的数据互通。使用glBindBufferRange接口还可以实现只绑定Ubo的部分数据,这样一个ubo里就可以包含多个Uniform块的数据,需要用时分别取用即可。
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");
glUniformBlockBinding(shaderA.ID, lights_index, 2);
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock);
// 或
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
从OpenGL4.2开始,我们还可以使用下面的方式手动指定绑定点,这样就不需要再代码里绑定了。
layout(std140, binding = 2) uniform Lights { ... };
别忘了,我们还可以往UBO中添加数据,只要你想,你可以在任何时候使用glBufferSubData往缓冲里添加数据,但是别忘了加偏移。注意,使用int来存需要放入shader的bool数据,例如:int b = true;
使用Uniform还可以减小OpenGL对Uniform数量(查询 GL_MAX_VERTEX_UNIFORM_COMPONENTS)的限制。