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.
Note
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 precompiled shader programs (see the Saving and loading a shader binary recipe). OpenGL 4.6 added the ability to load shader programs compiled to (or written in) SPIR-V, an intermediate language for defining shaders. See the Loading an SPIR-V shader recipe later in this chapter.
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:
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.glsl
:
#version 460 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 VertexPosition
and VertexColor
input attributes and passes them to the fragment shader via the gl_Position
and Color
output variables.
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, and 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 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 loading library to access the latest OpenGL functionality recipe).
Finally, load the shader source code into std::string
(or the char array). The following example assumes that the shaderCode
variable is std::string
containing the shader source code.
How to do it...
To compile a shader, use the following steps:
- Create the shader object:
GLuint vertShader = glCreateShader( GL_VERTEX_SHADER ); if( 0 == vertShader ) { std::cerr << "Error creating vertex shader." << std::endl; exit(EXIT_FAILURE); }
- Copy the source code into the shader object:
std::string shaderCode = loadShaderAsString("basic.vert.glsl"); const GLchar * codeArray[] = { shaderCode.c_str() }; glShaderSource( vertShader, 1, codeArray, NULL );
- Compile the shader:
glCompileShader( vertShader );
- Verify the compilation status:
GLint result; glGetShaderiv( vertShader, GL_COMPILE_STATUS, &result ); if( GL_FALSE == result ) { std::cerr << "Vertex shader compilation failed!" << std::endl; // Get and print the info log GLint logLen; glGetShaderiv(vertShader, GL_INFO_LOG_LENGTH, &logLen); if( logLen > 0 ) { std::string log(logLen, ' '); GLsizei written; glGetShaderInfoLog(vertShader, logLen, &written, &log[0]); std::cerr << "Shader log: " << std::endl << 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 vertShader
variable. 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 glShaderSource
function. 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 contain 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 the 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 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 the compilation was successful.
If the compile status is GL_FALSE
, 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 logLen
variable. 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 how to compile 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. Similar to a language like C++, we need to link the program. While shader programs can consist of a single shader, for many use cases we have to compile two or more shaders, 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
chapter01/scenebasic.cpp
file in the example code - The Linking a shader program recipe