OpenGL学习笔记28-Advanced GLSL

Advanced GLSL

Advanced-OpenGL/Advanced-GLSL

这一章不会真正向你展示给你的场景视觉质量带来巨大提升的超级高级酷的新功能。本章或多或少地介绍了GLSL中一些有趣的方面和一些可能对你未来的努力有所帮助的技巧。基本上,当你结合使用GLSL创建OpenGL应用程序时,一些值得了解的特性会让你的生活更轻松。

我们将讨论一些有趣的内置变量,组织着色器输入和输出的新方法,和一个非常有用的工具称为统一缓冲对象。

GLSL's built-in variables

着色器是非常流水线的,如果我们需要数据从任何其他来源的当前着色器,我们将不得不传递数据。我们学会了通过顶点属性、uniform和采样器来做到这一点。然而,还有一些额外的变量是由GLSL定义的,以gl_为前缀,这些变量为我们提供了收集和/或写入数据的额外方法。到目前为止,我们已经在章节中看到了其中的两个:顶点着色器的输出向量gl_Position和片段着色器的gl_FragCoord。

我们将讨论一些在GLSL中内置的有趣的内置输入和输出变量,并解释它们如何使我们受益。注意,我们不会讨论GLSL中存在的所有内置变量,所以如果你想查看所有内置变量,你可以查看OpenGL的wiki。

Vertex shader variables

我们已经看到了gl_Position,它是顶点着色器的剪辑空间输出位置向量。如果你想在屏幕上渲染任何东西,在顶点着色器中设置gl_Position是一个严格的要求。没有我们以前没见过的。

gl_PointSize

我们可以选择的渲染原语之一是GL_POINTS,在这种情况下,每个单个顶点都是一个原语,并被渲染为一个点。通过OpenGL的glPointSize函数可以设置被渲染的点的大小,但是我们也可以在顶点着色器中影响这个值。

GLSL定义的一个输出变量叫做gl_PointSize,它是一个浮动变量,您可以在其中设置点的宽度和高度(以像素为单位)。通过在顶点着色器中设置点的大小,我们可以对点的尺寸进行每个顶点的控制。

默认情况下,影响顶点着色器中的点大小是禁用的,但如果你想启用这个,你必须启用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

gl_Position和gl_PointSize是输出变量,因为它们的值是从顶点着色器的输出中读取的;我们可以通过给他们写信来影响结果。顶点着色器也给了我们一个有趣的输入变量,我们只能从中读取,叫做gl_VertexID。

整数变量gl_VertexID保存我们正在绘制的顶点的当前ID。当执行索引渲染(使用glDrawElements)时,这个变量保存我们正在绘制的顶点的当前索引。当绘图时没有索引(通过glDrawArrays),这个变量保存当前处理顶点的数量,从开始渲染调用。

Fragment shader variables

在fragment shader中,我们也可以使用一些有趣的变量。GLSL为我们提供了两个有趣的输入变量,即gl_FragCoord和gl_FrontFacing。

gl_FragCoord

在讨论深度测试期间,我们已经多次看到了gl_FragCoord,因为gl_FragCoord向量的z分量等于特定片段的深度值。然而,我们也可以使用这个向量的x和y分量来实现一些有趣的效果。

gl_FragCoord的x和y组件是片段的窗口或屏幕空间坐标,起源于窗口的左下角。我们使用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);        
}  

因为窗口的宽度等于800,所以每当一个像素的x坐标小于400时,它就必须位于窗口的左侧,我们将为该片段赋予不同的颜色。

我们现在可以计算两个完全不同的碎片着色结果,并将它们分别显示在窗口的不同一侧。这对于测试不同的照明技术来说是非常棒的。

gl_FrontFacing

片段着色器中另一个有趣的输入变量是gl_FrontFacing变量。在face culling一章中,我们提到OpenGL能够根据顶点的缠绕顺序判断出一个面是正面还是背面。gl_FrontFacing变量告诉我们当前片段是前置的还是后置的。例如,我们可以决定为所有的背面输出不同的颜色。

