Tutorial 7: Uniforms
Uniforms are variables that can be passed from a program running on the CPU, to a shader program running on the GPU.
More recent versions of OpenGL have deprecated the default matrix stack/operations, so instead the modelview matrix must be manually setup and passed to the vertex shader as a uniform variable.
Exercises:
In this tutorial:
- Learn to use uniform variables
- Pass the modelview/normal matrices to the vertex shader
- Transform vertices and normals into eye space
Using uniform variables
Uniform variables are passed to shaders using the appropriate glUniform function. Uniform variables can be passed as a single value, as a 2/3/4 component vector, or as a matrix.
With your code from last week, use the following vertex/fragment shaders (ignore lighting for now):
// shader.vert
void main(void)
{
vec4 esVert = gl_Vertex;
vec4 csVert = gl_ProjectionMatrix * esVert;
gl_Position = csVert;
}
// shader.frag
uniform vec3 color;
void main(void)
{
gl_FragColor = vec4(color, 1);
}
- Here the fragment shader expects
color
to be passed to it as auniform vec3
variable, this needs to be done within the program that is using the shader. To do this the program first needs a handle to the uniform variable (its location). After the shader has been created, bind it, and call the functionglGetUniformLocation(shaderProgram, "color")
. This function will return aGLint
that gives the location of the variable with the namecolor
. If the function failed it will return -1. - After getting the location, the value can be set using the function
glUniform3f(location, 1, 0, 0)
, note that thecolor
variable will keep this value until it is changed to something else (even if the shader is unbound and re-bound later). Confirm that this sets the color to red. Try changing the values and make sure the new color matches. - The color could be passed to the vertex shader instead, however to use this in the fragment shader, it would need to be sent to it as a separate varying variable.
Here are shaders that show an example of this:
// shader.vert
uniform vec3 uColor;
varying vec3 vColor;
void main(void)
{
vec4 esVert = gl_Vertex;
vec4 csVert = gl_ProjectionMatrix * esVert;
gl_Position = csVert;
vColor = uColor;
}
// shader.frag
varying vec3 vColor;
void main(void)
{
gl_FragColor = vec4(vColor, 1);
}
Passing modelview matrix as uniform
Now using the lighting shader from last week (or the color shader above), we will pass the modelview matrix to the vertex shader so that we can transform the vertex to eye space coordinates on the GPU instead of on the CPU.
- Declare a uniform variable at the top of the vertex shader as
uniform mat4 mvMat
. - Using the technique described above, get the location of
mvMat
once insidedisplayMultiView()
, and once insidedisplay()
(or alternatively, once after creating the shader, and storing the value in a global variable). Its value will be -1 since it will be compiled out of the shader until used. - Now each time modelViewMatrix is changed inside
displayMultiView()
anddisplay()
, call the functionglUniformMatrix4fv(mvMatLoc, 1, false, &modelViewMatrix[0][0])
to set it.
Converting to eye space
Transforming vertices using matrices is typically much faster (and easier) to do on the GPU - due to its parallelism - than on the CPU, which is why converting vertices to eye space is usually done in the vertex shader rather than on the CPU in the application program.
- Currently the program converts vertices to eye space inside the function
drawSineWave
. Each time the value ofrEC
is calculated, remove themodelViewMatrix
multiplication. - Now perform the calculation inside the shader, as
vec4 esVert = mvMat * gl_Vertex
. - Confirm that the program still draws the sine wave correctly.
Passing normal matrix
Just as the modelview matrix is used to transform vertices to eye space, the normal matrix is used to transform normals for lighting calculations. The normal matrix can be thought of as the modelview matrix, with translation information removed, and then corrected for any scaling applied. This can be calculated as the transposed inverse of the modelview matrix and is already being done and stored in a 3x3 matrix called normalMatrix
in the application program.
- Declare another uniform variable at the top of the vertex shader as
uniform mat3 nMat
. - Once again, get the location of
nMat
and set it using the functionglUniformMatrix3fv
just as was done with the modelview matrix. - Change the calculation of
nEC
insidedrawSineWave
to remove the use ofnormalMatrix
. - Now convert the normals into eye space inside the vertex shader using
nMat * normalize(gl_Normal)
and confirm the program still draws the sine wave correctly. The normals are used in lighting calculations, and if done correctly there should be no visible difference between doing them on the CPU or the GPU. If simply setting the colour to red then the normals are unused.
Extra: Calculating sine wave (to be continued next week)
- The sine wave calculations can also be performed inside the vertex shader instead of on the CPU. This means that only the
x
andz
vertex positions need to be set, and they
coordinate can be left out or simply set to 0.0. Try moving the calculation of they
position of the vertices from the application program to the vertex shader. - After doing this the normal vectors will also need to be calculated directly on the GPU. This means that they no longer need to be passed to the GPU.
Extra: Projection Matrix (to be continued next week)
You can also pass to and use the projection matrix in the vertex shader in the same way as the modelview matrix. This will have the effect of transforming vertices from eye space to clip space. Currently the program calls glOrtho
to set the projection matrix, which is used in the vertex shader as gl_ProjectionMatrix
.
- To create the projection matrix call the function
glm::ortho(-1.0, 1.0, -1.0, 1.0, -100.0, 100.0)
and store this in aglm::mat4
. - Pass this matrix to the vertex shader and use it instead of
gl_ProjectionMatrix
.