OpenGL 4 Shading Language Cookbook(Second Edition)
上QQ阅读APP看书,第一时间看更新

Compiling a shader

To get started, we need to know how to compile our GLSL shaders. The GLSL compiler is built right into the OpenGL library, and shaders can only be compiled within the context of a running OpenGL program. There is currently no external tool for precompiling GLSL shaders and/or shader programs.

Note

Recently, OpenGL 4.1 added the ability to save compiled shader programs to a file, enabling OpenGL programs to avoid the overhead of shader compilation by loading pre-compiled shader programs.

Compiling a shader involves creating a shader object, providing the source code (as a string or set of strings) to the shader object, and asking the shader object to compile the code. The process is roughly represented by the following diagram:

Compiling a shader

Getting ready

To compile a shader, we'll need a basic example to work with. Let's start with the following simple vertex shader. Save it in a file named basic.vert.

#version 430

in vec3 VertexPosition;
in vec3 VertexColor;

out vec3 Color;

void main()
{
   Color = VertexColor;
   gl_Position = vec4( VertexPosition, 1.0 );
}

In case you're curious about what this code does, it works as a "pass-through" shader. It takes the input attributes VertexPosition and VertexColor and passes them along to the fragment shader via the output variables gl_Position and Color.

Next, we'll need to build a basic shell for an OpenGL program using a window toolkit that supports OpenGL. Examples of cross-platform toolkits include GLFW, GLUT, FLTK, Qt, or wxWidgets. Throughout this text, I'll make the assumption that you can create a basic OpenGL program with your favorite toolkit. Virtually all toolkits have a hook for an initialization function, a resize callback (called upon resizing of the window), and a drawing callback (called for each window refresh). For the purposes of this recipe, we need a program that creates and initializes an OpenGL context, it need not do anything other than display an empty OpenGL window. Note that you'll also need to load the OpenGL function pointers (refer to the Using a function loader to access the latest OpenGL functionality recipe).

Finally, load the shader source code into a character array named shaderCode. Don't forget to add the null character at the end! This example assumes that the variable shaderCode points to an array of GLchar that is properly terminated by a null character.

How to do it...

To compile a shader, use the following steps:

  1. Create the shader object as follows:
    GLuint vertShader = glCreateShader( GL_VERTEX_SHADER );
    if( 0 == vertShader )
    {
      fprintf(stderr, "Error creating vertex shader.\n");
      exit(EXIT_FAILURE);
    }
  2. Copy the source code (perhaps from multiple locations) into the shader object:
    const GLchar * shaderCode = loadShaderAsString("basic.vert");
    const GLchar* codeArray[] = {shaderCode};
    glShaderSource( vertShader, 1, codeArray, NULL );
  3. Compile the shader:
    glCompileShader( vertShader );
  4. Verify the compilation status:
    GLint result;
    glGetShaderiv( vertShader, GL_COMPILE_STATUS, &result );
    if( GL_FALSE == result )
    {
      fprintf(stderr, "Vertex shader compilation failed!\n");
    
      GLint logLen;
      glGetShaderiv(vertShader, GL_INFO_LOG_LENGTH, &logLen);
       
      if( logLen > 0 )
      {
        char * log = new char[logLen];
          
        GLsizei written;
        glGetShaderInfoLog(vertShader, logLen, &written, log);
          
        fprintf(stderr, "Shader log:\n%s", log);
        delete [] log;
      }
    }

How it works...

The first step is to create the shader object using the glCreateShader function. The argument is the type of shader, and can be one of the following: GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, GL_GEOMETRY_SHADER, GL_TESS_EVALUATION_SHADER, GL_TESS_CONTROL_SHADER, or (as of version 4.3) GL_COMPUTE_SHADER. In this case, since we are compiling a vertex shader, we use GL_VERTEX_SHADER. This function returns the value used for referencing the vertex shader object, sometimes called the object "handle". We store that value in the variable vertShader. If an error occurs while creating the shader object, this function will return 0, so we check for that and if it occurs, we print an appropriate message and terminate.

Following the creation of the shader object, we load the source code into the shader object using the function glShaderSource. This function is designed to accept an array of strings (as opposed to just a single one) in order to support the option of compiling multiple sources (files, strings) at once. So before we call glShaderSource, we place a pointer to our source code into an array named sourceArray. The first argument to glShaderSource is the handle to the shader object. The second is the number of source code strings that are contained in the array. The third argument is a pointer to an array of source code strings. The final argument is an array of GLint values that contains the length of each source code string in the previous argument. In the previous code, we pass a value of NULL, which indicates that each source code string is terminated by a null character. If our source code strings were not null terminated then this argument must be a valid array. Note that once this function returns, the source code has been copied into OpenGL internal memory, so the memory used to store the source code can be freed.

The next step is to compile the source code for the shader. We do this by simply calling glCompileShader, and passing the handle to the shader that is to be compiled. Of course, depending on the correctness of the source code, the compilation may fail, so the next step is to check whether or not the compilation was successful.

We can query for the compilation status by calling glGetShaderiv, which is a function for querying the attributes of a shader object. In this case we are interested in the compilation status, so we use GL_COMPILE_STATUS as the second argument. The first argument is of course the handle to the shader object, and the third argument is a pointer to an integer where the status will be stored. The function provides a value of either GL_TRUE or GL_FALSE in the third argument indicating whether or not the compilation was successful.

If the compile status is GL_FALSE, then we can query for the shader log, which will provide additional details about the failure. We do so by first querying for the length of the log by calling glGetShaderiv again with a value of GL_INFO_LOG_LENGTH. This provides the length of the log in the variable logLen. Note that this includes the null termination character. We then allocate space for the log, and retrieve the log by calling glGetShaderInfoLog. The first parameter is the handle to the shader object, the second is the size of the character buffer for storing the log, the third argument is a pointer to an integer where the number of characters actually written (excluding the null terminator character) will be stored, and the fourth argument is a pointer to the character buffer for storing the log itself. Once the log is retrieved, we print it to stderr and free its memory space.

There's more...

The previous example only demonstrated compiling a vertex shader. There are several other types of shaders including fragment, geometry, and tessellation shaders. The technique for compiling is nearly identical for each shader type. The only significant difference is the argument to glCreateShader.

It is also important to note that shader compilation is only the first step. To create a working shader program, we often have at least two shaders to compile, and then the shaders must be linked together into a shader program object. We'll see the steps involved in linking in the next recipe.

Deleting a Shader Object

Shader objects can be deleted when no longer needed by calling glDeleteShader. This frees the memory used by the shader and invalidates its handle. Note that if a shader object is already attached to a program object (refer to the Linking a shader program recipe), it will not be immediately deleted, but flagged for deletion when it is detached from the program object.

See also

  • The Linking a shader program recipe