Book Image

OpenGL 4 Shading Language Cookbook - Second Edition

By : David Wolff, David A Wolff
Book Image

OpenGL 4 Shading Language Cookbook - Second Edition

By: David Wolff, David A Wolff

Overview of this book

OpenGL Shading Language (GLSL) is a programming language used for customizing parts of the OpenGL graphics pipeline that were formerly fixed-function, and are executed directly on the GPU. It provides programmers with unprecedented flexibility for implementing effects and optimizations utilizing the power of modern GPUs. With Version 4, the language has been further refined to provide programmers with greater power and flexibility, with new stages such as tessellation and compute. OpenGL 4 Shading Language Cookbook provides easy-to-follow examples that first walk you through the theory and background behind each technique, and then go on to provide and explain the GLSL and OpenGL code needed to implement it. Beginner level through to advanced techniques are presented including topics such as texturing, screen-space techniques, lighting, shading, tessellation shaders, geometry shaders, compute shaders, and shadows. OpenGL Shading Language 4 Cookbook is a practical guide that takes you from the fundamentals of programming with modern GLSL and OpenGL, through to advanced techniques. The recipes build upon each other and take you quickly from novice to advanced level code. You'll see essential lighting and shading techniques; examples that demonstrate how to make use of textures for a wide variety of effects and as part of other techniques; examples of screen-space techniques including HDR rendering, bloom, and blur; shadowing techniques; tessellation, geometry, and compute shaders; how to use noise effectively; and animation with particle systems. OpenGL Shading Language 4 Cookbook 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.
Table of Contents (17 chapters)
OpenGL 4 Shading Language Cookbook Second Edition
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Sending data to a shader using vertex attributes and vertex buffer objects


The vertex shader is invoked once per vertex. Its main job is to process the data associated with the vertex, and pass it (and possibly other information) along to the next stage of the pipeline. In order to give our vertex shader something to work with, we must have some way of providing (per-vertex) input to the shader. Typically, this includes the vertex position, normal vector, and texture coordinates (among other things). In earlier versions of OpenGL (prior to 3.0), each piece of vertex information had a specific "channel" in the pipeline. It was provided to the shaders using functions such as glVertex, glTexCoord, and glNormal (or within client vertex arrays using glVertexPointer, glTexCoordPointer, or glNormalPointer). The shader would then access these values via built-in variables such as gl_Vertex and gl_Normal. This functionality was deprecated in OpenGL 3.0 and later removed. Instead, vertex information must now be provided using generic vertex attributes, usually in conjunction with (vertex) buffer objects. The programmer is now free to define an arbitrary set of per-vertex attributes to provide as input to the vertex shader. For example, in order to implement normal mapping, the programmer might decide that position, normal vector and tangent vector should be provided along with each vertex. With OpenGL 4, it's easy to define this as the set of input attributes. This gives us a great deal of flexibility to define our vertex information in any way that is appropriate for our application, but may require a bit of getting used to for those of us who are used to the old way of doing things.

In the vertex shader, the per-vertex input attributes are defined by using the GLSL qualifier in. For example, to define a 3-component vector input attribute named VertexColor, we use the following code:

in vec3 VertexColor;

Of course, the data for this attribute must be supplied by the OpenGL program. To do so, we make use of vertex buffer objects. The buffer object contains the values for the input attribute. In the main OpenGL program we make the connection between the buffer and the input attribute and define how to "step through" the data. Then, when rendering, OpenGL pulls data for the input attribute from the buffer for each invocation of the vertex shader.

For this recipe, we'll draw a single triangle. Our vertex attributes will be position and color. We'll use a fragment shader to blend the colors of each vertex across the triangle to produce an image similar to the one shown as follows. The vertices of the triangle are red, green, and blue, and the interior of the triangle has those three colors blended together. The colors may not be visible in the printed text, but the variation in the shade should indicate the blending.

Getting ready

We'll start with an empty OpenGL program, and the following shaders:

