Brief Introduction to Shaders Using GLSL
Introduction
Formatting
First, some formatting which I will try to be consistent about. For blocks of code, I'm using prettify for syntax highlighting and LaTeXMathML for mathematical formulae (currently not supported in Chrome).This is a command
This is a comment
This is the name of an application
void main() {
//This is a code block
float a_value = 0.0f;
}
Other Resources
This tutorial assumes you have the code from this Eclipse workspace. If you are trying to run this example in the Ming Ong lab, you may have to do the following inside a MinGW Shell
to get it to compile:
mingw-get update
mingw-get upgrade gcc-c++
This is because some C++11 features are used in this example and require version 4.6 or later of gcc.
For a more complete tutorial of GLSL, see the tutorial at lighthouse3d.com
The OpenGL Pipeline
Before you start using GLSL, it is important to understand the OpenGL pipeline. Let's examine the fixed pipeline first.The orange boxes are what you control via the OpenGL fixed API. For example, you transform the vertices by making calls to glTranslate, glScale, glRotate, and glMultMatrix. These API calls modify the top of the matrix stack which is managed by OpenGL. In addition, you can specify how lighting is calculated by specifying up to GL_MAX_LIGHTS number of lights and a predefined lighting model with calls to glLight and glShadeModel.
If you want to use some other lighting model other than what is specified by glLightModel, you cannot without overriding this fixed functionality. This is where the programmable pipeline comes in.
When we override the fixed pipeline, we no longer have access to the matrix stack and fixed lighting functionality. In addition, vertex color and lighting calculations must be done manually. While this may sound like a lot more work up front, most of the work is merely learning how to set your program up, and if you use a math library such as GLM, then you will have access to the methods you are already familiar with for matrix transformations (e.g. glRotate, etc.).
Figure 3

