OpenGL ES教程——高级GLSL

2,820 阅读3分钟

今天来学习下GLSL中的一些有意思的特性

1、GLSL的内建变量

我们之前已经接触过两个内建变量了:

  • gl_Position,顶点着色器的输出变量
  • gl_FragCoord,表示当前片元着色器处理的候选片元窗口相对坐标信息,是一个 vec4 类型的变量 (x, y, z, 1/w), 其中 x, y 是当前片元的窗口坐标,z是深度值,w则是齐次坐标

glsl中还有一些其它的内建变量,我们一起来了解下。

1.1、顶点着色器变量

我们能够选用的其中一个图元是GL_POINTS,如果使用它的话,每一个顶点都是一个图元,都会被渲染为一个点。我们可以通过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;
}

1.2、片段着色器变量

gl_FragCoord,它是片段着色器的一个输入变量,gl_FragCoord的z分量等于对应片段的深度值

gl_FragCoord的x和y分量是片段的窗口空间(Window-space)坐标,其原点为窗口的左下角。我们已经使用glViewport设定了一个800x600的窗口了,所以片段窗口空间坐标的x分量将在0到800之间,y分量在0到600之间。

通过利用片段着色器,我们可以根据片段的窗口坐标,计算出不同的颜色。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);
}

最后,它的效果类似于这样:

image.png

gl_FrontFacing是另一个输入变量,它是一个bool值,告诉我们当前片段是属于正向面的一部分还是背向面的一部分,如果当前片段是正向面的一部分那么就是true,否则就是false

注意,如果你开启了面剔除,你就看不到箱子内部的面了,所以现在再使用gl_FrontFacing就没有意义了。

输入变量gl_FragCoord能让我们读取当前片段的窗口空间坐标,并获取它的深度值,但是它是一个只读(Read-only)变量。我们不能修改片段的窗口空间坐标,但实际上修改片段的深度值还是可能的。GLSL提供给我们一个叫做gl_FragDepth的输出变量,我们可以使用它来在着色器内设置片段的深度值。

要想设置深度值,我们直接写入一个0.0到1.0之间的float值到输出变量就可以了:

gl_FragDepth = 0.0; // 这个片段现在的深度值为 0.0

如果着色器没有写入值到gl_FragDepth,它会自动取用gl_FragCoord.z的值。

然而,由我们自己设置深度值有一个很大的缺点,只要我们在片段着色器中对gl_FragDepth进行写入,OpenGL就会禁用所有的提前深度测试(Early Depth Testing)。它被禁用的原因是,OpenGL无法在片段着色器运行之前得知片段将拥有的深度值,因为片段着色器可能会完全修改这个深度值。

2、Uniform缓冲对象

到本文为止,我们已经写了不少示例代码了,几乎每个着色器中都会有viewprojection的uniform变量,如果示例中存在两个着色器,那我们就要设置两遍,有没有一个办法能让我们设置一次就行了呢?有,就是Uniform缓冲对象

OpenGL为我们提供了一个叫做Uniform缓冲对象(Uniform Buffer Object)的工具,它允许我们定义一系列在多个着色器中相同的全局Uniform变量。当使用Uniform缓冲对象的时候,我们只需要设置相关的uniform一次。当然,我们仍需要手动设置每个着色器中不同的uniform。


#version 300 es
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的Uniform块,它储存了两个4x4矩阵。Uniform块中的变量可以直接访问,不需要加块名作为前缀。接下来,我们在OpenGL代码中将这些矩阵值存入缓冲中,每个声明了这个Uniform块的着色器都能够访问这些矩阵。

使用方法:

    //获取uniform缓冲的索引
	auto uniformRedIndex = glGetUniformBlockIndex(redShader.ID, "Matrices");
    auto uniformBlueIndex = glGetUniformBlockIndex(blueShader.ID, "Matrices");
    //绑定着色器的uniform缓冲索引到绑定点1上(当然也可以绑定到0或2之类的)
    glUniformBlockBinding(redShader.ID, uniformRedIndex, 1);
    glUniformBlockBinding(blueShader.ID, uniformBlueIndex, 1);
    //生成索引缓冲
    glGenBuffers(1, &uboMatrices);
    glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
    //给uniform缓冲分配内存,但不填充数据
    glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
    glBindBuffer(GL_UNIFORM_BUFFER, 0);
    //将uboMatrices uniform缓冲也绑定到绑定点2上
    glBindBufferRange(GL_UNIFORM_BUFFER, 1, uboMatrices, 0, 2 * sizeof(glm::mat4));

代码如上,可能有些同学比较迷糊,看一个图:

image.png

在OpenGL上下文中,定义了一些绑定点(Binding Point),我们可以将一个Uniform缓冲链接至它。在创建Uniform缓冲之后,我们将它绑定到其中一个绑定点上,并将着色器中的Uniform块绑定到相同的绑定点,把它们连接到一起。

将Uniform块绑定到一个特定的绑定点中,我们需要调用glUniformBlockBinding函数,它的第一个参数是一个程序对象,之后是一个Uniform块索引和链接到的绑定点。Uniform块索引(Uniform Block Index)是着色器中已定义Uniform块的位置值索引。这可以通过调用glGetUniformBlockIndex来获取

glUniformBlockBinding(redShader.ID, uniformRedIndex, 1);

我们还需要绑定Uniform缓冲对象到相同的绑定点上,这可以使用glBindBufferBase或glBindBufferRange来完成。

glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); 
// 或 
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);

现在,所有的东西都配置完毕了,我们可以开始向Uniform缓冲中添加数据了。只要我们需要,就可以使用glBufferSubData函数,用一个字节数组添加所有的数据,或者更新缓冲的一部分


    glm::mat4 projection = glm::perspective(glm::radians(45.0f), rat, 0.1f, 100.0f);
    glm::mat4 view = camera.getViewMatrix();
    glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
    glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
    glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
    glBindBuffer(GL_UNIFORM_BUFFER, 0);

2.1、Uniform块布局

上文中提到的 layout (std140) 是啥意思呢?

Uniform块的内容是储存在一个缓冲对象中的,它实际上只是一块预留内存。因为这块内存并不会保存它具体保存的是什么类型的数据,我们还需要告诉OpenGL内存的哪一部分对应着着色器中的哪一个uniform变量。

简单来说,因为opengl并不知道Uniform块会存储啥东西,如果我要更新最后一个数据,那调用 glBufferSubData 时,内存偏移应该是多少呢?

layout (std140)给我们指定了一种简单的、易算的一种内存布局。方便我们写代码,可以这么理解

每个变量都有一个基准对齐量(Base Alignment),它等于一个变量在Uniform块中所占据的空间(包括填充量(Padding)),这个基准对齐量是使用std140布局的规则计算出来的。接下来,对每个变量,我们再计算它的对齐偏移量(Aligned Offset),它是一个变量从块起始位置的字节偏移量。一个变量的对齐字节偏移量必须等于基准对齐量的倍数。

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
}; 

所以,如果我要更新如上的uniform块的bool值,应该怎么写代码呢?

glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // GLSL中的bool是4字节的,所以我们将它存为一个integer
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b); 
glBindBuffer(GL_UNIFORM_BUFFER, 0);