In the last lab we saw how to use programmable shaders written in GLSL. Hence if we are able to get shader source code, we could implement them within our standard OpenGL code using the load_shaders() routine from that lab. The true power of modern graphics hardware, however, is unlocked by being able to write our own programmable shaders to produce any effect we desired. There are many different shader languages including GLSL, HLSL, cG, etc. We will be focusing on GLSL which is the shader language for OpenGL. Even though the command set for shaders is very limited compared to C, the structure of a shader program is very similar, e.g. semi-colon terminated statements. However, writing shaders can be substantially more difficult to do primarily because the programs are very particular about syntax and because there is not a good debugging mechanism available to help correct non-functional shader programs (since they are only executed once they have been compiled and loaded onto the graphics card). A few debugging tools are available, such as glslDevil and OpenGL Shader Builder (for Apple Developers) but it is still a challenging task to write sophisticated shaders. The primary drawback to using programmable shaders is that if you use them, you must program every feature you want implemented, e.g. lighting, blending, etc. Hence it is essential to understand all the concepts we have discussed, i.e. the mathematical implementation, in order to program it in your shaders.

0. Getting Started

Download CS370_Lab11.zip, saving it into the labs directory.

Double-click on CS370_Lab11.zip and extract the contents of the archive into a subdirectory called CS370_Lab11

Navigate into the CS370_Lab11 directory and double-click on CS370_Lab11.sln (the file with the little Visual Studio icon with the 12 on it).

If the source file is not already open in the main window, open the source files by expanding the Source Files item in the Solution Explorer window and double-clicking simpleGLSL.cpp, simplevert.vs, and simplefrag.fs.

1. Built-in Shader Data Types

When we write shaders we can use many of the data types we are familiar with in C/Java such as bool, int, float (but no double) along with identical single dimensional array and structure notation. However since the pipeline primarily performs matrix/vector operations, there are several built-in data types for vectors and matrices. Vector types have the form

[i,b]vec{2,3,4}

where [i,b] is an optional indicator of integer / boolean data type (the default is float) and {2,3,4} represents the number of components in the vector. For example

vec4 col;

would define a variable named col that is a 4x1 float vector. Matrix types (which are always square) have a similar form

mat{2,3,4}

where {2,3,4} represents the number of rows and columns in the matrix. NOTE: Matrices can only be float's and are stored column-wise (like user defined transformation matrices).

Since the variables within the shaders often refer to common graphics quantities, e.g. vertices, colors, etc., in addition to standard integer indices we can refer to the elements of a vec? variable using the structure fields - {x,y,z,w} (vertex coordinates), {r,g,b,a} (color channels), or {s,t,p,q} (texture coordinates). For example, col[2], col.z, col.b, and col.p all refer to the same value (regardless of what the vector quantity actually represents). We can also use any combination of these fields, known as swizzling, to refer to parts of a vector that we wish to manipulate, i.e. col.rb would refer to only the red (first) and blue (third) components. Furthermore, since the graphics pipeline supports vector operations (on variables with compatible dimensions), the standard arithmetic operators can also be used on vector/matrix quantities as a whole (rather than having to perform operations element by element).

Tasks

2. Shader Variable Qualifiers

Since we cannot pass arguments to the shaders, there are several qualifiers for global shader variables (i.e. ones declared outside main() in the shader source file). The shader global variables are then given values by the application and/or transferred between the vertex and fragment shaders. The four qualifiers are:

Tasks

3. Shader Variables and Shader Arithmetic

In addition to values passed to the shaders from the application (either attribute or uniform) through variables, GLSL provides many predefined variables that represent OpenGL values set in the application by OpenGL functions (see the Quick Reference handout). Some of the common ones from the application used in the vertex shader are

Some common variables passed from the vertex shader to the fragment shader are

Finally the necessary variable in the fragment shader is

Hence to use shaders properly, the vertex shader will use gl_Vertex, gl_Color, and (usually) gl_ModelViewProjectionMatrix to compute gl_Position and gl_FrontColor. Then the fragment shader may use gl_FrontColor to compute (or simply set) gl_FragColor which is the final color for the fragment in the framebuffer. There are many other variables for effects like lighting/materials, normals, texture mapping, and other useful matrices (e.g. inverse matrices, eye coordinate matrices, etc.) We will use many of these other variables to perform bump mapping in a later lab.

Tasks

4. Associating Application Values

At this point we have created both a vertex and fragment shader program. From the last lab we also have boiler plate code (load_shaders()) that will load, compile, and link these shaders into a shader program. However now that we have defined a uniform variable in the vertex shader which is passed from the application, we need to associate a variable in our application with the variable in the shader. First we must associate an application parameter which identifies the desired shader variable using

param = glGetUniformLocation(progObj, "var");

where param is a GLint that identifies the shader variable (similar to a pointer), progObj is the shader program object (see lab10), and var is the variable name in the shader source file we wish to associate (note the name is enclosed in double quotes). A similar function - glGetAttribLocation( ) - exists for associating attribute shader variables. Whenever we wish to set the value of the shader variable, we simply call

glUniform[1,2,3]f(param, val);

where param is the identifier from above, [1,2,3] selects the number of values passed, and val is the value (either a literal, application variable, or expression) to be assigned to the associated shader variable. Again there is an equivalent glVertexAttrib?f( ) function to set attribute shader variables (usually per vertex). Just like many other OpenGL functions (e.g. glVertex3?( )), there are different forms of these commands based on the type and number of components for the respective shader variables. Finally we can also pass a uniform matrix with the command

glUniformMatrix{2,3,4}f(param, num, trans, *val);

where param is the identifier, num is the number of values to change, trans is a boolean specifying whether or not to use the transpose, and val is a pointer to an array containing the values to set (recall that all shader matrices are float).

NOTE: One of the above functions must be called every time the shader variable needs to be updated, i.e. simply changing the application variable will have no effect if the attachment function is not subsequently called.

Tasks

Compiling and running the program

Once you have completed typing in the code, you can build and run the program in one of two ways:

(On Linux/OSX: In a terminal window, navigate to the directory containing the source file and simply type make. To run the program type ./simpleGLSL.exe)

The output should look similar to below

image

To quit the program simply close the window.

A complete reference for GLSL is OpenGL Shading Language by Randi Rost (a.k.a. the "Orange Book"). The reference handout along with other resources can be found at http://www.opengl.org/documentation/glsl/.

Throughout the rest of the semester, we will be implementing our effects, e.g. lighting, texture mapping, etc., in the shaders rather than using the default pipeline behavior. Initially we will simply duplicate the default pipeline behavior, but you will be able to then modify this behavior to create new effects.