从顶点着色器向片段着色器发送数据时,可以声明几个对应的输入/输出变量。将它们一个一个声明是着色器间发送数据最简单的方式了,但当程序变得更大时,希望发送的可能就不只是几个变量了,它还可能包括数组和结构体;为了帮助我们管理这些变量,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;
}
//片元着色器
#version 330 core
out vec4 FragColor;
in VS_OUT
{
vec2 TexCoords;
} fs_in;
uniform sampler2D texture;
void main()
{
FragColor = texture(texture, fs_in.TexCoords);
}
OpenGL提供了一个叫做Uniform缓冲对象(Uniform Buffer Object)的工具,它允许我们定义一系列在多个着色器中相同的全局Uniform变量。当使用Uniform缓冲对象的时候,我们只需要设置相关的uniform一次。当然,我们仍需要手动设置每个着色器中不同的uniform。并且创建和配置Uniform缓冲对象会有一点繁琐。
因为Uniform缓冲对象是一个缓冲,可以使用glGenBuffers来创建它,将它绑定到GL_UNIFORM_BUFFER缓冲目标,并将所有相关的uniform数据存入缓冲。
GLuint ubo;
glGenBuffers(1,&ubo);
glBindBuffer(GL_UNIFORM_BUFFER,ubo);
glBufferData(GL_UNIFORM_BUFFER,size,buffer,GL_STATIC_RAW);
//顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
//将projection和view矩阵存储到Uniform块(Uniform Block)中:
//std140:定义的Uniform块对它的内容使用一个特定的内存布局。
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
uniform mat4 model;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
uniform 块的内容是储存在一个缓冲对象中,它实际只是一块预留内存。因为这块内存并不会保存它具体保存的是什么类型的数据,我们需要告诉openGL内存的哪一部分对应着着色器中的哪一个uniform变量。
layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
mat4 matrix;
float values[3];
bool boolean;
int integer;
};
默认情况下,GLSL会使用一个叫做共享(shared)布局的uniform 内存布局,共享是因为一旦硬件定义了偏移量,他们在多个程序中共享并一致的;使用共享布局时,GLSL是可以为了优化而对uniform 变量的位置进行变动,只要变量的顺序保持不变;因为我们无法发知道uniform 变量的偏移量,可以使用glGetUniformIndices来获取指定名称uniform 变量的索引位置,使用glGetActiveUniformsiv 来获取指定索引位置的偏移量和大小:
void glGetUniformIndices(GLuint program, GLsizei uniformCount, const char** uniformNames,GLuint* uniformIndices);
返回uniformCount 个 uniform变量的索引位置;变量的名称通过uniformNames来指定,每个名称都以NULL结尾;
//顶点着色器 和 片元着色器共享的 ”Uniforms“ 的 uniform 块
...
"uniform Uniform {"
" vec3 translation;"
" flaot scale;"
" vec4 rotation;"
" bool enabled;"
"}"
...
//程序代码
.....
enum{
Translation,
Scale,
Rotation,
Enabled,
NumUniforms
}
GLfloat scale = 0.5;
GLfloat translation[] = {0.1,0.1;0.0};
GLfloat rotation[] = {90,0.0,0.0,1.0};
GLBoolean enabled = GL_TRUE;
//对应着色器中的uniform 块中 uniform的变量名称
const char* names[NumUniforms] = {
"translation",
"scale",
"rotation",
"enabled"
}
GLuint indices[NumUniforms];
GLuint size[NumUniforms];
GLuint offset[NumUniforms];
GLuintt type[NumUniforms];
//获取所有uniform变量的索引
glGetUniformIndices(program,NumUniforms,names,indices);
//通过uniform 变量的索引来获取相应的偏移值、大小、类型等信息
glGetActiveUniformsiv(program,NumUniforms,indices,GL_UNIFORM_OFFSET,offset);
glGetActiveUniformsiv(program,NumUniforms,indices,GL_UNIFORM_SIZE,size);
glGetActiveUniformsiv(program,NumUniforms,indices,GL_UNIFORM_TYPE,type);
GLvoid* buffer;
//获取uniform缓存的索引,并获取整个块的大小
GLuint uboIndex = glGetActiveUniformBlockiv(program,"Uniforms");
glGetActiveUniformBlockiv(program,uboIndex,GL_UNIFORM_BLOCK_DATA_SIZE,&uboSize);
buffer = malloc(uboSize);
//组装buffer
memcpy(buffer + offset[Scale],&scale, size[Scale]*4);
...
当使用紧凑(Packed)布局时,是不能保证这个布局在每个程序中保持不变的(即非共享),因为它允许编译器去将uniform变量从uniform 块中优化掉,这在每个着色器中都可能是不同的。
std140 布局声明了每个变量的偏移量都是由一系列规则所决定的,这显式的声明了每个变量类型的内存布局;由于显式提及,我们可以手动的计算出每个变量的偏移量。
每个变量都有一个基准对齐量,它等于一个变量在uniform 块中所占据的空间,这个基准对齐量是使用std140 布局的规则计算出来的,接下来,在计算它的对齐偏移量,它是一个变量从块起始位置的字节偏移量;一个变量的对齐字节偏移量必须等于基准对其量的倍数。
GLSL中每个每个变量,比如int,float,bool,都被定义为4字节,每4个字节用N来表示:
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
};
使用示例见下文的 uniform 缓冲的使用
std430 只适用于GLSL版本4.30或者更高版本。
从OpenGL 4.2版本起,你也可以添加一个布局标识符,显式地将Uniform块的绑定点储存在着色器中,这样就不用再调用glGetUniformBlockIndex和glUniformBlockBinding了。下面的代码显式地设置了Lights Uniform块的绑定点。
layout(std140, binding = 2) uniform Lights { ... };
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);
在OpenGL 上下文中,定义了绑定点(binding point),我们可以将一个uniform 缓冲链接到它;在创建uniform 缓冲之后,可以将它绑定到其中的一个绑定点上;同时将着色器中的uniform块绑定到相同的绑定点上,就将他们连接到一起了。
使用如下方式将Uniform缓冲对象绑定到绑定点2上:
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock);
// 或
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
用如下方式将Lights Uniform 块 链接到绑定点2:
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");
glUniformBlockBinding(shaderA.ID, lights_index, 2);
以使用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);
GLSL 中的buffer 块,或者对于应用程序而言,就是着色器的存储缓存对象,他的行为和uniform类似。
buffer 块 和 uniform 的区别:
1. 着色器可以写入buffer块,修改其中的内容并呈现给其他的着色器调用或者应用程序本身;
2. 可以在渲染之前再决定它的大小,而不是在编译和链接的时候。
TODO具体使用
in int gl_VertexID; in int gl_InstanceID; out gl_PerVertex{ vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; }
gl_VertexID
整型变量gl_VertexID储存了正在绘制顶点的当前ID。当(使用glDrawElements)进行索引渲染的时候,这个变量会存储正在绘制顶点的当前索引。当(使用glDrawArrays)不使用索引进行绘制的时候,这个变量会储存从渲染调用开始的已处理顶点数量。
gl_InstanceID
顶点着色器的输入变量gl_InstanceID 保存在实例化绘图命令中的当前图元的实例数值;如果当前图元不来子实例化绘图命令,那么该值为0。
gl_Position
作为输出变量,gl_Position用来保存顶点位置的齐次坐标;该值用作图元装配、裁剪、筛选,以及其他同固定功能操作。
如果在顶点着色器中没有对g'l_Position 赋值,那么在定点处理阶段它的值是不确定的;作为输入变量,读取前着色器阶段写入的值作为gl_Position的值。
gl_PointSize
gl_PointSize输出变量,它是一个float变量,你可以使用它来设置点的宽高(像素)。在顶点着色器中修改点的大小的话,你就能对每个顶点设置不同的值了。
在顶点着色器中修改点大小的功能默认是禁用的,如果你需要启用它的话,你需要用图下方法启用:
glEnable(GL_PROGRAM_POINT_SIZE);
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
gl_PointSize = gl_Position.z;
}
gl_ClipDistance[]
in gl_PerVertex{ vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; }gl_in[]; in int gl_PrimitiveIDIn; in int gl_InvocationID; out gl_PerVertex{ vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; }; out int gl_PrimitiveID; out int gl_Layer; out int gl_ViewportIndex;
gl_PrimitiveIDIn
gl_InvocationID
gl_PrimitiveID
gl_Layer
gl_ViewportIndex
in vec4 gl_FragCoord; in bool gl_FrontFacing; in float gl_ClipDistance[]; in vec2 gl_PointCoord; in int gl_PrimitiveID; in int gl_SampleID; in vec2 gl_SamplePosition; in int gl_SampleMaskIn[]; in int gl_Layer; in int gl_ViewPortIndex; out float gl_FragDepth; out int gl_SampleMask[];
gl_FragCoord
输入片元着色器中的gl_FragCoord保存当前片元的窗口位置,另外gl_FragCoord的z分量等于对应片段的深度值;
gl_FrontFacing
OpenGL能够根据顶点的环绕顺序来决定一个面是正向还是背向面。如果我们不(启用GL_FACE_CULL来)使用面剔除,那么gl_FrontFacing将会告诉我们当前片段是属于正向面的一部分还是背向面的一部分。
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);
}
gl_PointCoord
当点图形被使用时,gl_PointCoord 的2维坐标值表示当前片元的点图元中的位置;他们的取值范围在【0.0,1.0】;如果当前未使用点图元或者点图形没有使用时,gl_PointCoord 的值是不确定的。
gl_SampleID
gl_SampleID 时当前处理的样本数量,他的取值范围是【0,gl_NumSamples -1】.使用这个变量的片元着色器,而这个着色器则评估为每样本。
gl_SamplePosition
在多样本绘图缓冲中当前样本的位置,g'l_SamplePositon 的 x, y 部分包含当前样本的子像素坐标,取值【0.0,1.0】;如果在任何一个片元着色器中使用这个变量,则这个片元着色器评估为每样本。
gl_FragDepth
gl_FragDepth是片元着色器的的输出变量,可以使用它来在着色器内设置片段的深度值。要想设置深度值,我们直接写入一个0.0到1.0之间的float值到输出变量就可以了,如果着色器没有写入值到gl_FragDepth,它会自动取用gl_FragCoord.z
的值。
gl_FragDepth = 0.0; // 这个片段现在的深度值为 0.0
当我们在片段着色器中对gl_FragDepth进行写入有一个很大的缺点:OpenGL会禁用所有的提前深度测试(Early Depth Testing)。它被禁用的原因是,OpenGL无法在片段着色器运行之前得知片段将拥有的深度值,因为片段着色器可能会完全修改这个深度值。