gl_FrontFacing变量是一个bool,如果片段是前部的一部分,它为真,否则为假。我们可以用这种方法创建一个立方体,里面的纹理和外面的纹理不同:


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

如果我们看一下容器内部,我们可以看到一个不同的纹理正在被使用。

注意,如果启用了face culling,那么就不能在容器内看到任何face,使用gl_FrontFacing就没有意义了。

gl_FragDepth

输入变量gl_FragCoord是一个输入变量,它允许我们读取屏幕空间坐标并获得当前片段的深度值,但它是一个只读变量。我们不能影响片段的屏幕空间坐标,但可以设置片段的深度值。GLSL为我们提供了一个名为gl_FragDepth的输出变量,我们可以用它来手动设置着色器中片段的深度值。

为了在着色器中设置深度值,我们在输出变量中写入0.0到1.0之间的任意值:


gl_FragDepth = 0.0; // this fragment now has a depth value of 0.0

如果着色器没有向gl_FragDepth写入任何内容,变量将自动从gl_fragcodel .z中获取它的值。

但是手动设置深度值有一个主要的缺点。这是因为一旦我们在片段着色器中写入到gl_FragDepth, OpenGL就会禁用早期深度测试(在深度测试一章中讨论过)。它被禁用,因为OpenGL在我们运行片段着色器之前不知道片段会有什么深度值,因为片段着色器可能会改变这个值。

通过写入到gl_FragDepth,您应该考虑到这种性能损失。然而,在OpenGL 4.2中,我们仍然可以通过在fragment shader的顶部用depth条件重新声明gl_FragDepth变量来协调两边:


layout (depth_<condition>) out float gl_FragDepth;

这个条件可以取以下值:

ConditionDescription
anyThe default value. Early depth testing is disabled.
greaterYou can only make the depth value larger compared to gl_FragCoord.z.
lessYou can only make the depth value smaller compared to gl_FragCoord.z.
unchangedIf you write to gl_FragDepth, you will write exactly gl_FragCoord.z.

通过指定greater或less作为depth条件,OpenGL可以假设您只写大于或小于片段深度值的深度值。通过这种方式,当深度缓冲区值属于gl_fragcoator .z的另一个方向时,OpenGL仍然能够进行早期深度测试。

一个例子,我们在片段着色器中增加了深度值,但仍然想保留一些早期的深度测试,如下面的片段着色器所示:


#version 420 core // note the GLSL version!
out vec4 FragColor;
layout (depth_greater) out float gl_FragDepth;

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

请注意,这个特性只能在OpenGL 4.2或更高版本中使用。

Interface blocks

到目前为止,每次我们将数据从顶点发送到片段着色器时,我们都声明了几个匹配的输入/输出变量。一次声明一个是最简单的方式发送数据从一个着色器到另一个,但当应用程序变得更大,你可能想要发送超过几个变量。

为了帮助我们组织这些变量,GLSL为我们提供了接口块,它允许我们将变量分组在一起。这样的接口块的声明看起来很像结构声明,除了它现在使用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;
}  

这一次,我们声明了一个名为vs_out的接口块,它将我们想要发送到下一个着色器的所有输出变量分组在一起。这是一个平凡的例子,但是你可以想象这有助于组织你的着色器的输入/输出。当我们想要将着色器输入/输出分组到数组中时,它也很有用,我们将在下一章中看到关于几何着色器的内容。

然后我们还需要声明一个输入接口块在下一个着色器,这是片段着色器。块名(VS_OUT)在片段着色器中应该是相同的,但是实例名(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);   
} 

只要两个接口块名称相等,它们对应的输入和输出就会匹配在一起。这是另一个有用的特性,帮助组织你的代码,证明有用的时候,某些shader阶段,如几何shader。

Uniform buffer objects

我们使用OpenGL已经有一段时间了,学习了一些很酷的技巧,但也有一些烦人的地方。例如,当使用一个以上的着色器时,我们必须不断地设置统一的变量,其中大多数变量对于每个着色器都是完全相同的。

