Hello GLSL
Last Updated: Aug 9th, 2012
Up until now we've been using the old Fixed Function Pipeline which did all our vertex operations (glTranslate(), glVertex(), glTexCoord(), etc) and fragment operations (I'll show you those in a little bit) for us. This was nice when you're a beginner and you're not doing anything complicated, but when you need power and flexibility the fixed function pipeline is constraining.Enter GLSL (OpenGL Shading Language) and the Programmable Pipeline. With GLSL, you can give your OpenGL program executable shader programs which control how your GPU handles the data you send it. The GLSL Programmable Pipeline can do everything the Fixed Function Pipeline can do and more which made the FFP obsolete. With the release of OpenGL 3.0+, the fixed function pipeline is deprecated and if you want to do any rendering you need to tell the GPU how to handle your data with GLSL.
Why didn't the tutorial set start off with the Programmable Pipeline? Because as you're about to see it takes significantly more work to get a GLSL shader program going. This tutorial will get you off the ground by creating your first GLSL shader program.
From main.cpp
//Do post window/context creation initialization if( !initGL() ) { printf( "Unable to initialize graphics library!\n" ); return 1; } //Load graphics programs if( !loadGP() ) { printf( "Unable to load shader programs!\n" ); return 1; } //Load media if( !loadMedia() ) { printf( "Unable to load media!\n" ); return 2; }
In this tutorial and future ones we'll loading shader programs. Shader programs control how our OpenGL program operates, so they could be loaded in the initGL() function. In
future tutorials we'll be loading text shader files so they could be loaded in the loadMedia() function.
Because graphics programs are so unique, they'll get their own loading function loadGP().
Because graphics programs are so unique, they'll get their own loading function loadGP().
From LShaderProgram.h
class LShaderProgram { public: LShaderProgram(); /* Pre Condition: -None Post Condition: -Initializes variables Side Effects: -None */ virtual ~LShaderProgram(); /* Pre Condition: -None Post Condition: -Frees shader program Side Effects: -None */ virtual bool loadProgram() = 0; /* Pre Condition: -A valid OpenGL context Post Condition: -Loads shader program Side Effects: -None */ virtual void freeProgram(); /* Pre Condition: -None Post Condition: -Frees shader program if it exists Side Effects: -None */ bool bind(); /* Pre Condition: -A loaded shader program Post Condition: -Sets this program as the current shader program -Reports to console if there was an error Side Effects: -None */ void unbind(); /* Pre Condition: -None Post Condition: -Sets default shader program as current program Side Effects: -None */ GLuint getProgramID(); /* Pre Condition: -None Post Condition: -Returns program ID Side Effects: -None */ protected: void printProgramLog( GLuint program ); /* Pre Condition: -None Post Condition: -Prints program log -Reports error is GLuint ID is not a shader program Side Effects: -None */ void printShaderLog( GLuint shader ); /* Pre Condition: -None Post Condition: -Prints shader log -Reports error is GLuint ID is not a shader Side Effects: -None */ //Program ID GLuint mProgramID; };
Here's the overview of the LShaderProgram class which will serve as the base class for all of our shader programs.
Don't obsess with the details of the class too much for now, but take notice of the "mProgramID" member variable. Just like we bind texture IDs and VBO IDs to use them, we'll be binding shader program IDs to use them.
Don't obsess with the details of the class too much for now, but take notice of the "mProgramID" member variable. Just like we bind texture IDs and VBO IDs to use them, we'll be binding shader program IDs to use them.
From LShaderProgram.cpp
LShaderProgram::LShaderProgram() { mProgramID = NULL; } LShaderProgram::~LShaderProgram() { //Free program if it exists freeProgram(); } void LShaderProgram::freeProgram() { //Delete program glDeleteProgram( mProgramID ); }
The constructor for LShaderProgram just initializes the ID to 0. The destructor just calls freeProgram() which just calls glDeleteProgram() to delete the program much in the same
way we would call glDeleteTextures() to delete a texture.
From LShaderProgram.cpp
bool LShaderProgram::bind() { //Use shader glUseProgram( mProgramID ); //Check for error GLenum error = glGetError(); if( error != GL_NO_ERROR ) { printf( "Error binding shader! %s\n", gluErrorString( error ) ); printProgramLog( mProgramID ); return false; } return true; } void LShaderProgram::unbind() { //Use default program glUseProgram( NULL ); } GLuint LShaderProgram::getProgramID() { return mProgramID; }
To bind a shader program for use, we call glUseProgram() on the program ID. To make sure the shader program bound successfully, we check if there were any errors using
glGetError(). If there were errors, we report them to the console. If there were no errors we return true.
To unbind the current shader program, we just bind a null ID. On OpenGL 2.1, this will cause the old fixed function pipeline to be used. In the post OpenGL 3.0 world, binding a NULL shader will cause nothing to be rendered because there's no fixed function pipeline.
Lastly, we have a function to get the program ID.
To unbind the current shader program, we just bind a null ID. On OpenGL 2.1, this will cause the old fixed function pipeline to be used. In the post OpenGL 3.0 world, binding a NULL shader will cause nothing to be rendered because there's no fixed function pipeline.
Lastly, we have a function to get the program ID.
From LShaderProgram.cpp
void LShaderProgram::printProgramLog( GLuint program ) { //Make sure name is shader if( glIsProgram( program ) ) { //Program log length int infoLogLength = 0; int maxLength = infoLogLength; //Get info string length glGetProgramiv( program, GL_INFO_LOG_LENGTH, &maxLength ); //Allocate string char* infoLog = new char[ maxLength ]; //Get info log glGetProgramInfoLog( program, maxLength, &infoLogLength, infoLog ); if( infoLogLength > 0 ) { //Print Log printf( "%s\n", infoLog ); } //Deallocate string delete[] infoLog; } else { printf( "Name %d is not a program\n", program ); } }
Now getting a GLSL shader program working requires quite a bit of communication with the GPU and we need to be able to print the GLSL shader program logs to know if something goes
wrong. This information is vital when trying to debug your GLSL shader program.
First we want to check if the ID we gave it was even a shader program using glIsProgram(). If it is, we check to find out how long the info log is in characters using glGetProgramiv(). Then we allocate the needed character string and get the actual program info log using glGetProgramInfoLog(). If the info log is longer than 0 characters (which means one actually exists) we print it out to the console. After we'll done with the program log, we deallocate the string.
Now if the ID was not even a program, we print an error to the console.
First we want to check if the ID we gave it was even a shader program using glIsProgram(). If it is, we check to find out how long the info log is in characters using glGetProgramiv(). Then we allocate the needed character string and get the actual program info log using glGetProgramInfoLog(). If the info log is longer than 0 characters (which means one actually exists) we print it out to the console. After we'll done with the program log, we deallocate the string.
Now if the ID was not even a program, we print an error to the console.
From LShaderProgram.cpp
void LShaderProgram::printShaderLog( GLuint shader ) { //Make sure name is shader if( glIsShader( shader ) ) { //Shader log length int infoLogLength = 0; int maxLength = infoLogLength; //Get info string length glGetShaderiv( shader, GL_INFO_LOG_LENGTH, &maxLength ); //Allocate string char* infoLog = new char[ maxLength ]; //Get info log glGetShaderInfoLog( shader, maxLength, &infoLogLength, infoLog ); if( infoLogLength > 0 ) { //Print Log printf( "%s\n", infoLog ); } //Deallocate string delete[] infoLog; } else { printf( "Name %d is not a shader\n", shader ); } }
Here we have printShaderLog() which prints out the log for a shader pretty much in the same way printProgramLog() prints out the info for a program.
You're probably wondering what's the difference between a shader and a program. A shader controls part of your graphics pipeline. A vertex shader controls how to process vertex data and a fragment shader controls fragment operations. The program has a vertex shader and a fragment shader attached to it (and maybe other shaders like a geometry shader). With the shaders attached to it, the shader program controls how data is rendered.
You're probably wondering what's the difference between a shader and a program. A shader controls part of your graphics pipeline. A vertex shader controls how to process vertex data and a fragment shader controls fragment operations. The program has a vertex shader and a fragment shader attached to it (and maybe other shaders like a geometry shader). With the shaders attached to it, the shader program controls how data is rendered.
From LPlainPolygonProgram2D.h
#include "LShaderProgram.h" class LPlainPolygonProgram2D : public LShaderProgram { public: bool loadProgram(); /* Pre Condition: -A valid OpenGL context Post Condition: -Loads plain polygon program Side Effects: -None */ private: };
Here we have the LPlainPolygonProgram2D shader program class. The only current difference between the base class is that it has a function defined to load a shader program.
Now that you seen the overall structure of these shader program classes, it's time to build your first shader.
Now that you seen the overall structure of these shader program classes, it's time to build your first shader.
From LPlainPolygonProgram2D.cpp
>bool LPlainPolygonProgram2D::loadProgram() { //Success flag GLint programSuccess = GL_TRUE; //Generate program mProgramID = glCreateProgram();
At the top of the loadProgram() function we allocate a shader program ID using glCreateProgram(). A shader program isn't very useful without some vertex or fragment operation
attached to it. So let's start attaching some shaders.
From LPlainPolygonProgram2D.cpp
//Create vertex shader GLuint vertexShader = glCreateShader( GL_VERTEX_SHADER ); //Get vertex source const GLchar* vertexShaderSource[] = { "void main() { gl_Position = gl_Vertex; }" }; //Set vertex source glShaderSource( vertexShader, 1, vertexShaderSource, NULL );
Ok here we allocate a vertex shader ID using glCreateShader() with the GL_VERTEX_SHADER argument. Then we have some GLSL source code put directly into an array of strings named
"vertexShaderSource". We then set the source code for the vertex shader using glShaderSource().
The first argument is the vertex shader ID. The second argument is how many source strings you're using. Caution: the GLSL compiler expects one long string per source file. Much like in C++, you can have more than one source file per shader. However, it will treat each string in the array as a source file.
The third argument is the pointer to the array of shader source strings. The last argument is the array of string lengths for each of the shader source strings. If this is null, the GLSL source compiler assumes each string is null terminated.
The first argument is the vertex shader ID. The second argument is how many source strings you're using. Caution: the GLSL compiler expects one long string per source file. Much like in C++, you can have more than one source file per shader. However, it will treat each string in the array as a source file.
The third argument is the pointer to the array of shader source strings. The last argument is the array of string lengths for each of the shader source strings. If this is null, the GLSL source compiler assumes each string is null terminated.
From LPlainPolygonProgram2D.cpp
//Compile vertex source glCompileShader( vertexShader ); //Check vertex shader for errors GLint vShaderCompiled = GL_FALSE; glGetShaderiv( vertexShader, GL_COMPILE_STATUS, &vShaderCompiled ); if( vShaderCompiled != GL_TRUE ) { printf( "Unable to compile vertex shader %d!\n", vertexShader ); printShaderLog( vertexShader ); return false; } //Attach vertex shader to program glAttachShader( mProgramID, vertexShader );
With the vertex shader source code set for the vertex shader, we compile the shader using glCompileShader().
After compiling, we need to check if there were any errors in compilation. Using glGetShaderiv(), we get the GL_COMPILE_STATUS. If the shader failed to compile, we output the log to use for debugging.
If the vertex shader compiled successfully, we attach the vertex shader to our program.
After compiling, we need to check if there were any errors in compilation. Using glGetShaderiv(), we get the GL_COMPILE_STATUS. If the shader failed to compile, we output the log to use for debugging.
If the vertex shader compiled successfully, we attach the vertex shader to our program.
From LPlainPolygonProgram2D.cpp
//Create fragment shader GLuint fragmentShader = glCreateShader( GL_FRAGMENT_SHADER ); //Get fragment source const GLchar* fragmentShaderSource[] = { "void main() { gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 ); }" }; //Set fragment source glShaderSource( fragmentShader, 1, fragmentShaderSource, NULL ); //Compile fragment source glCompileShader( fragmentShader ); //Check fragment shader for errors GLint fShaderCompiled = GL_FALSE; glGetShaderiv( fragmentShader, GL_COMPILE_STATUS, &fShaderCompiled ); if( fShaderCompiled != GL_TRUE ) { printf( "Unable to compile fragment shader %d!\n", fragmentShader ); printShaderLog( fragmentShader ); return false; } //Attach fragment shader to program glAttachShader( mProgramID, fragmentShader );
Here we compile and attach the fragment shader much in the same way we did with the vertex shader. Do make a note of the fact that the GLSL shader code is different for the
fragment shader. This is obviously because vertices and fragments are two different things that require different operations.
From LPlainPolygonProgram2D.cpp
//Link program glLinkProgram( mProgramID ); //Check for errors glGetProgramiv( mProgramID, GL_LINK_STATUS, &programSuccess ); if( programSuccess != GL_TRUE ) { printf( "Error linking program %d!\n", mProgramID ); printProgramLog( mProgramID ); return false; } return true; }
With both the vertex and fragment shaders attached to the shader program, we link the program. Vertex and fragment shaders typically have to have data sent between them so the
linking process makes sure the shaders play nice with each other.
Like with compilation, we use glGetProgramiv() to make sure the program linked properly. If it did, it means our program is ready to be used.
Like with compilation, we use glGetProgramiv() to make sure the program linked properly. If it did, it means our program is ready to be used.
From LUtil.cpp
//Basic shader LPlainPolygonProgram2D gPlainPolygonProgram2D; bool initGL() { //Initialize GLEW GLenum glewError = glewInit(); if( glewError != GLEW_OK ) { printf( "Error initializing GLEW! %s\n", glewGetErrorString( glewError ) ); return false; } //Make sure OpenGL 2.1 is supported if( !GLEW_VERSION_2_1 ) { printf( "OpenGL 2.1 not supported!\n" ); return false; } //Set the viewport glViewport( 0.f, 0.f, SCREEN_WIDTH, SCREEN_HEIGHT ); //Initialize Projection Matrix glMatrixMode( GL_PROJECTION ); glLoadIdentity(); glOrtho( 0.0, SCREEN_WIDTH, SCREEN_HEIGHT, 0.0, 1.0, -1.0 ); //Initialize Modelview Matrix glMatrixMode( GL_MODELVIEW ); glLoadIdentity(); //Initialize clear color glClearColor( 0.f, 0.f, 0.f, 1.f ); //Enable texturing glEnable( GL_TEXTURE_2D ); //Set blending glEnable( GL_BLEND ); glDisable( GL_DEPTH_TEST ); glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); //Check for error GLenum error = glGetError(); if( error != GL_NO_ERROR ) { printf( "Error initializing OpenGL! %s\n", gluErrorString( error ) ); return false; } //Initialize DevIL and DevILU ilInit(); iluInit(); ilClearColour( 255, 255, 255, 000 ); //Check for error ILenum ilError = ilGetError(); if( ilError != IL_NO_ERROR ) { printf( "Error initializing DevIL! %s\n", iluErrorString( ilError ) ); return false; } return true; }
At the top of LUtil.cpp we declare a shader object for us to use.
Make sure to notice how initGL() is the same as it used to be. It'll be an important thing to know when we get to rendering.
Make sure to notice how initGL() is the same as it used to be. It'll be an important thing to know when we get to rendering.
From LUtil.cpp
bool loadGP() { //Load basic shader program if( !gPlainPolygonProgram2D.loadProgram() ) { printf( "Unable to load basic shader!\n" ); return false; } //Bind basic shader program gPlainPolygonProgram2D.bind(); return true; }
In the loadGP() function, we load the LPlainPolygonProgram2D and bind it for use.
From LUtil.cpp
void render() { //Clear color buffer glClear( GL_COLOR_BUFFER_BIT ); //Reset transformations glLoadIdentity(); //Solid cyan quad in the center glTranslatef( SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 0.f ); glBegin( GL_QUADS ); glColor3f( 0.f, 1.f, 1.f ); glVertex2f( -50.f, -50.f ); glVertex2f( 50.f, -50.f ); glVertex2f( 50.f, 50.f ); glVertex2f( -50.f, 50.f ); glEnd(); //Update screen glutSwapBuffers(); }
In our render function, we have the code to render a cyan quad in the center of the screen. Yet for some reason, when this demo program compiles and runs we get this:
Why? Let's look at the shader source code we gave the shaders.
Why? Let's look at the shader source code we gave the shaders.
From Vertex Shader Source
void main() { gl_Position = gl_Vertex; }
Here is the GLSL code for the vertex shader. Even if you've never seen the GLSL documentation, its C like syntax should be easy to pick up on the fly.
Notice how the final position of the vertex is the same as the vertex we took in. We never multiplied against the projection or modelview matrices so glOrtho(), glTranslate(), and our other OpenGL matrix calls have no effect and the quad we rendered uses untransformed matrix coordinates. This is what we mean by programmable pipeline because you program how the GPU pipeline processes the data.
So because the vertices are untransformed, the quad will consume the entire screen as opposed to being 100 pixels wide in the center.
Notice how the final position of the vertex is the same as the vertex we took in. We never multiplied against the projection or modelview matrices so glOrtho(), glTranslate(), and our other OpenGL matrix calls have no effect and the quad we rendered uses untransformed matrix coordinates. This is what we mean by programmable pipeline because you program how the GPU pipeline processes the data.
So because the vertices are untransformed, the quad will consume the entire screen as opposed to being 100 pixels wide in the center.
From Fragment Shader Source
void main() { gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 ); }
Here's the fragment shader and the reason the quad is red as opposed to cyan. In the fragment shader we don't take into account the color attribute and just set the output fragment
to be red 1, green 0, blue 0, and alpha 1. While it appears strange how the OpenGL program behaved, it was only doing what we told it to do.
This is the power of shaders. By giving control to the programmer how the graphics data is processed you can achieve the powerful effects you see in modern games, or something completely pointless like untransformed red geometry =).
This is the power of shaders. By giving control to the programmer how the graphics data is processed you can achieve the powerful effects you see in modern games, or something completely pointless like untransformed red geometry =).