The vertex shader (basic.vert):

#version 430

layout (location=0) in vec3 VertexPosition;
layout (location=1) in vec3 VertexColor;

out vec3 Color;

void main()
{
  Color = VertexColor;

  gl_Position = vec4(VertexPosition,1.0);
}

Attributes are the input variables to a vertex shader. In the previous code, there are two input attributes: VertexPosition and VertexColor. They are specified using the GLSL keyword in. Don't worry about the layout prefix, we'll discuss that later. Our main OpenGL program needs to supply the data for these two attributes for each vertex. We will do so by mapping our polygon data to these variables.

It also has one output variable named Color, which is sent to the fragment shader. In this case, Color is just an unchanged copy of VertexColor. Also, note that the attribute VertexPosition is simply expanded and passed along to the built-in output variable gl_Position for further processing.

The fragment shader (basic.frag):

#version 430

in vec3 Color;

out vec4 FragColor;

void main() {
  FragColor = vec4(Color, 1.0);
}

There is just one input variable for this shader, Color. This links to the corresponding output variable in the vertex shader, and will contain a value that has been interpolated across the triangle based on the values at the vertices. We simply expand and copy this color to the output variable FragColor (more about fragment shader output variables in later recipes).

Write code to compile and link these shaders into a shader program (see "Compiling a shader" and "Linking a shader program"). In the following code, I'll assume that the handle to the shader program is programHandle.

How to do it...

Use the following steps to set up your buffer objects and render the triangle:

  1. Create a global (or private instance) variable to hold our handle to the vertex array object:

    GLuint vaoHandle;
  2. Within the initialization function, we create and populate the vertex buffer objects for each attribute:

    float positionData[] = {
          -0.8f, -0.8f, 0.0f,
          0.8f, -0.8f, 0.0f,
          0.0f,  0.8f, 0.0f };
    float colorData[] = {
          1.0f, 0.0f, 0.0f,
          0.0f, 1.0f, 0.0f,
          0.0f, 0.0f, 1.0f };
    
    // Create and populate the buffer objects
    GLuint vboHandles[2];
    glGenBuffers(2, vboHandles);
    GLuint positionBufferHandle = vboHandles[0];
    GLuint colorBufferHandle = vboHandles[1];
    
    // Populate the position buffer
    glBindBuffer(GL_ARRAY_BUFFER, positionBufferHandle);
    glBufferData(GL_ARRAY_BUFFER, 9 * sizeof(float), positionData, GL_STATIC_DRAW);
    
    // Populate the color buffer
    glBindBuffer(GL_ARRAY_BUFFER, colorBufferHandle);
    glBufferData(GL_ARRAY_BUFFER, 9 * sizeof(float), colorData, GL_STATIC_DRAW);
  3. Create and define a vertex array object, which defines the relationship between the buffers and the input attributes. (See "There's more…" for an alternate way to do this that is valid for OpenGL 4.3 and later.)

    // Create and set-up the vertex array object
    glGenVertexArrays( 1, &vaoHandle );
    glBindVertexArray(vaoHandle);
    
    // Enable the vertex attribute arrays
    glEnableVertexAttribArray(0);  // Vertex position
    glEnableVertexAttribArray(1);  // Vertex color
    
    // Map index 0 to the position buffer
    glBindBuffer(GL_ARRAY_BUFFER, positionBufferHandle);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
    
    // Map index 1 to the color buffer
    glBindBuffer(GL_ARRAY_BUFFER, colorBufferHandle);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  4. In the render function, we bind to the vertex array object and call glDrawArrays to initiate rendering:

    glBindVertexArray(vaoHandle);
    glDrawArrays(GL_TRIANGLES, 0, 3 );

How it works...

