Search icon CANCEL
Subscription
0
Cart icon
Close icon
You have no products in your basket yet
Save more on your purchases!
Savings automatically calculated. No voucher code required
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletters
Free Learning
Arrow right icon
$9.99 | ALL EBOOKS & VIDEOS
Save more on purchases! Buy 2 and save 10%, Buy 3 and save 15%, Buy 5 and save 20%
OpenGL 4 Shading Language Cookbook - Third Edition
OpenGL 4 Shading Language Cookbook - Third Edition

OpenGL 4 Shading Language Cookbook: Build high-quality, real-time 3D graphics with OpenGL 4.6, GLSL 4.6 and C++17, Third Edition

By David Wolff
$39.99 $9.99
Book Sep 2018 472 pages 3rd Edition
eBook
$39.99 $9.99
Print
$48.99
Subscription
$15.99 Monthly
eBook
$39.99 $9.99
Print
$48.99
Subscription
$15.99 Monthly

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Buy Now
Table of content icon View table of contents Preview book icon Preview Book

OpenGL 4 Shading Language Cookbook - Third Edition

Chapter 1. Getting Started with GLSL

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

Introduction


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.

 

 

GLSL

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.

Profiles – core versus compatibility

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.

 

 

Using a loading library to access the latest OpenGL functionality


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.

Getting ready

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.

 

How to do it...

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:

  1. To generate the header and source files, run the following command:
glad --generator=c --out-path=GL --profile=core --api=gl=4.6
  1. The previous step will generate its output into a directory named GL. There will be two directories: GL/include and GL/src. You can move the GL directory into your project as is, or move the individual files into appropriate locations. Include GL/src/glad.c in your build, and put GL/include into your include path. Within your program code, include glad/glad.h whenever you need access to the OpenGL functions. Note that this fully replaces gl.h, so there is no need to include that. 
  2. 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!

How it works...

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.

There's more...

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.

See also

  • Theingredients/scenerunner.h file in the example code
  • GLEW, an older, popular loader and extension manager, available from http://glew.sourceforge.net

Using GLM for mathematics


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.

 

Getting ready

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.

How to do it...

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; 

How it works...

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 modelvariable (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.

There's more...

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; 

Using the GLM types as input to OpenGL

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.

See also

  • 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

Determining the GLSL and OpenGL version


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.

Note

Note that these functions must be called after the OpenGL context has been created.

How to do it...

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);

 

How it works...

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.

There's more...

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 ) );

 

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:

  1. Create the shader object:
GLuint vertShader = glCreateShader( GL_VERTEX_SHADER ); 
if( 0 == vertShader ) { 
  std::cerr << "Error creating vertex shader." << std::endl;
  exit(EXIT_FAILURE); 
} 
  1. 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 ); 
  1. Compile the shader:
glCompileShader( vertShader );

 

 

  1. 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

Linking a shader program


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):

Getting ready

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.

How to do it...

In our OpenGL initialization function, and after the compilation of shader objects referred to by vertShader and fragShader, perform the following steps:

  1. 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); 
} 
  1. Attach the shaders to the program object as follows:
glAttachShader( programHandle, vertShader ); 
glAttachShader( programHandle, fragShader ); 
  1. Link the program:
glLinkProgram( programHandle );

 

 

  1. Verify the link status:
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;
  } 
} 
  1. 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);

How it works...

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.

There's more...

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.

Shader input/output variables

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.

Note

In fact, this use of layout qualifiers to specify variable locations is required when compiling to SPIR-V (see the Loading an SPIR-V shader program recipe).

Deleting a shader program

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.

See also

  • The chapter01/scenebasic.cpp file in the example code
  • The Compiling a shader recipe
  • The Using program pipelines recipe
  • The Loading an SPIR-V shader program recipe

 

 

Saving and loading a shader binary


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.

Note

Unfortunately, Apple has deprecated OpenGL in macOS Mojave.

