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.
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.
#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);
}
#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 )
);
}
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 );
GLuint blockIndex = glGetUniformBlockIndex(programHandle,
"BlobSettings");
GLint blockSize;
glGetActiveUniformBlockiv(programHandle, blockIndex,
GL_UNIFORM_BLOCK_DATA_SIZE, &blockSize);
GLubyte * blockBuffer= (GLubyte *) malloc(blockSize);
// 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);
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));
GLuint uboHandle;
glGenBuffers( 1, &uboHandle );
glBindBuffer( GL_UNIFORM_BUFFER, uboHandle );
glBufferData( GL_UNIFORM_BUFFER, blockSize, blockBuffer,
GL_DYNAMIC_DRAW );
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).
uniform BlobSettings {
vec4 InnerColor;
vec4 OuterColor;
float RadiusInner;
float RadiusOuter;
} Blob;
FragColor =
mix( Blob.InnerColor, Blob.OuterColor,
smoothstep( Blob.RadiusInner, Blob.RadiusOuter, dist )
);
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 {
...
};
layout( std140 ) uniform BlobSettings {
...
};
layout( row_major, shared ) uniform BlobSettings {
...
};
seems missing a glUniformBlockBinding here?
GLuint blockIndex = glGetUniformBlockIndex(programHandle, ...
GLuint blockBinding = 1; // indicated by yourself and less than GL_MAX_UNIFORM_BUFFER_BINDINGS
glUniformBlockBinding(programHandle, blockIndex, blockBinding);
/// ...
glBindBufferBase( GL_UNIFORM_BUFFER, blockBinding, uboHandle ); // seccond param should be a binding point