OpenGL给了我们一个叫做统一缓冲对象的工具,它允许我们声明一组在任意数量的着色程序上保持相同的全局统一变量。当使用统一的缓冲对象时,我们只在固定的GPU内存中设置一次相关的制服。我们仍然需要手动设置每个着色器唯一的uniform。不过,创建和配置统一的缓冲区对象需要做一些工作。

因为统一缓冲区对象是一个类似于任何其他缓冲区的缓冲区,我们可以通过glGenBuffers创建一个,将其绑定到GL_UNIFORM_BUFFER缓冲区目标,并将所有相关的统一数据存储到缓冲区中。对于如何存储统一缓冲区对象的数据,有一些规则,我们稍后会讲到。首先,我们将取一个简单的顶点着色器,并存储我们的投影和视图矩阵在所谓的统一块:


#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,它存储了两个4x4矩阵。可以直接访问统一块中的变量,而不需要将块名称作为前缀。然后我们将这些矩阵值存储在OpenGL代码中的某个缓冲区中,并且每个声明这个统一块的着色器都可以访问这些矩阵。

您现在可能想知道layout (std140)语句是什么意思。这就是说,当前定义的统一块为其内容使用了一个特定的内存布局;该语句设置统一的块布局。

Uniform block layout

一个统一块的内容存储在一个缓冲对象中,它实际上不过是一个全局GPU内存的保留块。因为这块内存没有关于它持有什么类型的数据的信息,我们需要告诉OpenGL内存的哪些部分对应于着色器中的哪些统一变量。

想象一下在一个着色器中如下的统一块:


layout (std140) uniform ExampleBlock
{
    float value;
    vec3  vector;
    mat4  matrix;
    float values[3];
    bool  boolean;
    int   integer;
};  

我们想知道的是每个变量的大小(以字节为单位)和偏移量(从块的开始),这样我们就可以按照它们各自的顺序将它们放到缓冲区中。每个元素的大小在OpenGL中有明确的说明,并直接对应于c++数据类型;向量和矩阵是浮点数的(大)数组。OpenGL没有明确说明的是变量之间的间距。这允许硬件定位或垫变量,因为它看到适合。例如,硬件可以将一个vec3与一个浮点数相邻。并不是所有的硬件都能处理这个问题,并在附加浮点数之前将vec3填充到一个包含4个浮点数的数组中。一个伟大的功能,但不方便我们。

默认情况下,GLSL使用一种称为共享布局的统一内存布局——共享是因为一旦由硬件定义了偏移量,它们就会在多个程序之间一致地共享。通过共享布局,GLSL允许为优化重新定位统一变量,只要变量的顺序保持不变。因为我们不知道每个统一变量的偏移量我们不知道如何精确地填充这个统一缓冲区。我们可以使用像glGetUniformIndices这样的函数来查询这些信息,但这不是我们在本章将要采用的方法。

虽然共享布局为我们提供了一些节省空间的优化,但我们需要查询每个统一变量的偏移量,这意味着要做很多工作。然而,一般的做法是不使用共享布局,而是使用std140布局。std140布局通过标准化由一组规则控制的各自的偏移量显式地声明每个变量类型的内存布局。由于这是标准化的,我们可以手动计算出每个变量的偏移量。

每个变量的基本对齐方式等于变量在使用std140布局规则的统一块中占用的空间(包括填充)。对于每个变量,我们计算其对齐偏移量:变量从块开始的字节偏移量。变量的对齐字节偏移量必须等于其基对齐的倍数。这有点拗口,但我们很快就会看到一些例子来澄清问题。

确切的布局规则可以在OpenGL的统一缓冲区规范中找到,但是我们将在下面列出最常见的规则。GLSL中的每个变量类型,如int、float和bool,都被定义为四个字节的数量,每个实体的4个字节用N表示。

TypeLayout rule
Scalar e.g. int or boolEach scalar has a base alignment of N.
VectorEither 2N or 4N. This means that a vec3 has a base alignment of 4N.
Array of scalars or vectorsEach element has a base alignment equal to that of a vec4.
MatricesStored as a large array of column vectors, where each of those vectors has a base alignment of vec4.
StructEqual to the computed size of its elements according to the previous rules, but padded to a multiple of the size of a vec4.

