本章主要对GLSL作一些深入的研究
引言
GLSL是一个强大的着色器语言,笔者对其研究也不过是些皮毛而已。学了本章之后,你不会一下子就成为着色器大师,或者弄出什么非常酷炫的东西。本章也不打算把GLSL的所有东西都呈现出来,如果你想完整的学习GLSL的所有内容,推荐你到Anton的网站上学习。
当然,多学点东西总是好的。对GLSL做一些深入的研究非常有必要,这样我们就能更好的在应用中组合使用GLSL的各个知识块,更快更好的实现想要的效果。
本章中,我们会讨论一些其他的内置变量,也会讨论数据块接口(重点是uniform块)。闲话少说,直接上正餐!
GLSL内置变量
目前为止,我们遇到的GLSL内置变量只有两个:gl_Position和gl_FragCoord。gl_Position表示顶点的位置,gl_FragCoord表示片元坐标。接下来我们将要讨论的五个内置变量(这只是很少一部分而已,不信你看这):gl_PointSize(顶点尺寸),gl_VertexID(顶点ID),gl_FragCoord(片元坐标),gl_FrontFacing(是否正面)以及gl_FragDepth(片元深度)。这些内置变量有些是顶点着色器的,有些是片元着色器的,应该很容易区分,前两个是顶点着色器的,后三个是片元着色器的。
顶点着色器内置变量
gl_PointSize
gl_PointSize可以用来设置顶点的显示尺寸大小,当我们绘制的是顶点时,这个设置更加明显。
我们来试试这个效果。新建一个工程命名成GLSLComing,将必要的文件都拷贝进去,修改顶点着色器如下:
void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
gl_PointSize = gl_Position.z; //gl_PointSize是float类型,因此我们直接赋值就行了。
}
我们将顶点的z分量赋值给了gl_PointSize,这样当我们把视点往后移时,我们将看到绘制的顶点越来越大!
附上源码给遇到困难的童鞋一点帮助。
gl_VertexID
当前正在被处理的顶点索引。如果你当前渲染的顶点是非索引形式的,那么它表示当前顶点的一个有效索引(已经处理的顶点数+首元素,有点像偏移)。如果你当前渲染的顶点是索引形式的,它就是一个从缓存中获取顶点的索引。
至于你可能会感到困惑的索引形式,glDrawElements绘制使用索引形式,glDrawArrays绘制使用非索引形式。
这个东西现在我们用不到,先把它介绍一下,备着以后用。
片元着色器变量
gl_FragCoord
对这个变量我们熟悉的很,在深度测试的时候就已经打过几次交到了。到目前为止,我们对其的认识就是其z分量是当前片元的深度值信息。当然,这个变量中可不仅有深度信息,用屁股想都知道肯定还有x和y的坐标信息。
值得高兴的是,gl_FragCoord变量中的x和y坐标就已经是窗口坐标了,原点在左下角,x的范围为0窗口宽度,y的范围为0窗口高度。有了这个信息,我们就可以对某些特殊位置做一些特殊处理了(例如某个地方是小地图,那么这个地方就不用绘制场景)。我们来做一些简单的实验,将窗口左半边的物体全部弄成绿色,右半边的物体全部弄成黄色:
void main()
{
if (gl_FragCoord.x < 640)
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
else
FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
运行之后,效果如下:
非常简单粗暴的效果,嘿嘿~
gl_FragDepth
gl_FragCoord变量虽然好用,但它有个缺点,那就是它是只读的。我们只能用其进行一些判断测试,无法修改它的内容。gl_FragDepth变量就稍稍弥补了其缺点,因为我们可以对其赋值从而修改片元的深度信息。
如果着色器没有给gl_FragDepth变量赋值,那么它就会自动从gl_FragCoord.z中获取。
另外,手动设置深度值有一个非常严重的问题,就是如果我们在片元着色器中对gl_FragDepth赋值了,那么OpenGL会自动禁用前期深度测试(early depth testing)。原因是OpenGL在运行片元着色器之前无法知道某个片元的深度值(前期深度测试是在片元着色器运行之前进行的)。所以,在使用gl_FragDepth变量时,你也需要稍稍考虑一下性能问题。
从OpenGL 4.2版本之后,我们有一种新的方法来缓解这个问题,那就是用下面这行代码来设置gl_FragDepth的条件:
layout (depth_<condition>) out float gl_FragDepth;
条件参数可以取以下这些值:
条件 | 描述 |
---|---|
any | 默认值。前期深度测试关闭,可能造成性能损失 |
greater | 只能设置比gl_FragCoord.z大的值 |
less | 只能设置比gl_FragCoord.z小的值 |
unchanged | 如果你写入gl_FragDepth,这个值也会写入gl_FragCoord.z |
通过指定greater或者less条件,OpenGL会认为你只会设置比gl_FragCoord.z大或者小的值,那么它仍然可以进行前期深度测试。另外两个参数则会禁用前期深度测试。
注意,这个设置只在4.2及更高版本有效
数据块接口
数据块,类似于C语言中的结构体,将一些变量集中起来进行管理。随着实现的功能越来越多,效果越来越华丽,我们需要用到的变量也会疯狂增加,那么,我们自然要想个办法将这些变量管理起来。根据经验,将变量整合到一个结构中是非常好的一个选择,于是,数据块就应运而生了。
根据变量的作用,一共有四种数据块:in块、out块、uniform块、buffer块。in/out块非常容易理解,就是将输入和输出变量整合到一起,uniform块将uniform变量整合到一起,buffer块还没到用它的时候,先无视。
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);
}
顶点着色器中定义了一个输出块,名字是VS_OUT,注意这是块名字,这个块的实例名是vs_out。要使用其中的元素,只需vs_out.TexCoords就可以了,和C中访问结构体成员一样。
相应的,片元着色器中需要定义一个名字一样的块,作为输入块,不过实例的名字可以不同。像上面的代码那样,片元着色器中的实例名是fs_in。使用方式和之前一样,fs_in.TexCoords即可使用。
(是不是觉得没啥实际用处,笔者开始也这么觉得~)
uniform块
之所以要将uniform块单独拿出来讲,是因为它重要并且理解较难。要定义一个uniform块,我们可以简单地像定义in/out块那样,也可以给它指定一些参数,这些参数就是难点所在!
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
layout (std140) uniform Matrics {
mat4 view;
mat4 projection;
};
void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
如上面的代码所写,我们把view和projection都放在了uniform块中(因为这两个矩阵不太会变,我们只要设置一次就好了,之前每次都设置太麻烦了。)如何去获取uniform块中的值我们后面再讲,这里先聚焦于新出现的layout(std140)上。
unifrom块布局
layout(std140)指定了uniform块中的数据内存对齐的方式(这是从C语言copy过来的名词)。C语言的struct结构体中,我们有时也会对其布局进行调整,不过时候不多,而且不需要我们去指定其内存对齐方式,因为我们用它默认的就好了。但是在GLSL中,我们对它的布局就非常感兴趣了,因为我们必须通过变量的位置去操作变量,对OpenGL来说着色器程序中的变量名是不可见的。
可以用的布局限定符如下所示:
布局限定符 | 描述 |
---|---|
shared | 设置uniform块是多个程序间共享的(默认布局方式) |
packed | 设置uniform块占用最小的内存空间,但是这样会禁止程序间共享这个块 |
std140 | 1.4版本之后的标准布局方式 |
std430 | 4.3版本之后的标准布局方式 |
row_major | 使用行主序的方式来存储uniform块中的矩阵 |
column_major | 使用列主序的方式来存储uniform块中的矩阵(默认值) |
layout(std140)指定了使用1.4版本之后的标准布局方式,没有设置矩阵是行主序还是列主序,默认就使用列主序,等价于layout(std140, row_major)。
默认情况下,GLSL使用的布局方式是shared。这种布局方式表示一旦偏移量被硬件确定之后,它就能在多个程序之间共享。使用共享布局,只要变量的顺序不变,GLSL就可以优化并重新定位uniform变量。但是我们不知道某个变量的偏移量,可以通过glGetUniformIndices函数来查询,这个超出了本章的讨论范围,我们暂时不进行说明。
因为shared布局会优化变量占用的内存空间,所以我们无法事先知道变量的偏移量,也就无法通过硬编码去设置变量。这里我们用一种更确切的方式定死每个变量的偏移,从而可以通过偏移量来设置变量,这种布局方式就是std140,先介绍一下std140的布局规则:
变量类型 | 布局规则 |
---|---|
标量类型:bool,float等 | 大小和对齐值都是基本机器类型的标量大小 |
两个分量的向量(如vec2) | 大小和对齐值是基础类型大小的2倍 |
三分量向量和4分量向量(vec3,vec4) | 大小和对齐值是基础标量类型大小的4倍 |
数组 | 数组中每个元素大小取整到vec4大小的整数倍 |
矩阵 | 类似于包含n个向量的数组,n是列总数 |
结构体 | 对齐值是最大结构成员的对齐值,取整到vec4大小的整数倍 |
感觉很复杂的样子?不要紧,我们来看一个例子就清楚了:
layout (std140) uniform ExampleBlock
{
// 大小 // 对齐值
float value; // 4 // 0
vec3 vector; // 16 // 16 (必须对齐到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
};
计算清楚每个变量的偏移之后,我们就可以使用glBufferSubData函数来给变量赋值(填充内存块)了。std140保证了在每个从程序中uniform块的布局都是一致的,虽然可能有点浪费内存。
使用uniform缓存对象
OpenGL提供了一个名叫uniform缓存对象(uniform buffer objects)的工具来声明一组全局的uniform变量。使用这个对象,我们只需要对变量进行一次设置就行。要使用uniform缓存对象我们还是需要平常的三部曲工作:
unsigned int uboMatrices;
glGenBuffers(1, &uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW);
生成-绑定-分配内存!无论我们想啥时候对其中的变量进行赋值,我们都可以绑定uboMatrices,然后调用glBufferSubData函数来赋值。但是,OpenGL怎么知道缓存对象绑定的是哪一个uniform块呢?
在OpenGL环境中,有许多内置的绑定点,我们可以将uniform缓存绑定到其中一个上,然后在着色器中将uniform块绑定到相同的位置,这样,双方的数据就可以互通了。
要设置uniform块的绑定点,我们需要调用glUniformBlockBinding函数,将需要绑定的位置作为参数传入。该函数的原型如下:void glUniformBlockBinding( GLuint program,GLuint uniformBlockIndex,GLuint uniformBlockBinding);
它需要3个参数,分别是:着色器ID,uniform块索引,以及一个绑定点。unifrom块索引可以通过glGetUniformBlockIndex获取,传入着色器ID和uniform块名就可以了。比如我们想将ExampleBlock绑定到位置2,就可以这么实现:
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "ExampleBlock");
glUniformBlockBinding(shaderA.ID, lights_index, 2);
如果有多个着色器共享这个uniform块,那么需要多次调用这段代码将每个着色器的相应块都绑定到2位置。
OpenGL 4.2之后可以在布局中指定绑定点,这样就需用在程序中调用函数了,比如:layout (std140, binding = 2) uniform ExampleBlock{...};
绑定完着色器中的uniform块之后,我们还要将ubo绑定到位置2。调用glBindBufferBase或者glBindBufferRange都是不错的选择。
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboMatrices);
// or
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboMatrices, 0, 152);
两个函数都需要BUFFER类型,绑定点,以及uboID作为参数。不同的是,glBindBufferRange函数可以指定绑定的范围,而glBindBufferBase会将整个区间都绑定。
完成绑定后,调用glBufferSubData就可以对某个位置写入数据了(注意笔者这里说的是某个位置,不是某个变量,因为我们在应用程序中是“看”不到变量名的,只能通过偏移值来对某块内存赋值):
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // 在glsl中bool值是4字节的,与int占用内存一样,所以我们保存成int型
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
一个简单的例子
照例,学完新知识后弄一个简单的东西来当练习。现在,要用4个着色器分别应用到4个盒子上,着色器有公用的uniform块,显示不同的颜色,我们来实现它。回顾之前的所有例子,观察矩阵和投影矩阵显然是可以共用的,我们把它放到一个uniform块中。块名就叫做Matrices:
#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);
}
接下来,把每个着色器的Matrices块都绑定到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);
再然后,生成一个uniform缓存对象,并绑定到绑定点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));
一切准备就绪,是时候往指定位置填充数据了。使用sizeof(glm::mat4)的方式来计算变量占用的空间和偏移值,生成view和projection矩阵后用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);
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);
上面的操作都是针对uniform块中的变量的。剩下的就是平常的操作了,没什么花头,跟着之前的代码走就行了:
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);
// ... 绘制绿色盒子
// ... 绘制蓝色盒子
// ... 绘制黄色盒子
整理完代码之后编译运行,你看到的结果应该和下面的图类似:
这正是我们想要的结果!附上源码以供参考。
总结
本章中,我们学习了GLSL的一些有趣的内置变量,包括gl_PointSize(顶点尺寸),gl_VertexID(顶点ID),gl_FragCoord(片元坐标),gl_FrontFacing(是否正面)以及gl_FragDepth(片元深度)。着重学习了数据块接口中的uniform块。使用uniform块,我们需要将需要共享的uniform块绑定到OpenGL的相同绑定点,然后用OpenGL的GL_UNIFORM_BUFFER绑定到一致的位置,通过操作uniform缓存对象来设置着色器中uniform块中的变量值。节省了对每个着色器都设置一遍的累赘操作。在最后的例子中,亲眼看到了学习的操作方式可行,不是停留在理论层面的飘飘然的东西。
呼~好累啊,休息休息
参考资料
www.learnopengl.com(非常好的网站,建议学习)