In this recipe, we'll outline the steps involved in saving and loading a compiled shader program.

Getting ready

We'll begin assuming that a shader program has been successfully compiled, and its ID is in the program variable.

How to do it...

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 ...
}

How it works...

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.

See also

  • The chapter01/scenebasic.cpp file in the example code
  • The Loading an SPIR-V shader program recipe

Loading a SPIR-V shader program


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.

Getting ready

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.glslandbasic.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.

 

Note

All variables used for input/output interfaces (in/out variables) must have a location assigned.

How to do it...

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.

How it works...

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

There's more...

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!

See also

  • The chapter01/scenebasic.cpp file in the example code
  • The Compiling a shader recipe
  • The Linking a shader program recipe
Left arrow icon Right arrow icon

Key benefits

Description

OpenGL 4 Shading Language Cookbook, Third Edition provides easy-to-follow recipes that first walk you through the theory and background behind each technique, and then proceed to showcase and explain the GLSL and OpenGL code needed to implement them. The book begins by familiarizing you with beginner-level topics such as compiling and linking shader programs, saving and loading shader binaries (including SPIR-V), and using an OpenGL function loader library. We then proceed to cover basic lighting and shading effects. After that, you'll learn to use textures, produce shadows, and use geometry and tessellation shaders. Topics such as particle systems, screen-space ambient occlusion, deferred rendering, depth-based tessellation, and physically based rendering will help you tackle advanced topics. OpenGL 4 Shading Language Cookbook, Third Edition also covers advanced topics such as shadow techniques (including the two of the most common techniques: shadow maps and shadow volumes). You will learn how to use noise in shaders and how to use compute shaders. The book provides examples of modern shading techniques that can be used as a starting point for programmers to expand upon to produce modern, interactive, 3D computer-graphics applications.

What you will learn

Compile, debug, and communicate with shader programs Use compute shaders for physics, animation, and general computing Learn about features such as shader storage buffer objects and image load/store Utilize noise in shaders and learn how to use shaders in animations Use textures for various effects including cube maps for reflection or refraction Understand physically based reflection models and the SPIR-V Shader binary Learn how to create shadows using shadow maps or shadow volumes Create particle systems that simulate smoke, fire, and other effects

Product Details

Country selected

Publication date : Sep 28, 2018
Length 472 pages
Edition : 3rd Edition
Language : English
ISBN-13 : 9781789342253
Category :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Buy Now

Product Details


Publication date : Sep 28, 2018
Length 472 pages
Edition : 3rd Edition
Language : English
ISBN-13 : 9781789342253
Category :

Table of Contents

17 Chapters
Title Page Chevron down icon Chevron up icon
Packt Upsell Chevron down icon Chevron up icon
Contributors Chevron down icon Chevron up icon
Preface Chevron down icon Chevron up icon
1. Getting Started with GLSL Chevron down icon Chevron up icon
2. Working with GLSL Programs Chevron down icon Chevron up icon
3. The Basics of GLSL Shaders Chevron down icon Chevron up icon
4. Lighting and Shading Chevron down icon Chevron up icon
5. Using Textures Chevron down icon Chevron up icon
6. Image Processing and Screen Space Techniques Chevron down icon Chevron up icon
7. Using Geometry and Tessellation Shaders Chevron down icon Chevron up icon
8. Shadows Chevron down icon Chevron up icon
9. Using Noise in Shaders Chevron down icon Chevron up icon
10. Particle Systems and Animation Chevron down icon Chevron up icon
11. Using Compute Shaders Chevron down icon Chevron up icon
1. Other Books You May Enjoy Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon

Customer reviews

Top Reviews
Rating distribution
Empty star icon Empty star icon Empty star icon Empty star icon Empty star icon 0
(0 Ratings)
5 star 0%
4 star 0%
3 star 0%
2 star 0%
1 star 0%
Filter icon Filter
Top Reviews

Filter reviews by


No reviews found
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.