OpenGL 4 Shading Language Cookbook(Second Edition)
上QQ阅读APP看书,第一时间看更新

Using uniform blocks and uniform buffer objects

If your program involves multiple shader programs that use the same uniform variables, one has to manage the variables separately for each program. Uniform locations are generated when a program is linked, so the locations of the uniforms may change from one program to the next. The data for those uniforms may have to be regenerated and applied to the new locations.

Uniform blocks were designed to ease the sharing of uniform data between programs. With uniform blocks, one can create a buffer object for storing the values of all the uniform variables, and bind the buffer to the uniform block. When changing programs, the same buffer object need only be re-bound to the corresponding block in the new program.

A uniform block is simply a group of uniform variables defined within a syntactical structure known as a uniform block. For example, in this recipe, we'll use the following uniform block:

uniform BlobSettings {
  vec4 InnerColor;
  vec4 OuterColor;
  float RadiusInner;
  float RadiusOuter;
};

This defines a block with the name BlobSettings that contains four uniform variables. With this type of block definition, the variables within the block are still part of the global scope and do not need to be qualified with the block name.

The buffer object used to store the data for the uniforms is often referred to as a uniform buffer object. We'll see that a uniform buffer object is simply just a buffer object that is bound to a certain location.

For this recipe, we'll use a simple example to demonstrate the use of uniform buffer objects and uniform blocks. We'll draw a quad (two triangles) with texture coordinates, and use our fragment shader to fill the quad with a fuzzy circle. The circle is a solid color in the center, but at its edge, it gradually fades to the background color, as shown in the following image:

Using uniform blocks and uniform buffer objects

Getting ready

Start with an OpenGL program that draws two triangles to form a quad. Provide the position at vertex attribute location 0, and the texture coordinate (0 to 1 in each direction) at vertex attribute location 1 (see the Sending data to a shader using vertex attributes and vertex buffer objects recipe).

We'll use the following vertex shader:

#version 430

layout (location = 0) in vec3 VertexPosition;
layout (location = 1) in vec3 VertexTexCoord;

out vec3 TexCoord;

void main()
{
  TexCoord = VertexTexCoord;
  gl_Position = vec4(VertexPosition,1.0);
}

The fragment shader contains the uniform block, and is responsible for drawing our fuzzy circle:

#version 430

in vec3 TexCoord;
layout (location = 0) out vec4 FragColor;

layout (binding = 0) uniform BlobSettings {
  vec4 InnerColor;
  vec4 OuterColor;
  float RadiusInner;
  float RadiusOuter;
};

void main() {
  float dx = TexCoord.x - 0.5;
  float dy = TexCoord.y - 0.5;
  float dist = sqrt(dx * dx + dy * dy);
  FragColor =mix( InnerColor, OuterColor,smoothstep( RadiusInner, RadiusOuter, dist ));
}

Note the uniform block named BlobSettings. The variables within this block define the parameters of our fuzzy circle. The variable OuterColor defines the color outside of the circle. InnerColor is the color inside of the circle. RadiusInner is the radius defining the part of the circle that is a solid color (inside the fuzzy edge), and the distance from the center of the circle to the inner edge of the fuzzy boundary. RadiusOuter is the outer edge of the fuzzy boundary of the circle (when the color is equal to OuterColor).

The code within the main function computes the distance of the texture coordinate to the center of the quad located at (0.5, 0.5). It then uses that distance to compute the color by using the smoothstep function. This function provides a value that smoothly varies between 0.0 and 1.0 when the value of the third argument is between the values of the first two arguments. Otherwise it returns 0.0 or 1.0 depending on whether dist is less than the first or greater than the second, respectively. The mix function is then used to linearly interpolate between InnerColor and OuterColor based on the value returned by the smoothstep function.

How to do it...

In the OpenGL program, after linking the shader program, use the following steps to assign data to the uniform block in the fragment shader:

  1. Get the index of the uniform block using glGetUniformBlockIndex.
    GLuint blockIndex = glGetUniformBlockIndex(programHandle, "BlobSettings");
  2. Allocate space for the buffer to contain the data for the uniform block. We get the size using glGetActiveUniformBlockiv:
    GLint blockSize;
    glGetActiveUniformBlockiv(programHandle, blockIndex,GL_UNIFORM_BLOCK_DATA_SIZE, &blockSize);
    
    GLubyte * blockBuffer;
    blockBuffer = (GLubyte *) malloc(blockSize);
  3. Query for the offset of each variable within the block. To do so, we first find the index of each variable within the block:
    const GLchar *names[] = { "InnerColor", "OuterColor","RadiusInner", "RadiusOuter" };
    GLuint indices[4];
    glGetUniformIndices(programHandle, 4, names, indices);
    
    GLint offset[4];
    glGetActiveUniformsiv(programHandle, 4, indices, GL_UNIFORM_OFFSET, offset);
  4. Place the data into the buffer at the appropriate offsets:
    // Store data within the buffer at the appropriate offsets
    GLfloat outerColor[] = {0.0f, 0.0f, 0.0f, 0.0f};
    GLfloat innerColor[] = {1.0f, 1.0f, 0.75f, 1.0f};
    GLfloat innerRadius = 0.25f, outerRadius = 0.45f;
    
    memcpy(blockBuffer + offset[0], innerColor, 4 * sizeof(GLfloat));
    memcpy(blockBuffer + offset[1], outerColor, 4 * sizeof(GLfloat));
    memcpy(blockBuffer + offset[2], &innerRadius, sizeof(GLfloat));
    memcpy(blockBuffer + offset[3], &outerRadius, sizeof(GLfloat));
  5. Create the buffer object and copy the data into it:
    GLuint uboHandle;
    glGenBuffers( 1, &uboHandle );
    glBindBuffer( GL_UNIFORM_BUFFER, uboHandle );
    glBufferData( GL_UNIFORM_BUFFER, blockSize, blockBuffer, GL_DYNAMIC_DRAW );
  6. Bind the buffer object to the uniform buffer binding point at the index specified by the binding layout qualifier in the fragment shader (0):
    glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboHandle);

