美文网首页
OpenGL学习22——GLSL

OpenGL学习22——GLSL

作者: 蓬篙人 | 来源:发表于2021-07-24 17:05 被阅读0次

1. 顶点着色器变量

  • gl_Position:顶点着色器中输出的裁剪空间位置矢量,顶点着色器中必须设置。

  • gl_PointSize:一个float类型的变量,可以设置像素点的宽度和高度。

  • 顶点着色器默认禁用顶点大小,想要启用我们需要启动OpenGL的GL_PROGRAM_POINT_SIZE标志。

glEnable(GL_PROGRAM_POINT_SIZE);
  • 下面我们将顶点大小设置为裁剪空间x-值的大小,这样顶点离观察者越远则越大。
void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    gl_PointSize = gl_Position.z;
}
  • 渲染效果。


    顶点大小渲染
  • gl_VertexID:顶点着色器的一个整数输入变量,包含当前绘制顶点的的ID。当使用glDrawElements函数进行索引渲染时,该变量包含当前绘制顶点的索引;当使用glDrawArray函数渲染时,该变量包含从开始渲染到目前所处理顶点的数量。

2. 片元着色器变量

  • gl_FragCoord:x和y分量是片元的窗体或屏幕空间坐标,原点为窗体左下角。如果我们通过glViewport函数指定窗体为800\times600,那么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);
}
片元位置渲染
  • gl_FrontFacing:告诉我们片元是属于正向面片还是背向面片,是一个bool类型变量,true代表片元属于正向面片,false则是背向。
  • 根据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_FragCoord是一个输入变量,允许我们读取片元的屏幕空间坐标和深度值,但是它是个只读变量。GLSL为我们提供了一个输出变量gl_FragDepth,我们可以在片元着色器中手动设置片元的深度值。要设置片元的深度值我们可以将任何0.0和1.0之间的值赋给gl_FragDepth
gl_FragDepth = 0.0;
  • 如果着色器中没有给gl_FragDepth赋值,变量自动取gl_FragCoord.z的值。但是,如果我们在着色器给gl_FragDepth赋值,OpenGL将会禁用早期深度测试,因为OpenGL无法在片元着色器运行前知道片元的深度值。
  • 虽然给gl_FragDepth赋值OpenGL会禁用早期深度测试,降低性能,但是从OpenGL 4.2,我们可以在片元着色器中使用一个深度条件(depth condition) 重新声明gl_FragDepth变量,为性能降低进行一定补偿。
layout (depth_<condition>) out float gl_FragDepth;
  • 深度条件包含以下值。
条件值 描述
any 默认值,禁用早期深度测试,丧失大部分性能。
greater 你只能让深度值大于gl_FragCoord.z
less 你只能让深度值小于gl_FragCoord.z
unchanged 你只能给gl_FragDepthgl_FragCoord.z值。
  • 下面是一个在片元着色器中增加片元深度值,但保留早期深度测试的例子。
// 注意这里的版本
#version 420 core
out vec4 FragColor;
layout (depth_greater) out float gl_FragDepth;

void main()
{
    FragColor = vec4(1.0);
    gl_FragDepth = gl_FragCoord.z + 0.1;
}

3. 接口模块(Interface blocks)

  • 目前,我们都是声明相匹配的输入/输出变量在顶点着色器和片元着色器之间传输数据。为了帮我们组织变量,GLSL提供了接口模块(interface block) 允许我们将变量组织到一起。接口模块的声明与结构体相似,只是基于输入或输出模块使用inout关键字。
#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;
}
  • 相应的在片元着色器中我们需要声明一个输入接口模块,片元着色器中的模块名称(VS_OUT)必须一样,但是实例名称(顶点着色器中的vs_out)可以是任何我们想要的名称。
#version 330 core
out vec4 FragColor;

in VS_OUT
{
    vec2 TexCoords;
} fs_in;

uniform sampler2D texture;

void main()
{
    FragColor = texture(texture, fs_in.TexCoords);
}

