今日来学习下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设定了一个800×600的窗口了,所以片段窗口空间坐标的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);
}

最终,它的作用类似于这样:

OpenGL ES教程——高级GLSL

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块,它储存了两个4×4矩阵。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));

代码如上,或许有些同学比较模糊,看一个图:

OpenGL ES教程——高级GLSL

在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);