Vertex attributes are the input variables to our vertex shader. In the given vertex shader, our two attributes are VertexPosition and VertexColor. The main OpenGL program refers to vertex attributes by associating each (active) input variable with a generic attribute index. These generic indices are simply integers between 0 and GL_MAX_VERTEX_ATTRIBS – 1. We can specify the relationship between these indices and the attributes using the layout qualifier. For example, in our vertex shader, we use the layout qualifier to assign VertexPosition to attribute index 0 and VertexColor to attribute index 1.

layout (location = 0) in vec3 VertexPosition;
layout (location = 1) in vec3 VertexColor;

We refer to the vertex attributes in our OpenGL code, by referring to the corresponding generic vertex attribute index.

Note

It is not strictly necessary to explicitly specify the mappings between attribute variables and generic attribute indexes, because OpenGL will automatically map active vertex attributes to generic indexes when the program is linked. We could then query for the mappings and determine the indexes that correspond to the shader's input variables. It may be somewhat more clear however, to explicitly specify the mapping as we do in this example.

The first step involves setting up a pair of buffer objects to store our position and color data. As with most OpenGL objects, we start by creating the objects and acquiring handles to the two buffers by calling glGenBuffers. We then assign each handle to a separate descriptive variable to make the following code more clear.

For each buffer object, we first bind the buffer to the GL_ARRAY_BUFFER binding point by calling glBindBuffer. The first argument to glBindBuffer is the target binding point. In this case, since the data is essentially a generic array, we use GL_ARRAY_BUFFER. Examples of other kinds of targets (such as GL_UNIFORM_BUFFER, or GL_ELEMENT_ARRAY_BUFFER) will be seen in later examples. Once our buffer object is bound, we can populate the buffer with our vertex/color data by calling glBufferData. The second and third arguments to this function are the size of the array and a pointer to the array containing the data. Let's focus on the first and last arguments. The first argument indicates the target buffer object. The data provided in the third argument is copied into the buffer that is bound to this binding point. The last argument is one that gives OpenGL a hint about how the data will be used so that it can determine how best to manage the buffer internally. For full details about this argument, take a look into the OpenGL documentation. In our case, the data is specified once, will not be modified, and will be used many times for drawing operations, so this usage pattern best corresponds to the value GL_STATIC_DRAW.

Now that we have set up our buffer objects, we tie them together into a Vertex Array Object (VAO). The VAO contains information about the connections between the data in our buffers and the input vertex attributes. We create a VAO using the function glGenVertexArrays. This gives us a handle to our new object, which we store in the (global) variable vaoHandle. Then we enable the generic vertex attribute indexes 0 and 1 by calling glEnableVertexAttribArray. Doing so indicates that that the values for the attributes will be accessed and used for rendering.

The next step makes the connection between the buffer objects and the generic vertex attribute indexes.

// Map index 0 to the position buffer
glBindBuffer(GL_ARRAY_BUFFER, positionBufferHandle);
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, NULL );

First we bind the buffer object to the GL_ARRAY_BUFFER binding point, then we call glVertexAttribPointer, which tells OpenGL which generic index that the data should be used with, the format of the data stored in the buffer object, and where it is located within the buffer object that is bound to the GL_ARRAY_BUFFER binding point. The first argument is the generic attribute index. The second is the number of components per vertex attribute (1, 2, 3, or 4). In this case, we are providing 3-dimensional data, so we want 3 components per vertex. The third argument is the data type of each component in the buffer. The fourth is a Boolean which specifies whether or not the data should be automatically normalized (mapped to a range of [-1, 1] for signed integral values or [0, 1] for unsigned integral values). The fifth argument is the stride, which indicates the byte offset between consecutive attributes. Since our data is tightly packed, we can use a value of zero. The last argument is a pointer, which is not treated as a pointer! Instead, its value is interpreted as a byte offset from the beginning of the buffer to the first attribute in the buffer. In this case, there is no additional data in either buffer before the first element, so we use a value of zero.

Note