4. Uniform缓冲区对象

  • 目前在使用多个着色器时,即使对于每个着色器的uniform变量是一样的,我们也需要重复地为每个着色器设置uniform变量值。OpenGL为我们提供一个称为uniform缓冲区对象(uniform buffer object)的工具,允许我们声明一个全局unifrom变量集合,可在任何着色器程序中保持一致。
  • 当使用unifrom缓冲区对象,我们只需在固定的GPU内存中对相应unfirom变量设置一次,但是对于每个着色器唯一的uniform变量,我们还是需要进行手动设置。
  • uniform缓冲区对象与其他缓冲区对象相似,我们可以使用glGenBuffers函数来创建,并绑定到GL_UNIFORM_BUFFER缓冲区目标,然后将相关uniform数据存储到该缓冲区。下面是一个顶点着色器的例子,我们将投影和视矩阵存储到一个称为uniform模块(uniform block)的结构中:
#version 330 core
layout (location = 0) in vec3 aPos;

layout (std140) uniform Matrices
{
    mat4 projection;
    met4 view;
};

uniform mat4 model;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
  • 上面的例子中我们声明了一个叫做Matrices的uniform模块,模块中的变量可以直接访问而无需使用前缀。在OepnGL代码中我们将矩阵值设置到相应缓冲区中,那么每个声明了这个uniform模块的着色器都能够访问这些矩阵。

5. uniform模块布局

  • uniform模块存储在一个缓冲区对象中——一块保留的GPU全局内存,为了OpenGL能准确使用这些数据,我们需要告诉OpenGL内存中那部分对应着色器的那个uniform变量。总的来说,我们需要知道uniform模块总的字节大小,模块中每个uniform变量的偏移量以及每个变量之间的空间(或者说间隙)。
  • GLSL默认使用一个称为共享(shared)布局的uniform内存布局,共享意味着一旦硬件定义了偏移量,对于不同着色器程序偏移量都是一样的。使用共享布局,GLSL能够优化uniform变量的存储,只要保证变量的顺序不变即可。因为我们不知道每个uniform变量的偏移值,因此我们需要使用glGetUniformIndices函数来查询这些信息,这样我们才能精确地将变量填充到uniform缓冲区。
  • 虽然共享布局能够优化存储空间,但是需要我们查询每个uniform变量的偏移量,一般情况下我们不会使用共享布局,而是使用std140布局。std140布局通过一些关于偏移量的规则对每种变量类型的布局进行显式说明。
  • 使用std140布局规则,uniform模块中的每个变量有一个基本对齐(base alignment)要求,基本对齐的大小与变量占用的空间(含padding)相等。对于每个变量,它的对齐偏移量(aligned offset) 就是从模块起始到变量位置的字节偏移量。一个变量对齐偏移量的字节数必须是基本对齐的倍数。
  • 常见的规则如下表所示,注意GLSL的每种变量类型如int,floatbool都被定义为四个字节,表中四个字节实体表示为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
};
  • 根据上面例子中不同变量的偏移值计算,使用std140布局我们可以通过glBufferSubData函数来将数据填充到缓冲区指定的位置。
  • 与共享布局相似,还有一种需要我们查询偏移量的布局,称为packed。当使用packed布局,因为允许编译器优化uniform变量,因此无法保证不同着色器程序uniform模块具有相同的布局。

6. 使用uniform缓冲区

  • 使用uniform模块,第一步就是创建uniform缓冲区对象,然后绑定到GL_UNIFORM_BUFFER目标并分配足够的内存。
unsigned int uboExampleBlock;
glGenBuffers(1, &uboExampleBlock);
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW);   // 分配152个字节
glBindBuffer(GL_UNIFORM_BUFFER, 0);
  • 在OpenGL的上下文中定义了一些绑定点(binding points),我们通过将创建的uniform缓冲区对象和着色器中的uniform模块链接到这些绑定点,我们就将两者链接到一起。(图片取自书中
    绑定点
  • 要将着色器中的uniform模块设置到指定绑定点,我们首先调用glGetUniformBlockIndex函数获取uniform模块索引(uniform block index)——uniform模块在着色器中的位置;然后调用glUniformBlockBinding函数进行设置。
// 注意,对于每个着色器中的uniform模块我们都需要执行以下操作
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");
glUniformBlockBinding(shaderA.ID, lights_index, 2);
  • 从OpenGL 4.2开始,我们也能够在着色器中显式设置uniform模块的绑定点。
layout (std140, binding = 2) unfirom Lights { ... }
  • 要将uniform缓冲区对象绑定到相应绑定点,我们可以通过glBindBufferBaseglBindBufferRange函数实现。
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock);

glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
  • 在将uniform模块和uniform缓冲区对象绑定到对应绑定点后,我们就可以往缓冲区填充数据来设置uniform变量的值。对于boolean类的uniform变量,我们可以如下进行设置:
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true;   // GLSL中的bool定义为4个字节,因此保存为整数
// 参数值参考我们前面布局计算的例子
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, $b);
glBindBuffer(GL_UNIFORM_BUFFER, 0);

7. 一个简单的例子

从前面章节的代码我们知道,在渲染过程中我们经常需要使用投影、视和模型矩阵。这些矩阵中,只有模型矩阵经常需要改变。如果我们有多个着色器需要使用这些矩阵,那么我们最好使用uniform缓冲区对象。

  • 在本小节的例子中,我们将投影和视矩阵存储到一个称为Matrices的uniform模块。
#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);
}
  • 本例中,我们使用4个不同的片元着色器渲染四个不同颜色的立方体,但是4个着色器程序使用同一个顶点着色器。首先,我们将顶点着色器的uniform模块绑定到绑定点0。
unsigned int red    = glGetUniformBlockIndex(shaderRed.ID,    "Matrices");
unsigned int green  = glGetUniformBlockIndex(shaderGreen.ID,  "Matrices");
unsigned int blue   = glGetUniformBlockIndex(shaderBlue.ID,   "Matrices");
unsigned int yellow = glGetUniformBlockIndex(shaderYellow.ID, "Matrices");

glUniformBlockBinding(shaderRed.ID,    red,    0);
glUniformBlockBinding(shaderGreen.ID,  green,  0);
glUniformBlockBinding(shaderBlue.ID,   blue,   0);
glUniformBlockBinding(shaderYellow.ID, yellow, 0);
  • 其次,我们创建uniform缓冲区对象并绑定到绑定点0。
// generate uniform buffer object and allocate memory
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);
// bind buffer to binding point 0
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));
  • 当我们将着色器的uniform模块和uniform缓冲区对象都绑定到相同的绑定点后,我们就可以根据uniform模块的布局将数据填充到缓冲区。
// store data into the buffer that link to uniform block
glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_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);
  • 最后在渲染循环中绘制四个不同颜色的立方体。
glBindVertexArray(objectVAO);
// red cube
redShader.use();
glm::mat4 model = glm::mat4(1.0f);
model = glm::scale(model, glm::vec3(0.6f));
model = glm::translate(model, cubePositions[0]);
redShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
// green cube
greenShader.use();
model = glm::mat4(1.0f);
model = glm::scale(model, glm::vec3(0.6f));
model = glm::translate(model, cubePositions[1]);
greenShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
// blue cube
blueShader.use();
model = glm::mat4(1.0f);
model = glm::scale(model, glm::vec3(0.6f));
model = glm::translate(model, cubePositions[2]);
blueShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
// yellow cube
yellowShader.use();
model = glm::mat4(1.0f);
model = glm::scale(model, glm::vec3(0.6f));
model = glm::translate(model, cubePositions[3]);
yellowShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
  • 渲染效果。


    缓冲区对象渲染
  • 与单个的uniform变量相比,uniform缓冲区对象具有以下几个优势:
      1. 一次性设置多个uniform变量比一次只设置一个uniform变量更快。
      1. 如果你想要对多个着色器的相同uniform进行修改,那么uniform缓冲区对象只需在缓冲区中修改一次。
      1. unifrom缓冲区对象可以让你使用更多的uniform变量。OpenGL对使用uniform数据有一个限制,可以通过GL_VERTEX_UNIFORM_COMPONENTS进行查询,但当使用uniform缓冲区对象这个限制值更高。

相关文章

网友评论

      本文标题:OpenGL学习22——GLSL

      本文链接:https://www.haomeiwen.com/subject/xqlvmltx.html