How it works...

Phew! This seems like a lot of work! However, the real advantage comes when using multiple programs where the same buffer object can be used for each program. Let's take a look at each step individually.

First we get the index of the uniform block by calling glGetUniformBlockIndex, then we query for the size of the block by calling glGetActiveUniformBlockiv. After getting the size, we allocate a temporary buffer named blockBuffer to hold the data for our block.

The layout of data within a uniform block is implementation dependent, and implementations may use different padding and/or byte alignment. So in order to accurately layout our data, we need to query for the offset of each variable within the block. This is done in two steps. First, we query for the index of each variable within the block by calling glGetUniformIndices. This accepts an array of variable names (third argument) and returns the indices of the variables in the array indices (fourth argument). Then we use the indices to query for the offsets by calling glGetActiveUniformsiv. When the fourth argument is GL_UNIFORM_OFFSET, this returns the offset of each variable in the array pointed to by the fifth argument. This function can also be used to query for the size and type, however, in this case we choose not to do so, to keep the code simple (albeit less general).

The next step involves filling our temporary buffer blockBuffer with the data for the uniforms at the appropriate offsets. Here we use the standard library function memcpy to accomplish this.

Now that the temporary buffer is populated with the data with the appropriate layout, we can create our buffer object and copy the data into the buffer object. We call glGenBuffers to generate a buffer handle, and then bind that buffer to the GL_UNIFORM_BUFFER binding point by calling glBindBuffer. The space is allocated within the buffer object and the data is copied when glBufferData is called. We use GL_DYNAMIC_DRAW as the usage hint here because uniform data may be changed somewhat often during rendering. Of course, this is entirely dependent on the situation.

Finally, we associate the buffer object with the uniform block by calling glBindBufferBase. This function binds to an index within a buffer binding point. Certain binding points are also so-called "indexed buffer targets". This means that the target is actually an array of targets, and glBindBufferBase allows us to bind to one index within the array. In this case, we bind it to the index that we specified in the layout qualifier in the fragment shader: layout (binding = 0) (see the "Getting ready…" section). These two indices must match.

Note

You might be wondering why we use glBindBuffer and glBindBufferBase with GL_UNIFORM_BUFFER. Aren't these the same binding points used in two different contexts? The answer is that the GL_UNIFORM_BUFFER point can be used in each function with a slightly different meaning. With glBindBuffer, we bind to a point that can be used for filling or modifying a buffer, but can't be used as a source of data for the shader. When we use glBindBufferBase, we are binding to an index within a location that can be directly sourced by the shader. Granted, that's a bit confusing.

There's more...

If the data for a uniform block needs to be changed at some later time, one can call glBufferSubData to replace all or part of the data within the buffer. If you do so, don't forget to first bind the buffer to the generic binding point GL_UNIFORM_BUFFER.

Using an instance name with a uniform block

A uniform block can have an optional instance name. For example, with our BlobSettings block we could have used the instance name Blob, as shown here:

uniform BlobSettings {
  vec4 InnerColor;
  vec4 OuterColor;
  float RadiusInner;
  float RadiusOuter;
} Blob;

In this case, the variables within the block are placed within a namespace qualified by the instance name. Therefore our shader code needs to refer to them prefixed with the instance name. For example:

FragColor =mix( Blob.InnerColor, Blob.OuterColor,smoothstep( Blob.RadiusInner, Blob.RadiusOuter, dist )
  );

Additionally, we need to qualify the variable names (with the block name: BlobSettings) within the OpenGL code when querying for variable indices:

const GLchar *names[] = { "BlobSettings.InnerColor", 
      "BlobSettings.OuterColor", "BlobSettings. RadiusInner",
      "BlobSettings.RadiusOuter" };
GLuint indices[4];
glGetUniformIndices(programHandle, 4, names, indices);

Using layout qualifiers with uniform blocks

Since the layout of the data within a uniform buffer object is implementation dependent, it required us to query for the variable offsets. However, one can avoid this by asking OpenGL to use the standard layout std140. This is accomplished by using a layout qualifier when declaring the uniform block. For example:

layout( std140 ) uniform BlobSettings {
  
};

The std140 layout is described in detail within the OpenGL specification document (available at http://www.opengl.org).

Other options for the layout qualifier that apply to uniform block layouts include packed and shared. The packed qualifier simply states that the implementation is free to optimize memory in whatever way it finds necessary (based on variable usage or other criteria). With the packed qualifier, we still need to query for the offsets of each variable. The shared qualifier guarantees that the layout will be consistent between multiple programs and program stages provided that the uniform block declaration does not change. If you are planning to use the same buffer object between multiple programs and/or program stages, it is a good idea to use the shared option.

There are two other layout qualifiers that are worth mentioning: row_major and column_major. These define the ordering of data within the matrix type variables within the uniform block.

One can use multiple (non-conflicting) qualifiers for a block. For example, to define a block with both the row_major and shared qualifiers, we would use the following syntax:

layout( row_major, shared ) uniform BlobSettings {
  
};

See also

  • The Sending data to a shader using uniform variables recipe