The glVertexAttribPointer function stores (in the VAO's state) a pointer to the buffer currently bound to the GL_ARRAY_BUFFER binding point. When another buffer is bound to that binding point, it does not change the value of the pointer.

The VAO stores all of the OpenGL state related to the relationship between buffer objects and the generic vertex attributes, as well as the information about the format of the data in the buffer objects. This allows us to quickly return all of this state when rendering. The VAO is an extremely important concept, but can be tricky to understand. It's important to remember that the VAO's state is primarily associated with the enabled attributes and their connection to buffer objects. It doesn't necessarily keep track of buffer bindings. For example, it doesn't remember what is bound to the GL_ARRAY_BUFFER binding point. We only bind to this point in order to set up the pointers via glVertexAttribPointer.

Once we have the VAO set up (a one-time operation), we can issue a draw command to render our image. In our render function, we clear the color buffer using glClear, bind to the vertex array object, and call glDrawArrays to draw our triangle. The function glDrawArrays initiates rendering of primitives by stepping through the buffers for each enabled attribute array, and passing the data down the pipeline to our vertex shader. The first argument is the render mode (in this case we are drawing triangles), the second is the starting index in the enabled arrays, and the third argument is the number of indices to be rendered (3 vertexes for a single triangle).

To summarize, we followed these steps:

  1. Make sure to specify the generic vertex attribute indexes for each attribute in the vertex shader using the layout qualifier.

  2. Create and populate the buffer objects for each attribute.

  3. Create and define the vertex array object by calling glVertexAttribPointer while the appropriate buffer is bound.

  4. When rendering, bind to the vertex array object and call glDrawArrays, or other appropriate rendering function (e.g. glDrawElements).

There's more...

In the following, we'll discuss some details, extensions, and alternatives to the previous technique.

Separate attribute format

With OpenGL 4.3, we have an alternate (arguably better) way of specifying the vertex array object state (attribute format, enabled attributes, and buffers). In the previous example, the glVertexAttribPointer function does two important things. First, it indirectly specifies which buffer contains the data for the attribute, which is the buffer currently bound (at the time of the call) to GL_ARRAY_BUFFER. Secondly, it specifies the format of that data (type, offset, stride, and so on). It is arguably clearer to separate these two concerns into their own functions. This is exactly what has been implemented in OpenGL 4.3. For example, to implement the same functionality as in step 3 of the previous How to do it… section, we would use the following code:

glGenVertexArrays(1, &vaoHandle);
glBindVertexArray(vaoHandle);
glEnableVertexArray(0);
glEnableVertexArray(1);

glBindVertexBuffer(0, positionBufferHandle, 0, sizeof(GLfloat)*3);
glBindVertexBuffer(1, colorBufferHandle, 0, sizeof(GLfloat)*3);

glVertexAttribFormat(0, 3, GL_FLOAT, GL_FALSE, 0);
glVertexAttribBinding(0, 0);
glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, 0);
glVertexAttribBinding(1, 1);

The first four lines of the previous code are exactly the same as in the first example. We create and bind to the VAO, then enable attributes 0 and 1. Next, we bind our two buffers to two different indexes within the vertex buffer binding point using glBindVertexBuffer. Note that we're no longer using GL_ARRAY_BUFFER binding point. Instead, we now have a new binding point specifically for vertex buffers. This binding point has several indexes (usually from 0 - 15), so we can bind multiple buffers to this point. The first argument to glBindVertexBuffer specifies the index within the vertex buffer binding point. Here, we bind our position buffer to index 0 and our color buffer to index 1.

Note

Note that the indexes within the vertex buffer binding point need not be the same as the attribute locations.

The other arguments to glBindVertexBuffer are as follows. The second argument is the buffer to be bound, the third is the offset from the beginning of the buffer to where the data begins, and the fourth is the stride, which is the distance between successive elements within the buffer. Unlike glVertexAttribPointer, we can't use a 0 value here for tightly packed data, because OpenGL can't determine the size of the data without more information, so we need to specify it explicitly here.

