本人初学者,文中定有代码、术语等错误,欢迎指正
我们将会讨论一些有趣的内建变量(Built-in Variable),管理着色器输入和输出的新方式以及一个叫做Uniform缓冲对象(Uniform Buffer Object)的有用工具。
啥是内建变量
在编写GLSL代码时,可以使用已经声明好了的变量就称为:内建变量,可供我们直接赋值使用。
例子
之前章节中遇到的:顶点着色器的输出向量gl_Position,和片段着色器的gl_FragCoord。
简介
每一个顶点都是一个图元,都会被渲染为一个点。
可以通过OpenGL的glPointSize函数来设置渲染出来的点的大小,但我们也可以在顶点着色器中修改这个值。
GLSL定义了一个叫做gl_PointSize输出变量,它是一个float变量,你可以使用它来设置点的宽高(像素)。在顶点着色器中修改点的大小的话,你就能对每个顶点设置不同的值了。
如何启用
默认是禁用的,启用OpenGL的GL_PROGRAM_POINT_SIZE:
glEnable(GL_PROGRAM_POINT_SIZE);
使用例子
将点的大小设置为裁剪空间位置的z值,也就是顶点距观察者的距离。点的大小会随着观察者距顶点距离变远而增大。
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
gl_PointSize = gl_Position.z;
}
对每个顶点使用不同的点大小,会在粒子生成之类的技术中很有意思
简介
整型变量gl_VertexID储存了正在绘制顶点的当前ID。
只能对它进行读取
当(使用glDrawElements)进行索引渲染的时候,这个变量会存储正在绘制顶点的当前索引。
当(使用glDrawArrays)不使用索引进行绘制的时候,这个变量会储存从渲染调用开始的已处理顶点数量。
简介
gl_FragCoord是输入变量,能让我们读取当前片段的窗口空间坐标,并获取它的深度值
gl_FragCoord的z分量等于对应片段的深度值。
gl_FragCoord的x和y分量是片段的窗口空间(Window-space)坐标,其原点为窗口的左下角。
例子
通过利用片段着色器的gl_FragCoord,我们可以根据片段的窗口坐标,计算出不同的颜色。
我们能够将屏幕分成两部分,在窗口的左侧渲染一种输出,在窗口的右侧渲染另一种输出。
void main()
{
if(gl_FragCoord.x < 400)
FragColor = vec4(1.0, 0.0, 0.0, 1.0);// 红色
else
FragColor = vec4(0.0, 1.0, 0.0, 1.0);// 绿色
}
简介
gl_FrontFacing是输入变量
前置知识
OpenGL能够根据顶点的环绕顺序来决定一个面是正向还是背向面。
gl_FrontFacing作用
如果我们不使用面剔除(不启用GL_FACE_CULL),那么gl_FrontFacing将会告诉我们当前片段是属于正向面的一部分还是背向面的一部分。
例子
我们可以这样子创建一个立方体,在内部和外部使用不同的纹理
#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_FrontFacing就没有意义了。
简介
前面介绍的:gl_FragCoord
是输入变量,能让我们读取当前片段的窗口空间坐标,并获取它的深度值,但是它是一个只读(Read-only)变量。
我们不能修改片段的窗口空间坐标,所以修改片段的深度值需要用到目前介绍的gl_FragDepth。
gl_FragDepth
是输出变量,我们可以使用它来在着色器内设置片段的深度值
例子
gl_FragDepth = 0.0; // 这个片段现在的深度值为 0.0
如果片段着色器没有写入值到gl_FragDepth,它会自动取用gl_FragCoord.z
的值。
缺陷
只要我们在片段着色器中对gl_FragDepth进行写入,OpenGL就会(像深度测试小节中讨论的那样)禁用所有的提前深度测试(Early Depth Testing)。
提前深度测试是:
它被禁用的原因是
OpenGL无法在片段着色器运行之前得知片段将拥有的深度值,因为片段着色器可能会完全修改这个深度值。
解决缺陷
从OpenGL 4.2起,我们仍可以对两者(写入gl_FragDepth 与 提前深度测试)进行一定的调和。
方法:在片段着色器的顶部使用深度条件(Depth Condition)重新声明gl_FragDepth变量:
layout (depth_<condition>) out float gl_FragDepth;
condition
可以为下面的值:
条件 | 描述 |
---|---|
any |
默认值。提前深度测试是禁用的,你会损失很多性能 |
greater |
你只能让深度值比gl_FragCoord.z 更大 |
less |
你只能让深度值比gl_FragCoord.z 更小 |
unchanged |
如果你要写入gl_FragDepth ,你将只能写入gl_FragCoord.z 的值 |
通过将深度条件设置为greater
或者less
,OpenGL就能假设你只会写入比当前片段深度值更大或者更小的值了。这样子的话,当深度值比片段的深度值要小的时候,OpenGL仍是能够进行提前深度测试的。
个人理解(可能有误)
声明写入gl_FragDepth的值只能更大
写入gl_FragDepth后
当前渲染的片段深度值铁定大于深度缓冲中的深度值,不管放到多大,当前片段一定会被丢弃,可依旧进行提前深度测试
声明写入gl_FragDepth的值只能更小
写入gl_FragDepth后:
当前渲染的片段深度值铁定小于深度缓冲中的深度值,不管放到多小,当前片段一定不会被丢弃,可依旧进行提前深度测试
解决缺陷例子
#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;
}
简介
作用:方便我们组合顶点着色器传入到片段着色器的这些输入/输出变量。(顶点位置、法线等顶点属性)
例子说明什么是接口块
输出
#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说明是输出块
out VS_OUT// VS_OUT(大写)是块名
{
vec2 TexCoords;
} vs_out;// vs_out(小写)是实例名
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
vs_out.TexCoords = aTexCoords;
}
输入
#version 330 core
out vec4 FragColor;
// 注意这里,in说明是输入块
in VS_OUT// VS_OUT(大写)是块名
{
vec2 TexCoords;
} fs_in;// fs_in(小写)是实例名
uniform sampler2D texture;
void main()
{
FragColor = texture(texture, fs_in.TexCoords);
}
块名:VS_OUT
顶点着色器与片段着色器的块名一致
实例名:
顶点着色器:vs_out
片段着色器:fs_in
只要两个接口块的名字一样,它们对应的输入和输出将会匹配起来。
之前使用Uniform缺陷
当使用多于一个的着色器时,尽管大部分的uniform变量都是相同的,我们还是需要不断地设置它们。
具体说明
比如一个场景有正方体、原体,他们两个用了两个着色器分别渲染,这两个着色器都有一个uniform mat4 project属性,代表都需要一个摄像机的投影矩阵,这样渲染前两个着色器分别需要设置上传一次这个project uniform,共两次。
使用Uniform缓冲对象
允许我们定义一系列在多个着色器中相同的全局Uniform变量。
当使用Uniform缓冲对象的时候,我们只需要设置相关的uniform一次。
tips:可以类别为编程语言中类中的普通变量和static变量。
例子
glsl
#version 330 core
layout (location = 0) in vec3 aPos;
// Uniform缓冲对象
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
uniform mat4 model;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
引出:什么是Uniform块布局
Uniform块的内容是储存在一个缓冲对象中的,它实际上只是一块预留内存
如上一节的
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
这是一个Uniform块声明,但是不具有内容
mat4 projection;
预留了一个4x4float数组大小的内存
mat4 view;
预留了一个4x4float数组大小的内存
需要指定内容
因为这块内存并不会保存它具体保存的是什么类型的数据,我们还需要告诉OpenGL在内存的哪一部分对应着着色器中的哪一个uniform变量(即哪块内存数据是给projection、哪块内存数据是给view)。
如何告诉,这就是Uniform块布局。
(可以类比之前:glVertexAttribPointer,来指定内存数组的顶点输入数据的哪一个部分对应顶点着色器的哪一个顶点属性)
假设着色器中有以下的这个Uniform块:
layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
mat4 matrix;
float values[3];
bool boolean;
int integer;
};
我们需要知道的是每个变量的大小(字节)和(从块起始位置的)偏移量,来让我们能够按顺序将它们放进缓冲中。
前置了解:硬件自动偏移量与共享布局
硬件自动定义了偏移量,GLSL会使用一个叫做共享(Shared)布局的Uniform内存布局
使用共享布局时,GLSL是可以为了优化而对uniform变量的位置进行变动的,只要变量的相对顺序保持不变。
能够使用像是glGetUniformIndices这样的函数来查询每个uniform变量的偏移量,从而计算获取这个uniform的位置进行上传数据。
小结:glsl会改变uniform的位置,则需要使用glGetUniformIndices函数查询uniform的偏移量,这会产生非常多的工作量。
std140布局
克服:硬件自动偏移量与共享布局的缺陷
简介
布局规则
GLSL中的每个变量,比如说int、float和bool,都被定义为4字节量。每4个字节将会用一个N
来表示。
类型 | 布局规则 |
---|---|
标量,比如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 vector;
由于:一个变量的对齐字节偏移量必须等于基准对齐量的倍数
本来:它的对齐偏移量是4的,但是它的基准对齐量是16,所以4需要向上增长到16为基准对齐量(16)的一倍
通过在Uniform块定义之前添加layout (std140)
语句,我们告诉OpenGL这个Uniform块使用的是std140布局。
我们已经讨论了如何在着色器中定义Uniform块,并设定它们的内存布局了,但我们还没有讨论该如何使用它们。
在OpenGL上下文中,定义了一些绑定点(Binding Point),我们可以将一个Uniform缓冲(图中的右边)链接至它。
在创建Uniform缓冲之后,我们将它绑定到其中一个绑定点上,并将着色器中的Uniform块(图中的左边)绑定到相同的绑定点,把它们连接到一起。
将Uniform块绑定到一个特定的绑定点中
// 将shaderA中的Lights Uniform块的索引点链接为绑定点的2号索引上
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");
glUniformBlockBinding(shaderA.ID, lights_index, 2);
glGetUniformBlockIndex
用来获取Uniform块索引(Uniform Block Index),是着色器中已定义Uniform块的位置值索引。
接受一个着色器程序对象和Uniform块的名称
glGetUniformBlockIndex
第一个参数是一个着色器程序对象
第二个参数是一个Uniform块索引和链接到的绑定点
/*从OpenGL 4.2版本起,你也可以添加一个布局标识符,显式地将Uniform块的绑定点储存在着色器中,这样就不用再调用glGetUniformBlockIndex和glUniformBlockBinding了。下面的代码显式地设置了Lights Uniform块的绑定点。*/
layout(std140, binding = 2) uniform Lights { ... };
绑定Uniform缓冲对象到相同的绑定点上
// 将uboExampleBlock缓冲链接为绑定点的2号索引上
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock);
// 或
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
glBindbufferBase
一个绑定点索引和一个Uniform缓冲对象作为它的参数
glBindBufferRange
向Uniform缓冲中添加数据
glBufferSubData函数,用一个字节数组添加所有的数据,或者更新缓冲的一部分。
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // GLSL中的bool是4字节的,所以我们将它存为一个integer
// 将缓冲的144字节开始的4个字节填充为b
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
对应的Uniform块,缓冲的144是boolean的对齐偏移量,4是boolean的基准对齐量
layout (std140) uniform ExampleBlock
{
// 基准对齐量 // 对齐偏移量
.....
float values[3]; // 16 // 96 (values[0])
// 16 // 112 (values[1])
// 16 // 128 (values[2])
bool boolean; // 4 // 144
.....
};
glsl
#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块设置为绑定点0
这4个着色器程序对象都使用这个顶点着色器,都各自具有一个uniform块,都链接到绑定点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缓冲对象本身
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));
首先我们为缓冲分配了足够的内存,它等于glm::mat4大小的两倍。GLM矩阵类型的大小直接对应于GLSL中的mat4。接下来,我们将缓冲中的特定范围(在这里是整个缓冲)链接到绑定点0。
四个Uniform块对应一个Uniform缓冲
填充这个缓冲
glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
// 向Uniform缓冲中添加数据,0位置开始,1个mat4大小,代表前个部分
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
这里我们将投影矩阵储存在Uniform缓冲的前半部分。
我们会将观察矩阵更新到缓冲的后半部分:
glm::mat4 view = camera.GetViewMatrix();
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
// 向Uniform缓冲中添加数据,1个mat4大小起始位置,1个mat4大小,代表后半部分
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
只需要设置一次
现在要用4个不同的着色器绘制4个立方体,它们的投影和观察矩阵都会是一样的。
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);
// ... 绘制绿色立方体
// ... 绘制蓝色立方体
// ... 绘制黄色立方体
唯一需要设置的uniform只剩model uniform了。在像这样的场景中使用Uniform缓冲对象会让我们在每个着色器中都剩下一些uniform调用。最终的结果会是这样的:
效果