Acceptable geometry for glDrawArrays.
A Simple Example
Let's take a look at what you need to do before you can start writing your own shaders. First, the functions such as glVertex, glNormal, and glTexCoord are no longer available. So how do you specify vertices, normals and texture coorindates? The answer is by using array buffers. Generally you have a model consisting of vertices which you transform via the modelview matrix, but, once loaded, you usually do not modify the position of each vertex individually. For this reason it is usually sufficient to store all of your vertices into a GL_ARRAY_BUFFER (at least all vertices for a single model) and then draw the data in several array buffers by calling glDrawArrays. When using glDrawArrays, you must use one of the geometry types specified in [F3]. glDrawArrays is what is used in this example, but, if you want to reuse vertices by way of an index array, you can do this be using glDrawElements.Setting up the Array Buffers
Let's assume we have vertex data stored in a floating point array in the following format:mesh.verts = [x1,y1,z1,x2,y2,z2,x3,y3,z3⎵Triangle1,x4,y4,z5,x5,y5,z5,x6,y6,z6⎵Triangle2,…]We first need to create the buffer:
- GLuint bufs[1];
- glGenBuffers(1, bufs);
-
- shader->vertexBuffer = bufs[0];
This code creates a single buffer (line 2) and stores an integer handle to that buffer. Now we need to fill the buffer with the
mesh.verts data:
- glBindBuffer(GL_ARRAY_BUFFER, shader->vertexBuffer);
- glBufferData(
- GL_ARRAY_BUFFER, //what kind of buffer (an array)
- mesh.numVerts * 3 * sizeof(float), //size of the buffer in bytes
- mesh.verts, //pointer to data we want to fill the buffer with
- GL_STATIC_DRAW //how we intend to use the buffer
- );
You can use this process to fill all of your vertex data. For example, if you have a normal and texture coordinate for each vertex, you would specify them in the same way:
- Create the buffer
- Bind the buffer you want to fill using glBindBuffer
- Use glBufferData to fill the buffer you just bound
A Simple Shader
Here is a simple vertex shader program:
- //These variables are constant for all vertices
- uniform mat4 M; //modelview matrix
- uniform mat4 P; //projection matrix
- uniform mat3 M_n; //normal matrix
-
- //input variables from host
- attribute vec3 pos; //vertex position
-
- //variables to be passed to the fragment shader
- varying vec4 frag_color;
-
- void main() {
- gl_Position = P * (M * vec4(pos, 1.0));
-
- //determine vertex color based on position and time
- vec4 color = vec4(pos,1.0);
- color = clamp(color, 0.0, 1.0);
- frag_color = clamp(color, 0.0, 1.0);
- }
And the corresponding fragment shader:
- varying vec4 frag_color;
-
- void main() {
- gl_FragColor = frag_color;
- }
In general, shader code is very similar to C code. See the
GLSL specs for more details on the GLSL language.
Let's go over the vertex shader first. There is a vertex program that processes each vertex in parallel. Remember that OpenGL no longer handles the transformation of our vertices. That is, there is no matrix stack we can glPush and glPop matrices on/off to. Instead, we will manage both the modelview and the projection matrices on the CPU and pass them to the GPU using uniform variables. These are specified in the top 3 lines of the vertex shader.
- uniform mat4 M; //modelview matrix
- uniform mat4 P; //projection matrix
- uniform mat3 M_n; //normal matrix
You can think of uniform variables as globally accessible to each vertex and fragment program. The values of M, P, and M_n are the same for every vertex and fragment program running.
In addition to uniform variables, each vertex has attributes associated with it (e.g. position, normal, color, texture coordinate, etc.). These are accessed via the followingattribute variables:
- attribute vec3 pos; //vertex position
For this example, there is only one vertex attribute which represents the vertex position.
varying variables can be thought of as indirect outputs to fragment shaders. I say indirectsince the values that are set here in the vertex shader are not exactly what gets passed to the fragment shader. Rather, the values of varying variables are interpolated before being passed to fragment shaders. Consider figure [F4]. The color of the fragment (which can be thought of as the frag_color variable in this example) is not exactly the color of any of the three vertices, but rather an interpolated (usually linearly) color.
Now that we know about some different types of variables that appear in shader code, let's take a look at our example fragment shader. In the fragment shader code, notice the gl_FragColorvariable. This is a built-in GLSL variable which represents the color of that fragment. The purpose of the fragment shader is to set this variable to something reasonable. In this case, we are just setting the color to the value interpolated by OpenGL (line 4 of the fragment program).
Compiling and Linking your Shader Source Code
Now that we've written our shader program, we need to compile and link it into a shader program. We do this using the Shader::fromFiles and Shader::setShaderSource functions.
- void Shader::fromFiles(std::string vertFile, std::string fragFile) {
- //These are shader objects containing the shader source code
- GLint vSource = setShaderSource(vertFile, GL_VERTEX_SHADER);
- GLint fSource = setShaderSource(fragFile, GL_FRAGMENT_SHADER);
-
- //Create a new shader program
- program = glCreateProgram();
-
- //Compile the source code for each shader and attach it to the program.
- glCompileShader(vSource);
- printLog("vertex compile log: ", vSource);
- glAttachShader(program, vSource);
-
- glCompileShader(fSource);
- printLog("fragment compile log: ", fSource);
- glAttachShader(program, fSource);
-
- //we could attach more shaders, such as a geometry or tessellation
- //shader here.
-
- //link all of the attached shader objects
- glLinkProgram(program);
- }
- GLint Shader::setShaderSource(std::string file, GLenum type) {
- //read source code
- ifstream fin(file.c_str());
- if (fin.fail()) {
- cerr << "Could not open " << file << " for reading" << endl;
- return -1;
- }
- fin.seekg(0, ios::end);
- int count = fin.tellg();
- char *data = NULL;
- if (count > 0) {
- fin.seekg(ios::beg);
- data = new char[count+1];
- fin.read(data,count);
- data[count] = '\0';
- }
- fin.close();
-
- //create the shader
- GLint s = glCreateShader(type);
- glShaderSource(s, 1, const_cast<const char **>(&data), NULL);
- delete [] data;
- return s;
- }
The inmportant information in the
fromFiles routine is on lines 7,10,12,14,16 and for the
setShaderSource routine is lines 20 and 21. The
Shader class assumes you only have a vertex and fragment shader. However, you may have other shaders such as a tessellation and geometry shader. You can use this process to attach whatever shaders you want to a shader program:
- Create a shader program (fromFiles:7)
- For each shader:
- Get your shader source code into a string (setShaderSource:2-17)
- Create a shader (setShaderSource:20)
- Set the shader source to the string you read in earlier (setShaderSource:21)
- Compile and attach the shader to the shader program (fromFiles:10-12,14-16)
- Link the shader program (fromFiles:22)
Specifying Vertex Attribute Data in OpenGL
Now that we've written our vertex and fragment shaders, we need to get handles to each variable in order to associate certain data to certain variables. We do this using glGet*Locationcalls:
- //Here's where we setup handles to each variable that is used in the shader
- //program. See the shader source code for more detail on what the difference
- //is between uniform and vertex attribute variables.
- shader->modelViewLoc = glGetUniformLocation(shader->program, "M");
- shader->projectionLoc = glGetUniformLocation(shader->program, "P");
- shader->normalMatrixLoc = glGetUniformLocation(shader->program, "M_n");
-
- shader->vertexLoc = glGetAttribLocation(shader->program, "pos");
Notice the string parameter to each
glGet*Location call corresponds to a variable in our shader code. The value returned by this function is the handle to that variable and allows us to specify the data that will fill that variable. Let's see how we specify the modelview, projection, and normal matrices. We will use the
GLM library to manipulate these matrices on the CPU.
- //Pass the matrices and animation time to the GPU
- glUniformMatrix4fv(
- shader->modelViewLoc, //handle to variable in the shader program
- 1, //how many matrices we want to send
- GL_FALSE, //don't transpose the matrix
- glm::value_ptr(modelCam) //a pointer to an array containing the entries for
- //the matrix
- );
- glUniformMatrix4fv(shader->projectionLoc, 1, GL_FALSE,
- glm::value_ptr(projection));
- glUniformMatrix3fv(shader->normalMatrixLoc, 1, GL_FALSE,
- glm::value_ptr(normalMatrix));
The last call to
glUnifromMatrix*fv is just a pointer to a floating point array containing 16 floating point values which represent a 4x4 matrix in column-major order:
[0481215913261014371115]If you are using a math library that uses row-major matrices, you will need to set the third parameter of glUniformMatrix*fv to GL_TRUE, which will transpose the matrix before sending it to the GPU. To set the per-vertex data using the buffer we created in section [S6] we do the following:
- glBindBuffer(GL_ARRAY_BUFFER, shader->vertexBuffer); //which buffer we want
- //to use
- glEnableVertexAttribArray(shader->vertexLoc); //enable the attribute
- glVertexAttribPointer(
- shader->vertexLoc, //handle to variable in shader program
- 3, //vector size (e.g. for texture coordinates this could be 2).
- GL_FLOAT, //what type of data (e.g. GL_FLOAT, GL_INT, etc.)
- GL_FALSE, //normalize the data?
- 0, //stride of data (e.g. offset in bytes). Most of the time leaving
- //this at 0 (assumes data is in one, contiguous array) is fine
- //unless we're doing something really complex.
- NULL //since our stride will be 0 in general, leaving this NULL is
- //also fine in general
- );
We can repeat this sequence of function calls for each buffer/vertex attribute we have. Once we have associated each shader variable with the apprpriate data, we can draw our geometry using
glDrawArrays:
- glDrawArrays(GL_TRIANGLES, 0, mesh.numVerts);
This says that our vertex data is packed into triangles (as described in section
[S6]) and that we have
mesh.numVerts vertices.
Summary
In summary, one can use the following steps to create a GLSL program:- Read your vertex data into arrays (vertices, normals, texture coordinates etc) and setup your matrices (modelview, projection, etc.).
- Setup GL_ARRAY_BUFFERs for each array [S6]
- Write your shaders [S7]
- Load your shader source, compile it, and link it [S8]
- Get handles to the variables in your shader code and associate each shader variable to the appropriate data [S9]
- Draw your data using glDrawArrays or glDrawElements.