In this chapter, we will cover the following recipes:
- Using a loading library to access the latest OpenGL functionality
- Using GLM for mathematics
- Determining the GLSL and OpenGL version
- Compiling a shader
- Linking a shader program
- Saving and loading a shader binary
- Loading a SPIR-V shader program
The OpenGL Shading Language (GLSL) Version 4 brings unprecedented power and flexibility to programmers interested in creating modern, interactive, and graphical programs. It allows us to harness the power of modern Graphics Processing Units (GPUs) in a straightforward way by providing a simple yet powerful language and API. Of course, the first step toward using GLSL is to create a program that utilizes the OpenGL API. GLSL programs don't stand on their own; they must be a part of a larger OpenGL program. In this chapter, we will provide some tips and techniques for getting started. We'll cover how to load, compile, link, and export a GLSL shader program. First, let's start with some background.
The GLSL is a fundamental and integral part of the OpenGL API. Every program written using the OpenGL API will internally utilize one or several GLSL programs. These "mini-programs" are referred to as shader programs. A shader program usually consists of several components called shaders. Each shader executes within a different stage of the OpenGL pipeline. Each shader runs on the GPU, and as the name implies, they (typically) implement the algorithms related to lighting and shading effects. However, shaders are capable of doing much more than just shading. They can perform animation, generate additional geometry, tessellate geometry, or even perform generalized computation.
Note
The field of study called General Purpose Computing on Graphics Processing Units (GPGPU) is concerned with the utilization of GPUs (often using specialized APIs such as CUDA or OpenCL) to perform general-purpose computations such as fluid dynamics, molecular dynamics, and cryptography. With compute shaders, introduced in OpenGL 4.3, we can now do GPGPU within OpenGL. See Chapter 11, Using Compute Shaders, for details about using compute shaders.
Shader programs are designed for direct execution on the GPU and are executed in parallel. For example, a fragment shader might be executed once for every pixel, with each execution running simultaneously. The number of processors on the graphics card determines how many can be executed at one time. This makes shader programs incredibly efficient, and provides the programmer with a simple API for implementing highly-parallel computation.
Shader programs form essential parts of the OpenGL pipeline. Prior to OpenGL Version 2.0, the shading algorithm was hardcoded into the pipeline and had only limited configurability. When we wanted to implement custom effects, we used various tricks to force the fixed-function pipeline into being more flexible than it really was. With the advent of GLSL, we now have the ability to replace this hardcoded functionality with our own programs written in GLSL, thus giving us a great deal of additional flexibility and power. For more details on this programmable pipeline, see the introduction to Chapter 3, The Basics of GLSL Shaders.
In fact, OpenGL Version 3.2 and above not only provide this capability, but they require shader programs as part of every OpenGL program. The old fixed-function pipeline has been deprecated in favor of a new programmable pipeline, a key part of which is the shader program written in GLSL.
OpenGL Version 3.0 introduced a deprecation model, which allowed for the gradual removal of functions from the OpenGL specification. Functions or features can be marked as deprecated, meaning that they are expected to be removed from a future version of OpenGL. For example, immediate mode-rendering using glBegin
/glEnd
was marked as deprecated in version 3.0 and removed in version 3.1.
In order to maintain backwards compatibility, compatibility profiles were introduced with OpenGL 3.2. A programmer that is writing code for a particular version of OpenGL (with older features removed) would use the core profile. Those who wanted to maintain compatibility with older functionality could use the compatibility profile.
Note
It may be somewhat confusing that there is also the concept of a forward-compatible context, which is distinguished slightly from the concept of a core/compatibility profile. A context that is considered forward-compatible basically indicates that all deprecated functionality has been removed. In other words, if a context is forward-compatible, it only includes functions that are in the core, but not those that were marked as deprecated. Some Windows APIs provide the ability to select a forward-compatible status along with the profile.
The steps for selecting a core or compatibility profile depend on the Windows system's API. For example, with GLFW, one can select a forward-compatible, 4.6 core profile using the following code:
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow *window = glfwCreateWindow(800, 600, "Title", nullptr, nullptr);
All programs in this book are designed to be compatible with a forward-compatible OpenGL 4.6 core profile. However, many of them can be used with older versions or even compatibility profiles.
The OpenGL application binary interface (ABI) is frozen to OpenGL Version 1.1 on Windows. Unfortunately for Windows developers, that means that it is not possible to link directly to functions that are provided in newer versions of OpenGL. Instead, one must gain access to the OpenGL functions by acquiring a function pointer at runtime. Getting access to the function pointers is not difficult, but requires somewhat tedious work, and has a tendency to clutter code. Additionally, Windows typically comes with a standard OpenGL gl.h
file that also conforms to OpenGL 1.1.
The OpenGL wiki states that Microsoft has no plans to ever update the gl.h
and opengl32.lib
that come with their compilers. Thankfully, others have provided libraries that manage all of this for us by transparently providing the needed function pointers, while also exposing the needed functionality in header files. Such a library is called an OpenGL Loading Library (or OpenGL function loader), and there are several such libraries available. One of the oldest is OpenGL Extension Wrangler (GLEW). However, there are a few issues with GLEW. First, it provides one large header file that includes everything from all versions of OpenGL. It might be preferable to have a more streamlined header file that only includes functions that we might use. Second, GLEW is distributed as a library that needs to be compiled separately and linked into our project. I find it preferable to have a loader that can be included into a project simply by adding the source files and compiling them directly into our executable, avoiding the need to support another link-time dependency.
In this recipe, we'll use a loader generator named GLAD, available from https://github.com/Dav1dde/glad. This very flexible and efficient library can generate a header that includes only the needed functionality, and also generates just a few files (a source file and a few headers) that we can add directly into our project.
To use GLAD, you can either download and install it using pip
(or from https://github.com/Dav1dde/glad ),or you canuse the web service available here: http://glad.dav1d.de/. If you choose to install it, you'll need Python. The install is simple and described in detail on the GitHub page.
The first step is to generate the header and source files for the OpenGL version and profile of your choice. For this example, we'll generate files for an OpenGL 4.6 core profile. We can then copy the files into our project and compile them directly alongside our code:
- To generate the header and source files, run the following command:
glad --generator=c --out-path=GL --profile=core --api=gl=4.6
- The previous step will generate its output into a directory named
GL
. There will be two directories:GL/include
andGL/src
. You can move the GL directory into your project as is, or move the individual files into appropriate locations. IncludeGL/src/glad.c
in your build, and putGL/include
into yourinclude
path. Within your program code, includeglad/glad.h
whenever you need access to the OpenGL functions. Note that this fully replacesgl.h
, so there is no need to include that. - In order to initialize the function pointers, you need to make sure to call a function that does so. The needed function is
gladLoadGL()
. Somewhere just after the GL context is created (typically in an initialization function), and before any OpenGL functions are called, use the following code:
if(!gladLoadGL()) { std::cerr << "Unable to load OpenGL functions!" << std::endl; exit(EXIT_FAILURE); }
That's all there is to it!
The command in step 1 generates a few header files and a source file. The header provides prototypes for all of the selected OpenGL functions, redefines them as function pointers, and defines all of the OpenGL constants as well. The source file provides initialization code for the function pointers as well as some other utility functions. We can include the glad/glad.h
header file wherever we need prototypes for OpenGL functions, so all function entry points are available at compile time. At runtime, the gladLoadGL()
call will initialize all available function pointers.
Note
Some function pointers may not be successfully initialized. This might happen if your driver does not support the requested OpenGL version. If that happens, calling the functions will fail.
The command-line arguments available to GLAD are fully documented on the GitHub site and are available via glad -h
. One can select any OpenGL version, select core/compatibility profiles, include desired extensions, and/or create debug callbacks.
GLAD provides a web service at http://glad.dav1d.de/ that makes it easy to generate the loader source and header files without installing GLAD. Simply visit the URL, select the desired configuration, and the loader files will be generated and downloaded.
- The
ingredients/scenerunner.h
file in the example code - GLEW, an older, popular loader and extension manager, available from http://glew.sourceforge.net
Mathematics is the core to all of computer graphics. In earlier versions, OpenGL provided support for managing coordinate transformations and projections using the standard matrix stacks (GL_MODELVIEW
and GL_PROJECTION
). In modern versions of core OpenGL however, all of the functionality supporting the matrix stacks has been removed. Therefore, it is up to us to provide our own support for the usual transformation and projection matrices, and then pass them into our shaders. Of course, we could write our own matrix and vector classes to manage this, but some might prefer to use a ready-made, robust library.
One such library is OpenGL Mathematics (GLM), written by Christophe Riccio. Its design is based on the GLSL specification, so the syntax will be familiar to anyone using GLSL. Additionally, it provides extensions that include functionality similar to some of the much-missed OpenGL utility functions, such as glOrtho
, glRotate
, or gluLookAt
.
Since GLM is a header-only library, the installation is simple. Download the latest GLM distribution from http://glm.g-truc.net. Then, unzip the archive file, and copy the glm
directory contained inside to anywhere in your compiler's include path.
To use the GLM libraries, include the core header file, and headers for any extensions. For this example, we'll include the matrix transform extension:
#include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp>
The GLM classes are available in the glm
namespace. The following is an example of how you might go about making use of some of them:
glm::vec4 position = glm::vec4( 1.0f, 0.0f, 0.0f, 1.0f ); glm::mat4 view = glm::lookAt( glm::vec3(0.0f, 0.0f, 5.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f) ); glm::mat4 model(1.0f); // The identity matrix model = glm::rotate( model, 90.0f, glm::vec3(0.0f,1.0f,0.0) ); glm::mat4 mv = view * model; glm::vec4 transformed = mv * position;
The GLM library is a header-only library. All of the implementation is included within the header files. It doesn't require separate compilation and you don't need to link your program to it. Just placing the header files in your include path is all that's required!
The previous example first creates vec4
(a four-component vector), which represents a position. Then, it creates a 4 x 4 view matrix by using the glm::lookAt
function. This works in a similar fashion to the old gluLookAt
function. Here, we set the camera's location at (0, 0, 5), looking toward the origin, with the up direction in the direction of the positive y axis. We then go on to create the model matrix by first storing the identity matrix in the model
variable (via the single-argument constructor), and multiplying it by a rotation matrix using the glm::rotate
function.
The multiplication here is implicitly done by the glm::rotate
function. It multiplies its first parameter by the rotation matrix (on the right) that is generated by the function. The second parameter is the angle of rotation (in degrees), and the third parameter is the axis of rotation. Since before this statement, model
is the identity matrix, the net result is that model
becomes a rotation matrix of 90 degrees around the y axis.
Finally, we create our model-view matrix (mv
) by multiplying the view
and model
variables, and then use the combined matrix to transform the position. Note that the multiplication operator has been overloaded to behave in the expected way.
Note
The order is important here. Typically, the model matrix represents a transformation from object space to world space, and the view matrix is a transformation from world space to camera space. So to get a single matrix that transforms from object space to camera space, we want the model matrix to apply first. Therefore, the model matrix is multiplied on the right-hand side of the view matrix.
It is not recommended to import all of the GLM namespaces using the following command:
using namespace glm;
This will most likely cause a number of namespace clashes. Instead, it is preferable to import symbols one at a time with the using
statements as needed. For example:
#include <glm/glm.hpp> using glm::vec3; using glm::mat4;
GLM supports directly passing a GLM type to OpenGL using one of the OpenGL vector functions (with the v
suffix). For example, to pass mat4
named proj
to OpenGL, we can use the following code:
glm::mat4 proj = glm::perspective( viewAngle, aspect, nearDist, farDist ); glUniformMatrix4fv(location, 1, GL_FALSE, &proj[0][0]);
Alternatively, rather than using the ampersand operator, we can use the glm::value_ptr
function to get a pointer to the content of the GLM type:
glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(proj));
The latter version requires including the header file glm/gtc/type_ptr.hpp
. The use of value_ptr
is arguably cleaner, and works for any GLM type.
- The Qt SDK includes many classes for vector/matrix mathematics, and is another good option if you're already using Qt
- The GLM website (http://glm.g-truc.net) has additional documentation and examples
In order to support a wide range of systems, it is essential to be able to query for the supported OpenGL and GLSL version of the current driver. It is quite simple to do so, and there are two main functions involved: glGetString
and glGetIntegerv
.
The following code will print the version information to stdout
:
const GLubyte *renderer = glGetString( GL_RENDERER ); const GLubyte *vendor = glGetString( GL_VENDOR ); const GLubyte *version = glGetString( GL_VERSION ); const GLubyte *glslVersion = glGetString( GL_SHADING_LANGUAGE_VERSION ); GLint major, minor; glGetIntegerv(GL_MAJOR_VERSION, &major); glGetIntegerv(GL_MINOR_VERSION, &minor); printf("GL Vendor : %s\n", vendor); printf("GL Renderer : %s\n", renderer); printf("GL Version (string) : %s\n", version); printf("GL Version (integer) : %d.%d\n", major, minor); printf("GLSL Version : %s\n", glslVersion);
Note that there are two different ways to retrieve the OpenGL version: using glGetString
and glGetIntegerv
. The former can be useful for providing readable output, but may not be as convenient for programmatically checking the version because of the need to parse the string. The string provided by glGetString(GL_VERSION)
should always begin with the major and minor versions separated by a dot, however, the minor version could be followed with a vendor-specific build number. Additionally, the rest of the string can contain additional vendor-specific information and may also include information about the selected profile (see the Introduction section of this chapter). It is important to note that the use of glGetIntegerv
to query for version information requires OpenGL 3.0 or greater.
The queries for GL_VENDOR
and GL_RENDERER
provide additional information about the OpenGL driver. The glGetString(GL_VENDOR)
call returns the company responsible for the OpenGL implementation. The call to glGetString(GL_RENDERER)
provides the name of the renderer, which is specific to a particular hardware platform (such as the ATI Radeon HD 5600 Series). Note that both of these do not vary from release to release, so they can be used to determine the current platform.
Of more importance to us in the context of this book is the call to glGetString(GL_SHADING_LANGUAGE_VERSION)
, which provides the supported GLSL version number. This string should begin with the major and minor version numbers separated by a period, but similar to the GL_VERSION
query, may include other vendor-specific information.
It is often useful to query for the supported extensions of the current OpenGL implementation. Extension names are indexed and can be individually queried by index. We use the glGetStringi
variant for this. For example, to get the name of the extension stored at index i
, we use glGetStringi(GL_EXTENSIONS, i)
. To print a list of all extensions, we could use the following code:
GLint nExtensions; glGetIntegerv(GL_NUM_EXTENSIONS, &nExtensions); for( int i = 0; i < nExtensions; i++ ) printf("%s\n", glGetStringi( GL_EXTENSIONS, i ) );
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:

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.
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); }
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; } }
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.
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.
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.
Once we have compiled our shaders and before we can actually install them into the OpenGL pipeline, we need to link them together into a shader program. Among other things, the linking step involves making the connections between input variables from one shader to output variables of another, and making the connections between the input/output variables of a shader to appropriate locations in the OpenGL environment.
Linking involves steps that are similar to those involved in compiling a shader. We attach each shader object to a new shader program object and then tell the shader program object to link (making sure that the shader objects are compiled before linking):

