Making Shaders

We glossed over exactly how these text strings called shaders actually get sent to OpenGL. We will go into some detail on that now.

Note

If you are familiar with how shaders work in other APIs like Direct3D, that will not help you here. OpenGL shaders work very differently from the way they work in other APIs.

Shaders are written in a C-like language. So OpenGL uses a very C-like compilation model. In C, each individual .c file is compiled into an object file. Then, one or more object files are linked together into a single program (or static/shared library). OpenGL does something very similar.

A shader string is compiled into a shader object; this is analogous to an object file. One or more shader objects is linked into a program object.

A program object in OpenGL contains code for all of the shaders to be used for rendering. In the tutorial, we have a vertex and a fragment shader; both of these are linked together into a single program object. Building that program object is the responsibility of this code:

Example 1.6. Program Initialization

void InitializeProgram()
{
    std::vector<GLuint> shaderList;
    
    shaderList.push_back(CreateShader(GL_VERTEX_SHADER, strVertexShader));
    shaderList.push_back(CreateShader(GL_FRAGMENT_SHADER, strFragmentShader));
    
    theProgram = CreateProgram(shaderList);

    std::for_each(shaderList.begin(), shaderList.end(), glDeleteShader);
}

The first statement simply creates a list of the shader objects we intend to link together. The next two statements compile our two shader strings. The CreateShader function is a function defined by the tutorial that compiles a shader.

Compiling a shader into a shader object is a lot like compiling source code. Most important of all, it involves error checking. This is the implementation of CreateShader:

Example 1.7. Shader Creation

GLuint CreateShader(GLenum eShaderType, const std::string &strShaderFile)
{
    GLuint shader = glCreateShader(eShaderType);
    const char *strFileData = strShaderFile.c_str();
    glShaderSource(shader, 1, &strFileData, NULL);
    
    glCompileShader(shader);
    
    GLint status;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint infoLogLength;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);
        
        GLchar *strInfoLog = new GLchar[infoLogLength + 1];
        glGetShaderInfoLog(shader, infoLogLength, NULL, strInfoLog);
        
        const char *strShaderType = NULL;
        switch(eShaderType)
        {
        case GL_VERTEX_SHADER: strShaderType = "vertex"; break;
        case GL_GEOMETRY_SHADER: strShaderType = "geometry"; break;
        case GL_FRAGMENT_SHADER: strShaderType = "fragment"; break;
        }
        
        fprintf(stderr, "Compile failure in %s shader:\n%s\n", strShaderType, strInfoLog);
        delete[] strInfoLog;
    }

	return shader;
}

An OpenGL shader object is, as the name suggests, an object. So the first step is to create the object with glCreateShader. This function creates a shader of a particular type (vertex or fragment), so it takes a parameter that tells what kind of object it creates. Since each shader stage has certain syntax rules and pre-defined variables and constants (thus making different shader stages different dialects of GLSL), the compiler must be told what shader stage is being compiled.

Note

Shader and program objects are objects in OpenGL. But they work rather differently from other kinds of OpenGL objects. For example, creating buffer objects, as shown above, uses a function of the form glGen* where * is Buffer. It takes a number of objects to create and a list to put those object handles in.

There are many other differences between shader/program objects and other kinds of OpenGL objects.

The next step is to actually compile the text shader into the object. The C-style string is retrieved from the C++ std::string object, and it is fed into the shader object with the glShaderSource function. The first parameter is the shader object to put the string into. The next parameter is the number of strings to put into the shader. Compiling multiple strings into a single shader object works analogously to compiling header files in C files. Except of course that the .c file explicitly lists the files it includes, while you must manually add them with glShaderSource.

The next parameter is an array of const char* strings. The last parameter is normally an array of lengths of the strings. We pass in NULL, which tells OpenGL to assume that the string is null-terminated. In general, unless you need to use the null character in a string, there is no need to use the last parameter.

Once the strings are in the object, they are compiled with glCompileShader, which does exactly what it says.

After compiling, we need to see if the compilation was successful. We do this by calling glGetShaderiv to retrieve the GL_COMPILE_STATUS. If this is GL_FALSE, then the shader failed to compile; otherwise compiling was successful.

If compilation fails, we do some error reporting. It prints a message to stderr that explains what failed to compile. It also prints an info log from OpenGL that describes the error; think of this log as the compiler output from a regular C compilation.

After creating both shader objects, we then pass them on to the CreateProgram function:

Example 1.8. Program Creation

GLuint CreateProgram(const std::vector<GLuint> &shaderList)
{
    GLuint program = glCreateProgram();
    
    for(size_t iLoop = 0; iLoop < shaderList.size(); iLoop++)
    	glAttachShader(program, shaderList[iLoop]);
    
    glLinkProgram(program);
    
    GLint status;
    glGetProgramiv (program, GL_LINK_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint infoLogLength;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
        
        GLchar *strInfoLog = new GLchar[infoLogLength + 1];
        glGetProgramInfoLog(program, infoLogLength, NULL, strInfoLog);
        fprintf(stderr, "Linker failure: %s\n", strInfoLog);
        delete[] strInfoLog;
    }
    
    for(size_t iLoop = 0; iLoop < shaderList.size(); iLoop++)
        glDetachShader(program, shaderList[iLoop]);

    return program;
}

This function is fairly simple. It first creates an empty program object with glCreateProgram. This function takes no parameters; remember that program objects are a combination of all shader stages.

Next, it attaches each of the previously created shader objects to the programs, by calling the function glAttachShader in a loop over the std::vector of shader objects. The program does not need to be told what stage each shader object is for; the shader object itself remembers this.

Once all of the shader objects are attached, the code links the program with glLinkProgram. Similar to before, we must then fetch the linking status by calling glGetProgramiv with GL_LINK_STATUS. If it is GL_FALSE, then the linking failed and we print the linking log. Otherwise, we return the created program.

Note

In the above shaders, the attribute index for the vertex shader input position was assigned directly in the shader itself. There are other ways to assign attribute indices to attributes besides layout(location = #). OpenGL will even assign an attribute index if you do not use any of them. Therefore, it is possible that you may not know the attribute index of an attribute. If you need to query the attribute index, you may call glGetAttribLocation with the program object and a string containing the attribute's name.

Once the program was successfully linked, the shader objects are removed from the program with glDetachShader. The program's linking status and functionality is not affected by the removal of the shaders. All it does is tell OpenGL that these objects are no longer associated with the program.

After the program has successfully linked, and the shader objects removed from the program, the shader objects are deleted using the C++ algorithm std::for_each. This line loops over each of the shaders in the list and calls glDeleteShader on them.

Using Programs.  To tell OpenGL that rendering commands should use a particular program object, the glUseProgram function is called. In the tutorial this is called twice in the display function. It is called with the global theProgram, which tells OpenGL that we want to use that program for rendering until further notice. It is later called with 0, which tells OpenGL that no programs will be used for rendering.

Note

For the purposes of these tutorials, using program objects is not optional. OpenGL does have, in its compatibility profile, default rendering state that takes over when a program is not being used. We will not be using this, and you are encouraged to avoid its use as well.

Fork me on GitHub