Next, we call glVertexAttribFormat to specify the format of the data for the attribute. Note that this time, this is decoupled from the buffer that stores the data. Instead, we're just specifying the format to expect for this attribute. The arguments are the same as the first four arguments to glVertexAttribPointer.

The function glVertexAttribBinding specifies the relationship between buffers that are bound to the vertex buffer binding point and attributes. The first argument is the attribute location, and the second is the index within the vertex buffer binding point. In this example, they are the same, but they need not be.

Also note that the buffer bindings of the vertex buffer binding point (specified by glBindVertexBuffer) are part of the VAO state, unlike the binding to GL_ARRAY_BUFFER, which is not.

This version is arguably more clear and easy to understand. It removes the confusing aspects of the "invisible" pointers that are managed in the VAO, and makes the relationship between attributes and buffers much more clear with glVertexAttribBinding. Additionally, it separates concerns that really need not be combined.

Fragment shader output

You may have noticed that I've neglected to say anything about the output variable FragColor in the fragment shader. This variable receives the final output color for each fragment (pixel). Like vertex input variables, this variable also needs to be associated with a proper location. Of course, we typically would like this to be linked to the back color buffer, which by default (in double buffered systems) is "color number" zero. (The relationship of the color numbers to render buffers can be changed by using glDrawBuffers). In this program, we are relying on the fact that the linker will automatically link our only fragment output variable to color number zero. To explicitly do so, we could (and probably should) have used a layout qualifier in the fragment shader:

layout (location = 0) out vec4 FragColor;

We are free to define multiple output variables for a fragment shader, thereby enabling us to render to multiple output buffers. This can be quite useful for specialized algorithms such as deferred rendering (see Chapter 5, Image Processing and Screen Space Techniques).

Specifying attribute indexes without using layout qualifiers

If you'd rather not clutter up your vertex shader code with the layout qualifiers (or you're using a version of OpenGL that doesn't support them), you can define the attribute indexes within the OpenGL program. We can do so by calling glBindAttribLocation just prior to linking the shader program. For example, we'd add the following code to the main OpenGL program just before the link step:

glBindAttribLocation(programHandle, 0, "VertexPosition");
glBindAttribLocation(programHandle, 1, "VertexColor");

This would indicate to the linker that VertexPosition should correspond to generic attribute index 0 and VertexColor to index 1.

Similarly, we can specify the color number for fragment shader output variables without using the layout qualifier. We do so by calling glBindFragDataLocation prior to linking the shader program:

glBindFragDataLocation(programHandle, 0, "FragColor");

This would tell the linker to bind the output variable FragColor to color number 0.

Using element arrays

It is often the case that we need to step through our vertex arrays in a non-linear fashion. In other words, we may want to "jump around" the data rather than just moving through it from beginning to end as we did in this example. For example, we might want to draw a cube where the vertex data consists of only eight positions (the corners of the cube). In order to draw the cube, we would need to draw 12 triangles (2 for each face), each of which consists of 3 vertices. All of the needed position data is in the original 8 positions, but to draw all the triangles, we'll need to jump around and use each position for at least three different triangles.

To jump around in our vertex arrays, we can make use of element arrays. The element array is another buffer that defines the indices used when stepping through the vertex arrays. For details on using element arrays, take a look at the function glDrawElements in the OpenGL documentation (http://www.opengl.org/sdk/docs/man).

Interleaved arrays

In this example, we used two buffers (one for color and one for position). Instead, we could have used just a single buffer and combined all of the data. In general, it is possible to combine the data for multiple attributes into a single buffer. The data for multiple attributes can be interleaved within an array, such that all of the data for a given vertex is grouped together within the buffer. Doing so just requires careful use of the stride argument to glVertexAttribPointer or glBindVertexBuffer. Take a look at the documentation for full details (http://www.opengl.org/sdk/docs/man).

The decision about when to use interleaved arrays and when to use separate arrays, is highly dependent on the situation. Interleaved arrays may bring better results due to the fact that data is accessed together and resides closer in memory (so-called locality of reference), resulting in better caching performance.