Passing Data from Stage to Stage
So far, you have seen how to pass data into a vertex shader by creating a vertex attribute using the in keyword, how to communicate with fixed-function blocks by reading and writing built-in variables such as gl_VertexID and gl_Position, and how to output data from the fragment shader using the out keyword. However, it’s also possible to send your own data from shader stage to shader stage using the same in and out keywords. Just as you used the out keyword in the fragment shader to create the output variable to which it writes its color values, so you can also create an output variable in the vertex shader by using the out keyword. Anything you write to an output variable in one shader is sent to a similarly named variable declared with the in keyword in the subsequent stage. For example, if your vertex shader declares a variable called vs_color using the out keyword, it would match up with a variable named vs_color declared with the in keyword in the fragment shader stage (assuming no other stages were active in between).
If we modify our simple vertex shader as shown in Listing 3.3 to include vs_color as an output variable, and correspondingly modify our simple fragment shader to include vs_color as an input variable as shown in Listing 3.4, we can pass a value from the vertex shader to the fragment shader. Then, rather than outputting a hard-coded value, the fragment can simply output the color passed to it from the vertex shader.
Listing 3.3: Vertex shader with an output
#version 450 core // 'offset' and 'color' are input vertex attributes layout (location = 0) in vec4 offset; layout (location = 1) in vec4 color; // 'vs_color' is an output that will be sent to the next shader stage out vec4 vs_color; void main(void) { const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0), vec4(-0.25, -0.25, 0.5, 1.0), vec4(0.25, 0.25, 0.5, 1.0)); // Add 'offset' to our hard-coded vertex position gl_Position = vertices[gl_VertexID] + offset; // Output a fixed value for vs_color vs_color = color; }
As you can see in Listing 3.3, we declare a second input to our vertex shader, color (this time at location 1), and write its value to the vs_output output. This is picked up by the fragment shader of Listing 3.4 and written to the framebuffer. This allows us to pass a color all the way from a vertex attribute that we can set with glVertexAttrib*() through the vertex shader, into the fragment shader, and out to the framebuffer. As a consequence, we can draw different-colored triangles!
Listing 3.4: Fragment shader with an input
#version 450 core // Input from the vertex shader in vec4 vs_color; // Output to the framebuffer out vec4 color; void main(void) { // Simply assign the color we were given by the vertex shader to our output color = vs_color; }
Interface Blocks
Declaring interface variables one at a time is possibly the simplest way to communicate data between shader stages. However, in most nontrivial applications, you will likely want to communicate a number of different pieces of data between stages; these may include arrays, structures, and other complex arrangements of variables. To achieve this, we can group together a number of variables into an interface block. The declaration of an interface block looks a lot like a structure declaration, except that it is declared using the in or out keyword depending on whether it is an input to or output from the shader. An example interface block definition is shown in Listing 3.5.
Listing 3.5: Vertex shader with an output interface block
#version 450 core // 'offset' is an input vertex attribute layout (location = 0) in vec4 offset; layout (location = 1) in vec4 color; // Declare VS_OUT as an output interface block out VS_OUT { vec4 color; // Send color to the next stage } vs_out; void main(void) { const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0), vec4(-0.25, -0.25, 0.5, 1.0), vec4(0.25, 0.25, 0.5, 1.0)); // Add 'offset' to our hard-coded vertex position gl_Position = vertices[gl_VertexID] + offset; // Output a fixed value for vs_color vs_out.color = color; }
Note that the interface block in Listing 3.5 has both a block name (VS_OUT, uppercase) and an instance name (vs_out, lowercase). Interface blocks are matched between stages using the block name (VS_OUT in this case), but are referenced in shaders using the instance name. Thus, modifying our fragment shader to use an interface block gives the code shown in Listing 3.6.
Listing 3.6: Fragment shader with an input interface block
#version 450 core // Declare VS_OUT as an input interface block in VS_OUT { vec4 color; // Send color to the next stage } fs_in; // Output to the framebuffer out vec4 color; void main(void) { // Simply assign the color we were given by the vertex shader to our output color = fs_in.color; }
Matching interface blocks by block name but allowing block instances to have different names in each shader stage serves two important purposes. First, it allows the name by which you refer to the block to be different in each stage, thereby avoiding confusing things such as having to use vs_out in a fragment shader. Second, it allows interfaces to go from being single items to arrays when crossing between certain shader stages, such as the vertex and tessellation or geometry shader stages, as we will see in a short while. Note that interface blocks are only for moving data from shader stage to shader stage—you can’t use them to group together inputs to the vertex shader or outputs from the fragment shader.