OpenGL-GLSL language getting started tutorial (2)

table of Contents

Interface block

in/out块

uniform block

Uniform Buffer Objects

Uniform block layout

Use Uniform buffer

A simple example

buffer block


Reference website: LearnOpenGL

Reference books: OpenGL Programming Guide Ninth Edition

Interface block

So far, whenever we want to send data from the vertex shader to the fragment shader, we declare a corresponding number of input / output variables. Them one by one statement is to send data between shader easiest way, but when the problem grows bigger, you might want to send not just a few variables, it may also include arrays and structures .

To help us manage these variables, GLSL provides something called interface block (Interface Block) for us, to help us combination of these variables. And interface block declaration statement struct somewhat similar, except that now it is in accordance with an input or output block (Block), in or out using the defined key.

uniform uniform variable block may be used, the input and output variables in and out block may be used, the buffer memory can be used shaders buffer block.

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

This time we declare a called vs_out interface block, which we want to send a package to all output variables under a shader. This is just a very simple example, but you can imagine, it can help you manage shader inputs and outputs. When we want to input or output shader packaged as an array, it will be very useful.

After that, we also need a next shader, i.e. a fragment shader, define an input interface block. Block name (Block Name) should be the same and the shader (VS_OUT), but an instance name (Instance Name) (used in the vertex shader is VS_OUT) can be random, but avoid the use of misleading name, such as an input interface block for actually contains the variable named 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);   
}

As long as the names of the two interfaces of the blocks, which correspond to the input and output will match up. This is another useful feature to help you manage your code, so it is interspersed with a particular shader stage scene can be useful in the geometry shader.

uniform block

Uniform Buffer Objects

When using more than one shader, although most of the uniform variables are the same, we still need to continue to set them, so why bother to set them to repeat it?

OpenGL provides a tool called Uniform buffer object (Uniform Buffer Object) for us, it allows us to define a series of the same in the plurality of shader global Uniform variables . When using the Uniform buffer objects, we only need to set the relevant uniform once . Of course, we still need to set each shader in a different uniform manual. And create and configure Uniform buffer object will be a little tedious.

Because the Uniform buffer objects is still a buffer, we can use glGenBuffers to create it, bind it to GL_UNIFORM_BUFFER buffer target, and store all relevant uniform data buffering. Uniform data stored in the buffer object is there are some rules, we will discuss it after. First, we will use a simple vertex shader, and the projection matrix, to view a so-called Uniform blocks (Uniform Block) in which:

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

In most of our cases, we will render in each iteration, set and view Uniform projection matrix for each shader. This is a perfect example of using the Uniform buffer objects, because now we only need to store these matrices it once.

Here, we declare called Matrices of Uniform block, which stores two 4x4 matrix. Uniform block variables can be accessed directly, no need to add the block name as a prefix . Next, we deposited the buffer values in the matrix in the OpenGL codes, each declaration block this Uniform shader can access these matrices.

Note: uniform variable naming restrictions will not be block, then an error occurs at compile time when the same variables are declared in uniform blocks of different names.

Now you might think layout (std140)this statement is what it means. It means that, Uniform defined block of the current contents of its memory using a particular layout. This statement has Uniform block layout (Uniform Block Layout).

Uniform block layout

Uniform content block is stored in a buffer object, it is actually just one reserved memory . Because this memory will not save it save what specific type of data, we also need to tell OpenGL which part corresponds to the memory of the shader which one uniform variable .

Suppose shader Uniform blocks have the following:

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

We need to know that each variable size (in bytes) , and (from the start of the block) offset , in order to be able to let them into the buffer. Size of each element are all clearly stated in the OpenGL, and C ++ data types correspond directly, wherein the vector and matrix are large float array. OpenGL is not declared between these variables spacing (Spacing) . This allows the hardware can be placed in its proper position that variable. For example, some hardware might be placed in a vec3 float edge. Not all hardware can otherwise might be before you attach the float, first vec3 filling (Pad) is a four array of float. This feature itself is great, but we will cause trouble.

By default, the GLSL Uniform uses shared memory called a layout (the Shared) layout, because once the shared hardware defines the offset in which a plurality of programs are shared and consistent. When using a shared layout, GLSL can be carried out changes in the position of uniform variables to optimize, as long as the order of the variables remain constant. Because we can not know the offset of each uniform variable, and we do not know how to fill our Uniform cushioned accurately.

