Lazy Foo' Productions

SDL Forums external link SDL Tutorials Articles OpenGL Tutorials OpenGL Forums external link
Follow BlueSky Follow Facebook Follow Twitter Follow Threads
Donate
News FAQs Contact Bugs

Hello GLSL

Hello GLSL screenshot

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().
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
preview

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.
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 =).