高级数据&高级GLSL

GLSL内建变量

顶点着色器

  1. gl_Position

这个在顶点着色器里面已经见的太多了,设置顶点的坐标这便是它全部的功能。

  1. gl_PointSize

这个变量可以在顶点着色器中设置每个顶点像素的宽高,但是一般是被禁用的,需要用glEnable(GL_PROGRAM_POINT_SIZE)进行开启。

  1. gl_VertexID

这个变量记录了当前绘制顶点的索引,如果不是使用glDrawElements的话就会是从绘制开始已经绘制了的顶点的数。

片段着色器

  1. gl_FragCoord
    gl_FragCoord是片段着色器中记录每个像素的x、y以及深度信息的变量。

  2. gl_FrontFacing
    这个变量是bool类型,主要是判断当前像素是否是正面,如果是正面便设为true反面则为false

  3. gl_FragDepth
    这个变量用来控制片段的深度值,虽然gl_FragDepth.z是深度值,但它只是可读的变量,所以要改变深度值要用gl_FragDepth
    但是如果使用了这个变量着色器将不会进行提前深度测试,因为提前深度测试在深度着色器之前运行,如果使用了这个变量,那么着色器将无法得知最终的深度值。

接口块

接口块可以将我们要传递的数据进行打包,例如我们可以声明一个这样的接口块:

1
2
3
4
5
6
out VS_OUT{
vec3 Postion;
vec3 Normal;
vec2 Texcoord;
mat4 Projection;
} vs_out;

可以看到接口块的声明和结构体非常相似,其中out和之前功能一样,也是表示输出和输入,然后VS_OUT则是接口块名称,最后vs_out则是实例名称。然后在下一个着色器我们可以这样声明来接收:

1
2
3
4
5
6
in VS_OUT{
vec3 Postion;
vec3 Normal;
vec2 Texcoord;
mat4 Projection;
} fg_in;

其中接口名必须要求是一样的,但是实例名可以随便,因此我们可以取一个和着色器相对应的名字,例如在片段着色器则取名为fg_in
接口块可以方便我们对变量进行管理,同时还可以快速的声明数组,例如这样我们就声明了一个接口块数组:

1
2
3
4
5
6
out VS_OUT{
vec3 Postion;
vec3 Normal;
vec2 Texcoord;
mat4 Projection;
} vs_out[];

Uniform缓冲对象

Uniform缓冲对象也叫(Uniform Buffer Object,UBO),它可以让我们在多个着色器中共享数据。例如如果我这样声明:

1
2
3
4
5
6
7
8
9
layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
mat4 matrix;
float values[3];
bool boolean;
int integer;
};

这样凡是声明了同样的Uniform块的着色器都将得到这些数据。

Uniform块布局

由于uniform块最终要存储到UBO缓冲对象中,而缓冲对象也只是CPU为你预留的一块内存区域,就像VBO一样,在glBufferData之后还是需要glVertexAttribPointer()来指明每个部分对应着着色器中的location=x,同样UBO也需要你指明,但是这里不再是像VBO那样需要自己手动设置,UBO会通过提前设置好的布局来指定数据,也就是上面着色器中的std140
std140只是众多布局的一种,但是也是最好设定数据的,我们只需要按照它的这种布局规则将数据填入相应的位置即可。首先每种数据类型都会有基准对其量对齐偏移量,基准对其量指的是每种数据的大小,每四个字节对应一个N,这里是几个基本数据的大小说明。

而对齐偏移量其实就是数据在内存中开始位置的偏移量,但是这里需要说明的是对齐偏移量必须是基准对其量的整数倍,也就是说,例如前面有个float变量只用了四个字节,但是紧随其后的vec3变量不能直接从四字节后开始,而是要从第十六个字节开始,因为vec3的基准对其量是16,所以它对其偏移量只能是16、32、64…等等。因此这样就导致了std140布局并不是最节省空间的布局,相反它中间可能会有很多这样的“空隙”。

使用Uniform缓冲对象

我们以这样一个Uniform块为例说一下Uniform缓冲对象要如何使用:

1
2
3
4
layout(std140) uniform Matrices{
mat4 Projection;
mat4 View;
};

我们在着色器里面声明了这样一个Uniform块,对应的我们需要创建一个Uniform缓冲对象

1
2
unsigned int UBO;
glGenBuffers(1,&UBO);

之后和VBO一样,也需要绑定然后再填充数据

1
2
3
glBindBuffer(GL_UNIFORM_BUFFER,UBO);
glBufferData(GL_UNIFORM_BUFFER,2*sizeof(mat4),NULL,GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER,0);

这里可以注意到,我们首先并没有直接填写数据,这是因为UBO的数据要按照stb140的规则来(前面讨论的),所以我们后面便使用glBufferSubData来填写数据

1
2
3
4
glBindBuffer(GL_UNIFORM_BUFFER,UBO);
glBufferData(GL_UNIFORM_BUFFER,2*sizeof(mat4),NULL,GL_STATIC_DRAW);
glBufferSubData(GL_UNIFORM_BUFFER,0,sizeof(mat4),value_ptr(projection));
glBufferSubData(GL_UNIFORM_BUFFER,sizeof(mat4),sizeof(mat4),value_ptr(View));

这里还需要说明的是,glBufferSubData的参数,第一个便是绑定到的缓冲,第二个数据的起始位置,第三个是数据的长度,第四个便是数据的地址。这样我们就算是把数据成功绑定到UBO缓冲中了,但是这样还没结束,我们要知道Uniform块我们可以申请很多个,UBO我们也可以申请很多个,那么它们之间改如何对应呢?所以接下来就要开始将Uniform块和UBO缓冲绑定起来。

绑定点

我们是通过绑定点的方式来将Uniform块和UBO缓冲结合在一起的,也即是绑定到同一个绑定点的Uniform块将拥有相同的UBO缓冲,也即拥有相同的数据

首先先使用glUniformBlockBinding函数将Uniform块绑定到绑定点上,这个函数的参数是第一个是着色器程序Id,第二个是Uniform块索引,第三个是绑定点序列,而Uniform块则是这个Uniform块在这个着色器程序里面的唯一标识,需要通过glGetUniformBlockIndex(Shader_ID,Uniform_Name)函数获取。
UBO这里相对简单,只需要使用glBindBufferBase或者glBindBufferRange来绑定,后者比前者多了范围参数,可以指定绑定UBO缓冲中的哪一部分。

最后我们的Uniform块和UBO便设置完成并绑定到一起了,我们可以使用Uniform缓冲来储存一些常用的数据,例如Projection矩阵或者是View矩阵,这样我们只需要进行一次改变便可以将所有绑定相同的Uniform块的数据同步更新。最后欣赏一下微软图标:-),其中的四个正方体只有Model矩阵不同,所以就用Uniform块储存了Projection矩阵和View矩阵。