While sharing arrangement gives us a lot of optimization to save space, but we need to check the offset of each uniform variable, which results in a lot of effort. The usual practice is, does not use shared layout, but the use of std140 layout. std140 declared offset layout of each variable is determined by a set of rules, which explicitly declares the type of each variable memory layout. Since this is explicitly mentioned, we can manually calculate the offset of each variable.

Layout qualifier description
binding=N Set the binding position of the cache. Need to use the OpenGL API
shared Setting a uniform block is shared among a plurality of programs (default layout)
packed Setting uniform block occupies minimal memory space, but this will prohibit shared among programs that block
std140 Standard layout after version 1.4
std430 Standard layout after version 4.3
offset=N Forcibly set of member variables in the cache at byte offset N
align=N Forcibly set member variable offset location is a multiple of N
row_major Use row major way to store uniform block matrix
column_major Column using a main sequence memory matrix manner (default) uniform block

Each variable has a reference alignment amount (the Alignment Base) , which is equal to a variable space occupied by the Uniform block (including filling amount (Padding)), the reference amount is a usage rule std140 alignment layout calculated. Next, for each variable, we then calculate its alignment offset (Aligned Offset), which is a variable from the start of the block byte offset. Alignment byte offset of a variable to be equal to the reference amount of aligned fold .

The original layout rules can be buffered in Uniform OpenGL specification of where to find, but we will list the most common rules below. GLSL each variable, for example, int, float, and BOOL, is defined as 4-byte quantity . Each one will use 4 bytes Nto represent.

Types of Layout rules
Scalar, such as int and bool Each alignment reference scalar quantity is N.
vector 2N or 4N. This means vec3 alignment reference amount for 4N.
Array of scalar or vector Aligned by the amount of each element is the same as vec4.
matrix Stored as an array of column vectors, each vector is aligned by an amount of the same vec4.
Structure The size of all elements equals the rule calculation, but will be filled into multiple vec4 size.

And most of the OpenGL specification, the use of examples can be more easily understood. We will use the previously introduced is called the Uniform ExampleBlock blocks, using the calculated layout std140 alignment offset for each member:

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

As an exercise, try to calculate their own offsets, and tables and compared. After using the calculated offset value, according to the layout rules std140, we can use such glBufferSubData function in accordance with the variable data offset filled into the buffer. Although std140 layout is not the most efficient layout, but it ensures the memory layout of each statement this program Uniform block is the same.

By adding before Uniform block definition layout (std140)statements, we tell OpenGL this Uniform block using std140 layout. In addition you can also choose two layouts, but they all require us to query each offset before filling the buffer. We've seen sharedthe layout, and the rest of a layout packed. When a compact (Packed) layout, the layout is not guaranteed to remain unchanged (i.e., non-shared) in each program, because it allows the compiler to uniform variable optimized away from Uniform blocks, in which each of the colored vessel is likely to be different.

Use Uniform buffer

We have discussed how to define a shader Uniform block, and set the layout of their memory, but we do not discuss how to use them.

First of all, we need to call glGenBuffers , create a Uniform buffer object. Once we have a buffer object, we need to bind it to GL_UNIFORM_BUFFER target, and calls glBufferData , allocate enough memory.

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

Now, whenever we need to update or insert data buffer, we will be bound to uboExampleBlock, and use glBufferSubData to update its memory. We only need to update the Uniform buffer once , all shaders using this buffer is to use the updated data. But how can let OpenGL know what Uniform Uniform buffer block which corresponds to what is it?

In OpenGL context, the definition of a number of binding points (the Binding Point) , we can be a Uniform buffer link to it. After creating Uniform buffer, it will be bound to one binding site and bind shader Uniform block binding to the same point, they are joined together. The following diagram illustrates this:

Uniform buffer and binding point

You can see that we can bind multiple Uniform buffer to a different binding points. Because shaders A and B has a link shader to block binding point 0 Uniform, their Uniform uniform block will share the same data, uboMatrices, with the proviso that two shaders are defined the same block of the Matrices Uniform .