像大多数OpenGL规范一样,通过示例更容易理解。我们采用前面介绍过的叫做ExampleBlock的统一块,并使用std140布局计算其每个成员的对齐偏移量:


layout (std140) uniform ExampleBlock
{
                     // base alignment  // aligned offset
    float value;     // 4               // 0 
    vec3 vector;     // 16              // 16  (offset must be multiple of 16 so 4->16)
    mat4 matrix;     // 16              // 32  (column 0)
                     // 16              // 48  (column 1)
                     // 16              // 64  (column 2)
                     // 16              // 80  (column 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等函数在适当的偏移量处填充缓冲区。虽然不是最有效的,但std140布局确实保证了在声明这个统一块的每个程序上,内存布局保持不变。

通过在统一块的定义中添加语句布局(std140),我们告诉OpenGL这个统一块使用std140布局。还有另外两个布局需要我们在填充缓冲区之前查询每个偏移量。我们已经看到了共享布局,其他剩余布局被打包。当使用包装布局,没有保证布局保持相同之间的程序(不共享),因为它允许编译器优化统一变量从统一块可能不同的着色器。

Using uniform buffers

我们已经定义了统一块并指定了它们的内存布局,但是我们还没有讨论如何实际使用它们。

首先,我们需要创建一个统一的缓冲区对象,这是通过熟悉的glGenBuffers来完成的。一旦有了缓冲区对象,我们就将其绑定到GL_UNIFORM_BUFFER目标,并通过调用glBufferData来分配足够的内存。


unsigned int uboExampleBlock;
glGenBuffers(1, &uboExampleBlock);
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW); // allocate 152 bytes of memory
glBindBuffer(GL_UNIFORM_BUFFER, 0);

现在,无论何时我们想要更新或向缓冲区中插入数据,我们都绑定到uboExampleBlock并使用glBufferSubData来更新它的内存。我们只需要更新一次这个统一的缓冲区,所有使用这个缓冲区的着色器现在使用它更新的数据。但是,OpenGL如何知道哪些统一缓冲区对应哪些统一块?

在OpenGL上下文中,定义了许多绑定点,我们可以将统一缓冲区链接到这些绑定点上。一旦我们创建了一个统一的缓冲区,我们就将它链接到其中一个绑定点,同时我们也将着色器中的统一块链接到同一个绑定点,有效地将它们链接在一起。下图说明了这一点:

如您所见,我们可以将多个统一缓冲区绑定到不同的绑定点。因为shader A和shader B都有一个连接到同一个绑定点0的统一块,它们的统一块共享uboMatrices中发现的相同的统一数据;一个要求是,两个着色器定义相同的矩阵统一块。

要将一个着色器统一块设置为一个特定的绑定点,我们称之为glUniformBlockBinding,它接受一个程序对象、统一块索引和要链接到的绑定点。统一块索引是在着色器中定义的统一块的位置索引。这可以通过调用glGetUniformBlockIndex来检索,该调用接受一个程序对象和统一块的名称。我们可以将图中的灯光均匀块设置为绑定点2如下:


unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");   
glUniformBlockBinding(shaderA.ID, lights_index, 2);

注意,我们必须对每个着色器重复这个过程。

从OpenGL 4.2版本开始,通过添加另一个布局说明符,也可以在着色器中显式地存储统一块的绑定点,节省了我们对glGetUniformBlockIndex和glUniformBlockBinding的调用。下面的代码显式设置了light统一块的绑定点:


layout(std140, binding = 2) uniform Lights { ... };

然后,我们还需要将统一的缓冲区对象绑定到相同的绑定点,这可以通过glBindBufferBase或glBindBufferRange来完成。


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

函数glBindbufferBase需要一个目标、一个绑定点索引和一个统一的缓冲区对象。该函数将uboExampleBlock链接到绑定点2;从这一点开始,结合点的两边都连接起来了。您还可以使用需要额外偏移量和大小参数的glBindBufferRange—通过这种方式,您可以仅将统一缓冲区的特定范围绑定到一个绑定点。使用glBindBufferRange,您可以将多个不同的统一块链接到单个统一缓冲区对象。

