- Shaders and OpenGL
- OpenGL's Programmable Pipeline
- An Overview of the OpenGL Shading Language
- Interface Blocks
- Compiling Shaders
- Shader Subroutines
- Separate Shader Objects
- SPIR-V
OpenGL’s Programmable Pipeline
While Chapter 1 provided a brief introduction to OpenGL’s rendering pipeline, we glossed over the mechanics of the shaders themselves and didn’t even show you what the simple shaders used by the first example contained. Here, we describe in greater detail the various stages and what operations they carry out. Version 4.5’s graphical pipeline contains four processing stages, plus a compute stage, each of which you control by providing a shader.
The Vertex shading stage receives the vertex data that you specified in your vertex-buffer objects, processing each vertex separately. This is the only mandatory stage, and all OpenGL programs must have a shader bound to it when drawing. We describe vertex shading operation in Chapter 3, “Drawing with OpenGL.”
The Tessellation shading stage is an optional stage that generates additional geometry within the OpenGL pipeline, as compared to having the application specify each geometric primitive explicitly. This stage, if activated, receives the output of the vertex shading stage and does further processing of the received vertices.
The tessellation stage is actually divided into two shaders known as the tessellation control shader and the tessellation evaluation shader. These will be explained in more detail in Chapter 9, “Tessellation Shaders.” We use the term tessellation shader to mean either or both of these shading stages, and will sometimes use the terms control shader and evaluation shader as shorthand for its two parts.
The Geometry shading stage is another optional stage that can modify entire geometric primitives within the OpenGL pipeline. This stage operates on individual geometric primitives allowing each to be modified. In this stage, you might generate more geometry from the input primitive, change the type of geometric primitive (e.g., converting triangles to lines), or discard the geometry altogether. If activated, a geometry shader receives its input either after vertex shading has completed processing the vertices of a geometric primitive or from the primitives generated from the tessellation shading stage, if it’s been enabled. The geometry shading stage is described in Chapter 10, “Geometry Shaders.”
Finally, the last part of the OpenGL shading pipeline is the Fragment shading stage. This stage processes the individual fragments (or samples, if sample-shading mode is enabled) generated by OpenGL’s rasterizer and must have a shader bound to it. In this stage, a fragment’s color and depth values are computed and then sent for further processing in the fragment-testing and blending parts of the pipeline. Fragment shading operation is discussed in many sections of the text.
The Compute shading stage is not part of the graphical pipeline like the stages above, but stands on its own as the only stage in a program. A compute shader processes generic work items, driven by an application-chosen range, rather than by graphical inputs like vertices and fragments. Compute shaders can process buffers created and consumed by other shader programs in your application. This includes framebuffer post-processing effects or really anything you want. Compute shaders are described in Chapter 12, “Compute Shaders.”
An important concept to understand in general is how data flows between the shading stages. Shaders, as you saw in Chapter 1, are like a function call: Data is passed in, processed, and passed back out. In C, for example, this can either be done using global variables or arguments to the function. GLSL is a little different. Each shader looks a complete C program, in that its entry point is a function named main(). Unlike C, GLSL’s main() doesn’t take any arguments; rather all data going into and out of a shader stage is passed using special global variables in the shader. (Please don’t confuse them with global variables in your application; shader variables are entirely separate from the variables you’ve declared in your application code.) For example, take a look at Example 2.1.
Example 2.1 A Simple Vertex Shader
#version 450 core in vec4 vPosition; in vec4 vColor; out vec4 color; uniform mat4 ModelViewProjectionMatrix; void main() { color = vColor; gl_Position = ModelViewProjectionMatrix * vPosition; }
Even though that’s a very short shader, there are a lot of things to take note of. Regardless of which shading stage you’re programming for, shaders will generally have the same structure as this one. This includes starting with a declaration of the version using #version.
First, notice the global variables. Those are the inputs and outputs OpenGL uses to pass data through the shader. Aside from each variable having a type (e.g., vec4, which we’ll get into more momentarily), data is copied into the shader from OpenGL through the in variables and likewise copied out of the shader through the out variables. The values in those variables are updated every time OpenGL executes the shader (e.g., if OpenGL is processing vertices, then new values are passed through those variables for each vertex; when processing fragments, then for each fragment). The other category of variable that’s available to receive data from an OpenGL application are uniform variables. Uniform values don’t change per vertex or fragment, but have the same value across geometric primitives until the application updates them.