In order to block the Uniform bound to a specific binding points, we need to call glUniformBlockBinding function whose first argument is a program object, followed by a Uniform block index and link to the binding points. Uniform block index (Uniform Block Index) is an index position value shader Uniform blocks defined. This can be obtained by calling glGetUniformBlockIndex, it takes an object and the name of the Uniform program block. We can use the following manner Lights Uniform linked to the block diagram in binding point 2:

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

 Note that we need for each to repeat this step shaders.

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

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

 glBindbufferBase需要一个目标,一个绑定点索引和一个Uniform缓冲对象作为它的参数。这个函数将uboExampleBlock链接到绑定点2上,自此,绑定点的两端都链接上了。你也可以使用glBindBufferRange函数,它需要一个附加的偏移量和大小参数,这样子你可以绑定Uniform缓冲的特定一部分到绑定点中。通过使用glBindBufferRange函数,你可以让多个不同的Uniform块绑定到同一个Uniform缓冲对象上。

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

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

同样的步骤也能应用到Uniform块中其它的uniform变量上,但需要使用不同的范围参数。 

一个简单的例子

所以,我们来展示一个真正使用Uniform缓冲对象的例子。如果我们回头看看之前所有的代码例子,我们不断地在使用3个矩阵:投影、观察和模型矩阵。在所有的这些矩阵中,只有模型矩阵会频繁变动。如果我们有多个着色器使用了这同一组矩阵,那么使用Uniform缓冲对象可能会更好。

我们会将投影和观察矩阵存储到一个叫做Matrices的Uniform块中。我们不会将模型矩阵存在这里,因为模型矩阵在不同的着色器中会不断改变,所以使用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);
}

这里没什么特别的,除了我们现在使用的是一个std140布局的Uniform块。我们将在例子程序中,显示4个立方体,每个立方体都是使用不同的着色器程序渲染的。这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缓冲对象本身,并将其绑定到绑定点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大小的两倍。GLM矩阵类型的大小直接对应于GLSL中的mat4。接下来,我们将缓冲中的特定范围(在这里是整个缓冲)链接到绑定点0。

剩余的就是填充这个缓冲了。如果我们将投影矩阵的视野(Field of View)值保持不变(所以摄像机就没有缩放了),我们只需要将其在程序中定义一次——这也意味着我们只需要将它插入到缓冲中一次。因为我们已经为缓冲对象分配了足够的内存,我们可以使用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);

这里我们将投影矩阵储存在Uniform缓冲的前半部分。在每次渲染迭代中绘制物体之前,我们会将观察矩阵更新到缓冲的后半部分:

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

Uniform缓冲对象的部分就结束了。每个包含了Matrices这个Uniform块的顶点着色器将会包含储存在uboMatrices中的数据。所以,如果我们现在要用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调用。最终的结果会是这样的:

使用Buffer

因为修改了模型矩阵,每个立方体都移动到了窗口的一边,并且由于使用了不同的片段着色器,它们的颜色也不同。这只是一个很简单的情景,我们可能会需要使用Uniform缓冲对象,但任何大型的渲染程序都可能同时激活有上百个着色器程序,这时候Uniform缓冲对象的优势就会很大地体现出来了。

Uniform缓冲对象比起独立的uniform有很多好处。第一,一次设置很多uniform会比一个一个设置多个uniform要快很多。第二,比起在多个着色器中修改同样的uniform,在Uniform缓冲中修改一次会更容易一些。最后一个好处可能不会立即显现,如果使用Uniform缓冲对象的话,你可以在着色器中使用更多的uniform。OpenGL限制了它能够处理的uniform数量,这可以通过GL_MAX_VERTEX_UNIFORM_COMPONENTS来查询。当使用Uniform缓冲对象时,最大的数量会更高。所以,当你达到了uniform的最大数量时(比如再做骨骼动画(Skeletal Animation)的时候),你总是可以选择使用Uniform缓冲对象。

全部代码下载

我的网盘
提取码:waxk

buffer块

就是着色器的存储缓存对象。
常见特性如下:
1.不仅可以被应用程序进行修改,而且还可以被着色器程序修改。
2.可以在渲染之前决定buffer接口块大小。
3.只能4.3以上版本使用(OpenGL编程指南第九版中文版53页写的只能是std430布局,错误)。

对于特性2,举例代码如下:

buffer BlockName
{
    float value;
    vec3  points[];//未定义大小
}ShaderBlockName;

由于笔者的OpenGL版本没有到达4.3及以上,不做测验了。

更多OpenGL知识:现代OpenGL入门教程

有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系。

 

 

发布了163 篇原创文章 · 获赞 471 · 访问量 26万+

Guess you like

Origin blog.csdn.net/lady_killer9/article/details/89482145