For this recipe, we'll assume that you've already compiled two shader objects whose handles are stored in the vertShader
and fragShader
variables.
For this and a few other recipes in this chapter, we'll use the following source code for the fragment shader:
#version 460 in vec3 Color; out vec4 FragColor; void main() { FragColor = vec4(Color, 1.0); }
For the vertex shader, we'll use the source code from the previous recipe, Compiling a shader.
In our OpenGL initialization function, and after the compilation of shader objects referred to by vertShader
and fragShader
, perform the following steps:
- Create the program object using the following code:
GLuint programHandle = glCreateProgram(); if( 0 == programHandle ) { std::cerr << "Error creating program object." << std::endl; exit(EXIT_FAILURE); }
- Attach the shaders to the program object as follows:
glAttachShader( programHandle, vertShader ); glAttachShader( programHandle, fragShader );
- Link the program:
glLinkProgram( programHandle );
GLint status; glGetProgramiv( programHandle, GL_LINK_STATUS, &status ); if( GL_FALSE == status ) { std::cerr << "Failed to link shader program!" << std::endl; GLint logLen; glGetProgramiv(programHandle, GL_INFO_LOG_LENGTH, &logLen); if( logLen > 0 ) { std::string(logLen, ' '); GLsizei written; glGetProgramInfoLog(programHandle, logLen, &written, &log[0]); std::cerr << "Program log: " << std::endl << log; } }
- If linking is successful, we can install the program into the OpenGL pipeline with
glUseProgram
:
else glUseProgram( programHandle );
Regardless of whether the link was successful, it is a good idea to clean up our shader objects. Once the program is linked, they are not needed anymore:
// Detach and delete shader objects glDetachShader(programHandle, vertShader); glDetachShader(programHandle, fragShader); glDeleteShader(vertShader); glDeleteShader(fragShader);
We start by calling glCreateProgram
to create an empty program object. This function returns a handle to the program object, which we store in a variable named programHandle
. If an error occurs with program creation, the function will return 0. We check for that, and if it occurs, we print an error message and exit.
Next, we attach each shader to the program object using glAttachShader
. The first argument is the handle to the program object, and the second is the handle to the shader object to be attached.
Then, we link the program by calling glLinkProgram
, providing the handle to the program object as the only argument. As with compilation, we check for the success or failure of the link, with the subsequent query.
We check the status of the link by calling glGetProgramiv
. Similar to glGetShaderiv
, glGetProgramiv
allows us to query various attributes of the shader program. In this case, we ask for the status of the link by providing GL_LINK_STATUS
as the second argument. The status is returned in the location pointed to by the third argument, in this case named status
.
The link status is either GL_TRUE
or GL_FALSE
, indicating the success or failure of the link. If the value of the status is GL_FALSE
, we retrieve and display the program information log, which should contain additional information and error messages. The program log is retrieved by the call to glGetProgramInfoLog
. The first argument is the handle to the program object, the second is the size of the buffer to contain the log, the third is a pointer to a GLsizei
variable where the number of bytes written to the buffer will be stored (excluding the null terminator), and the fourth is a pointer to the buffer that will store the log. The buffer can be allocated based on the size returned by the call to glGetProgramiv
with the GL_INFO_LOG_LENGTH
parameter. The string that is provided in log
will be properly null terminated.
Finally, if the link is successful, we install the program into the OpenGL pipeline by calling glUseProgram
, providing the handle to the program as the argument.
It is a good idea to detach and delete the shader object, regardless of whether the link is successful. However, if the shader objects might be needed to link another program, you should detach it from this program and skip deletion until later.
With the simple fragment shader from this recipe and the vertex shader from the previous recipe compiled, linked, and installed into the OpenGL pipeline, we have a complete OpenGL pipeline and are ready to begin rendering. Drawing a triangle and supplying different values for the Color
attribute yields an image of a multi-colored triangle where the vertices are red, green, and blue, and inside the triangle, the three colors are interpolated, causing a blending of colors throughout:

Note
For details on how to render the triangle, see Chapter 2, Working with GLSL Programs.
You can use multiple shader programs within a single OpenGL program. They can be swapped in and out of the OpenGL pipeline by calling glUseProgram
to select the desired program.
You may have noticed that the Color
variable is used to send data from the vertex shader to the fragment shader. There is an output variable (out vec3
) in the vertex shader and an input variable (in vec3
) in the fragment shader, both with the same name. The value that the fragment shader receives is a value that is interpolated from the values of the corresponding output variable for each of the vertices (hence the blended colors in the earlier image). This interpolation is automatically done by hardware rasterizer before the execution of the fragment stage.
When linking a shader program, OpenGL makes the connections between input and output variables in the vertex and fragment shaders (among other things). If a vertex shader's output variable has the same name and type as a fragment shader's input variable, OpenGL will automatically link them together.
It is possible to connect (link) variables that do not have the same name or type by using layout qualifiers. With a layout qualifier, we can specify the location for each variable specifically. For example, suppose that I used this set of output variables in my vertex shader:
layout (location=0) out vec4 VertColor; layout (location=1) out vec3 VertNormal;
I could use these variables in the fragment shader:
layout (location=0) in vec3 Color; layout (location=1) in vec3 Normal;
Despite the fact that these have different names (and for Color
, types), they will be connected by the linker when the program is linked due to the fact that they are assigned the same locations. In this example, VertColor
will be linked to Color
, and VertNormal
will be linked to Normal
. This makes things more convenient. We're not required to use the same names for input/output variables, which gives us the flexibility to use names that might be more descriptive in each shader stage. More importantly, it is part of a larger framework, called separate shader objects. A full example of separate shader objects can be found in the Using program pipelines recipe.
If a program is no longer needed, it can be deleted from OpenGL memory by calling glDeleteProgram
, providing the program handle as the only argument. This invalidates the handle and frees the memory used by the program. Note that if the program object is currently in use, it will not be immediately deleted, but will be flagged for deletion when it is no longer in use.
Also, the deletion of a shader program detaches the shader objects that were attached to the program but does not delete them unless those shader objects have already been flagged for deletion by a previous call to glDeleteShader
. Therefore, as mentioned before, it is a good idea to detach and delete them immediately, as soon as the program is linked, to avoid accidentally leaking shader objects.
OpenGL 4.1 introduced the glGetProgramBinary
and glProgramBinary
functions, which allow us to save and load compiled shader program binaries. Note that this functionality is still quite dependent on the OpenGL driver, and is not widely supported. For example, the Intel drivers on macOS do not support any binary formats.
In this recipe, we'll outline the steps involved in saving and loading a compiled shader program.
We'll begin assuming that a shader program has been successfully compiled, and its ID is in the program
variable.
To save the shader binary, first verify that the driver supports at least one shader binary format:
GLint formats = 0; glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &formats); if( formats < 1 ) { std::cerr << "Driver does not support any binary formats." << std::endl; exit(EXIT_FAILURE); }
Then, assuming at least one binary format is available, use glGetProgramBinary
to retrieve the compiled shader code and write it to a file:
// Get the binary length GLint length = 0; glGetProgramiv(program, GL_PROGRAM_BINARY_LENGTH, &length); // Retrieve the binary code std::vector<GLubyte> buffer(length); GLenum format = 0; glGetProgramBinary(program, length, NULL, &format, buffer.data()); // Write the binary to a file. std::string fName("shader.bin"); std::cout << "Writing to " << fName << ", binary format = " <<format << std::endl; std::ofstream out(fName.c_str(), std::ios::binary); out.write( reinterpret_cast<char *>(buffer.data()), length ); out.close();
To load and use a shader binary, retrieve the compiled program from storage, and use glProgramBinary
to load it into the OpenGL context:
GLuint program = glCreateProgram(); // Load binary from file std::ifstream inputStream("shader.bin", std::ios::binary); std::istreambuf_iterator<char> startIt(inputStream), endIt; std::vector<char> buffer(startIt, endIt); // Load file inputStream.close(); // Install shader binary glProgramBinary(program, format, buffer.data(), buffer.size() ); // Check for success/failure GLint status; glGetprogramiv(program, GL_LINK_STATUS, &status); if( GL_FALSE == status ) { // Handle failure ... }
Drivers can support zero or more binary formats. The call to glGetIntegerv
with the GL_NUM_PROGRAM_BINARY_FORMATS
constant queries the driver to see how many are available. If this number is zero, the OpenGL driver does not support reading or writing shader binaries. If the value is one or more, we're good to go.
If at least one binary format is available, we can use glGetProgramBinary
to retrieve the compiled shader code shown earlier. The function will write the binary format used to the location pointed to by the fourth parameter. In the preceding example, the data is stored in the vector named buffer
.
To load the shader binary, we can use glProgramBinary
. This function will load a previously saved shader binary. It requires the binary format to be passed as the second parameter. We can then check GL_LINK_STATUS
to verify that it was loaded without error.
Standard, Portable Intermediate Representation - V (SPIR-V) is an intermediate language designed and standardized by the Khronos Group for shaders. It is intended to be a compiler target for a number of different languages. In the Vulkan API, shaders are required to be compiled to SPIR-V before they can be loaded. SPIR-V is intended to provide developers with the freedom to develop their shaders in any language they want (as long as it can be compiled to SPIR-V), and avoid the need for an OpenGL (or Vulkan) implementation to provide compilers for multiple languages.
Support for SPIR-V shader binaries was added to OpenGL core with version 4.6, but is also available via the ARB_gl_spirv
extension for earlier OpenGL versions.
Currently, the Khronos Group provides a reference compiler for compiling GLSL to SPIR-V. It is available on GitHub at https://github.com/KhronosGroup/glslang.
In this recipe, we'll go through the steps involved in precompiling a GLSL shader pair to SPIR-V, and then load it into an OpenGL program.
Download and compile the OpenGL shader validator from https://github.com/KhronosGroup/glslang. Make sure that the glslangValidator
binary is available in your PATH
command line. In this example, we'll use the shader pair located in the basic.vert.glsl
andbasic.frag.glsl
files.
Note that you'll need to use explicit locations for all of your input/output variables in the shaders. For details, see the Linking a shader program recipe.
Start by compiling the shader pair into SPIR-V using the glslangValidator
tool:
glslangValidator -G -o basic.vert.spv basic.vert.glsl glslangValidator -G -o basic.frag.spv basic.frag.glsl
If successful, this produces the basic.vert.spv
and basic.frag.spv
SPIR-V output files.
To load your SPIR-V shaders into an OpenGL program, use glShaderBinary
and glSpecializeShader
. With glShaderBinary
, use GL_SHADER_BINARY_FORMAT_SPIR_V
as the binary format:
GLuint vertShader = glCreateShader(GL_VERTEX_SHADER); // Load the shader into a std::vector std::ifstream inStream("basic.vert.spv", std::ios::binary); std::istreambuf_iterator<char> startIt(inStream), endIt; std::vector<char> buffer(startIt, endIt); inStream.close(); // Load using glShaderBinary glShaderBinary(1, &vertShader, GL_SHADER_BINARY_FORMAT_SPIR_V, buffer.data(), buffer.size()); // Specialize the shader (specify the entry point) glSpecializeShader( vertShader, "main", 0, 0, 0); // Check for success/failure GLint status; glGetShaderiv(vertShader, GL_COMPILE_STATUS, &status); if( GL_FALSE == status ) { // Loading failed... }
The process is nearly exactly the same for the fragment shader; just use GL_FRAGMENT_SHADER
instead of GL_VERTEX_SHADER
on the first line.
Finally, we create the program object, attach the shaders, and link. This process is identical to that shown in the Linking a shader program recipe, so we won't reproduce it here.
The glShaderBinary
function provides us with the ability to load shaders that have been compiled to the SPIR-V format. This part is fairly straightforward.
The function that might be a bit more confusing is glSpecializeShader
. We are required to call this function before the shader stage can be linked. This call is needed because a single SPIR-V file can have multiple entry points, and SPIR-V files can have specialization constants, which are parameters that the user can provide before it is compiled into native code.
At a minimum, we need to define the entry point for our shader. Since the source language is GLSL, the entry point is main
. We specify the entry point(s) via the second argument. For GLSL, we simply use the main
constant string. The last three parameters can be used to define the specialization constants. The first of the three is the number of constants, the next is a pointer to an array of constant indices, and the last is a pointer to an array of constant values.
The process of specializing an SPIR-V shader is similar to compiling a GLSL shader. Before calling glSpecializeShader
, or if specialization fails, the compile status will be GL_FALSE
. If specialization succeeds, the compile status will be GL_TRUE
. As with GLSL shaders, we can query the shader info log to get detailed error messages (see the Compiling a shader recipe).
SPIR-V appears to be the future of shader programming in the Vulkan/OpenGL space. However, GLSL is not going away anytime soon. GLSL compilers still ship with OpenGL and there's currently no sign that they will be removed or deprecated. The OpenGL specification still considers GLSL to be the primary shading language.
However, if you're interested in getting on board with SPIR-V early, or you have an interest in moving toward Vulkan, it might be valuable to you to start working with SPIR-V now in OpenGL. Fortunately, that's possible, at least in recent versions of OpenGL.
The future of SPIR-V is very bright. There is already a (mostly complete) compiler for HLSL that targets SPIR-V, and it is likely that other languages will be developed soon. It's an exciting time for shader programming!