现在一切都设置好了,我们可以开始向统一缓冲区添加数据了。我们可以将所有数据作为单个字节数组添加,或者在任何需要时使用glBufferSubData更新缓冲区的部分。要更新uniform变量布尔值,我们可以更新uniform缓冲区对象如下:


glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // bools in GLSL are represented as 4 bytes, so we store it in an integer
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b); 
glBindBuffer(GL_UNIFORM_BUFFER, 0);

同样的过程也适用于统一块内的所有其他统一变量,但具有不同的范围参数。

A simple example

让我们来演示一个统一缓冲区对象的真实示例。如果我们回顾之前所有的代码示例,我们一直在使用3个矩阵:投影矩阵、视图矩阵和模型矩阵。在所有这些矩阵中,只有模型矩阵经常变化。如果我们有多个着色器使用相同的矩阵集合,我们可能会更好地使用统一的缓冲对象。

用统一的缓冲对象,我们要将投影与视图矩阵存储在一个均匀的块矩阵。我们不会把模型矩阵存储在那里,因为模型矩阵往往会在着色器之间频繁变化,所以我们不会真正受益于统一的缓冲对象。


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

这里没有做太多,除了我们现在使用带有std140布局的统一块。在我们的示例应用程序中,我们将要做的是显示4个立方体,每个立方体用不同的着色程序显示。每一个着色程序使用相同的顶点着色器,但有一个唯一的片段着色器,它只输出单一的颜色,不同的着色器。

首先,我们将顶点着色器的统一块设置为绑定点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);

接下来,我们创建实际的统一缓冲区对象,并将该缓冲区绑定到绑定点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));

首先,我们为缓冲区分配了足够的内存,它等于glm::mat4大小的2倍。GLM矩阵类型的大小直接对应于GLSL中的mat4。然后我们将缓冲区的一个特定范围(在本例中是整个缓冲区)链接到绑定点0。

现在剩下要做的就是填充缓冲区。如果我们保持投影矩阵的视场值不变(这样就不会有相机变焦),我们只需要在应用程序中更新它一次——这意味着我们也只需要将它插入缓冲区一次。因为我们已经在缓存对象中分配了足够的内存,我们可以在进入渲染循环之前使用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);  

这就是均匀缓冲对象的情况。每个顶点着色器包含一个矩阵统一块现在将包含数据存储在uboMatrices。所以如果我们现在用4种不同的着色器绘制4个立方体,它们的投影和视图矩阵应该是相同的:


glBindVertexArray(cubeVAO);
shaderRed.use();
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f));	// move top-left
shaderRed.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);        
// ... draw Green Cube
// ... draw Blue Cube
// ... draw Yellow Cube	  

我们还需要设置的唯一制服是模型制服。在这样的场景中使用统一的缓冲区对象可以节省我们对每个着色器的多次统一调用。结果是这样的:

通过转换模型矩阵,每个立方体被移动到窗口的一边,多亏了不同的碎片着色器,它们的颜色每个物体都不同。这是一个相对简单的场景,我们可以使用统一的缓冲区对象,但任何大型渲染应用程序可以有超过数百个着色程序的活动,这是统一的缓冲区对象真正开始发光。

您可以在这里here. 找到统一示例应用程序的完整源代码。

统一缓冲区对象比单一统一有几个优点。首先,一次设置很多制服比一次设置多个制服要快。第二,如果你想在几个着色器上改变相同的制服,在一个统一的缓冲区中改变一次制服会容易得多。最后一个不太明显的优势是,你可以在着色器中使用更多的制服,使用统一的缓冲对象。OpenGL对它能处理多少统一数据有限制,这些数据可以通过GL_MAX_VERTEX_UNIFORM_COMPONENTS查询。当使用统一缓冲区对象时,这个限制要高得多。因此,当你达到制服的最大数量时(例如在做骨骼动画时),总会有统一的缓冲对象。

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值