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 re-generated 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. Then 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.
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: Sending data to a shader using per-vertex attributes and vertex buffer objects).
We'll use the following vertex shader:
#version 400 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 400 in vec3 TexCoord; layout (location = 0) out vec4 FragColor; 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 ) ); }
The uniform block is 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 it 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 send data to the uniform block in the fragment shader:
Get the index of the uniform block using
glGetUniformBlockIndex
.GLuint blockIndex = glGetUniformBlockIndex(programHandle, "BlobSettings");
Allocate space for the buffer to contain the data for the uniform block. We get the size of the block using
glGetActiveUniformBlockiv
.GLint blockSize; glGetActiveUniformBlockiv(programHandle, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &blockSize); GLubyte * blockBuffer= (GLubyte *) malloc(blockSize);
Query for the offset of each variable within the block. To do so, we first find the index of each variable within the block.
// Query for the offsets of each block variable 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);
Place the data into 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));
Create the OpenGL 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 );
Bind the buffer object to the uniform block.
glBindBufferBase( GL_UNIFORM_BUFFER, blockIndex, 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.
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 within the OpenGL code when querying for variable indices. The OpenGL specification says that they must be qualified with the block name (BlobSettings
). However, my tests using the ATI Catalyst (10.8) drivers required me to use the instance name (Blob
).
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 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
Sending data to a shader using uniform variables