GLSL Tutorial – Inter shader communication
Prev: Uniform Blocks | Next: Spaces and Matrices |
Shaders communicate between pipeline stages through variables. For instance a vertex shader outputs a color per vertex. Among the shaders that output vertices (vertex, geometry and tessellation vertices), the output of one stage is copied to the input of the next stage.
Afterwards, this value is fed to the primitive assembly and rasterization stages, where the color value is interpolated for each resulting fragment. Finally the fragments are fed to the fragment shader. Therefore, the inputs of the fragment shader are interpolated values.
To qualify a variable as input or output of a given shader stage the keywords in
and out
are available. We’ve already seen these keywords in use for the inputs of the vertex shader, and the output of the fragment shader. However, this section focuses only on inter shader communication.
GLSL provides several mechanisms to match the output of a stage to the input of the next stage. Each of these will be detailed in the next subsections, exploring the pros and cons of each mechanism.
Name based matching
The simplest of the matching mechanisms, from a syntax point of view, is name based. For instance we can name an output variable as color
in the vertex shader, and have an input with the same name in the fragment shader.
// vertex shader out vec4 color; ------------------- // fragment shader in vec4 color;
Name based matching is simple syntactically, and if only two shader stages were to be considered, it would be a great solution. The problem in the above solution arises when we decide to add a third stage, for instance a geometry shader, to the pipeline.
As the output of the vertex shader is called color
, the input of the geometry shader must match the name. However, we must now find another name for the output of the geometry shader, and consequently change the name of the input and alter the fragment shader code accordingly. The new variable distribution between shaders could now be as follows:
// vertex shader out vec4 color; ------------------- // geometry shader in vec4 color[]; out vec4 colorFromGeom; --------------------- // fragment shader in vec4 colorFromGeom;
As seen from the code above, the fragment shader must be altered to reflect the new input name for the variable containing the fragment’s interpolated color.
But what if we were using the same vertex and fragment shader in two pipelines: one with a geometry shader, and the other without? The only solution would be to have two versions of the fragment shader, with different input names.
And now, if we want to make some changes to the fragment shader, we have two shaders to modify…
It is clear that things can get pretty hard to maintain and debug with this approach.
Location based matching
Each variable has a location, and these can be used to match the input of a pipeline stage to the next stage. Two variables will match if the output location of a stage matches the input location of the following stage. Using location based matching, variable names do not have to coincide. For instance consider the following variable distribution in a vertex and fragment shader:
// vertex shader layout (location = 0) out vec3 normalOut; layout (location = 1) out vec4 colorOut; --------------------- // fragment shader layout (location = 0) in vec3 normalIn; layout (location = 1) in vec4 colorIn;
In this case normalOut
from the vertex shader will match to normalIn
from the fragment shader, since they share the same location. The same reasoning applies to colorOut
and colorIn
.
What happens now if we add a geometry shader to the pipeline? As opposed to name based matching, this will have no implications on the fragment shader as long as we take into account the previous locations.
// vertex shader layout (location = 0) out vec3 normalOut; layout (location = 1) out vec4 colorOut; --------------------- // geometry shader layout (location = 0) in vec3 normalIn[]; layout (location = 1) in vec4 colorIn[]; layout (location = 0) out vec3 normalOut; layout (location = 1) out vec4 colorOut; --------------------- // fragment shader layout (location = 0) in vec3 normalIn; layout (location = 1) in vec4 colorIn;
The output variables of the vertex shader will match the input variables of the geometry shader, and the output variables of the geometry shader will match the input variables of the fragment shader. As show in the above example, there is no need to come up with new names for the inputs of the fragment shader, hence the cons of name based matching are gone with this approach.
However, there are some issues to consider with location based matching. First we must be proficient at determining locations. For instance, the following code would generate a compilation error:
// vertex shader layout (location = 0) out vec3 someAttribute[2]; layout (location = 1) out vec4 colorOut; main() { someAttribute[1] = …; colorOut = …; … }
with NVIDIA drivers 305.67 we would get the following error:
error C5121: multiple bindings to output semantic "ATTR1"
Each location, is a vector location, i.e. it can hold up to a vector of 4 elements, float or int. Hence, the second array element of someAttribute
shares the same location of colorOut
. The compiler is actually quite permissive, and it will only output an error when the location is accessed with both variables in the same shader.
Location based matching therefore requires extra care. Double vectors dvec3
and dvec4
require 2 locations. Matrices also take multiple locations, one per matrix line for floats and ints. Structs imply counting the locations of each field.
For instance consider the following structure:
layout(location = 0) out struct S{ vec3 normalOut; mat3 aMatrix; int a; float b; }s;
The variable normalOut
will get location 0, aMatrix
will have locations 1,2 and 3, one for each line. Location 4 will store a
, and location 5 b
.
So if we wanted to declare another output with a location based matching approach we would have to use location 6. But what happens when we want to add things to the struct, making it larger? We would have to relocate the variables starting from location 6, on this shader, and all other shaders that receive as inputs the outputs of this shader in particular. Again things are starting to get messy …
Concluding, location based has an advantage over name based matching since it does not have implications on the variables names, hence no code rewriting. On the other hand counting locations is not friendly at all. Furthermore, changes in one output data type, or struct members, can imply changes in all subsequent locations.
Block based matching
The third approach is based on interface blocks. We’ve already covered blocks in section Uniform blocks. Inter shader communication blocks are similar to uniform blocks in their construction. Blocks can have multiple fields, and the matching is done by block name.
A block example:
out Data { vec3 normal; vec3 eye; vec3 lightDir; vec2 texCoord; } DataOut;
In the above example, Data
is the block’s name, and DataOut
is the instance’s name. Inside the shader we will refer to the variables inside the block, prefixing them with the instance name. For instance:
DataOut.normal = normalize(someVector);
With this approach we can define our blocks in the vertex and fragment shaders as follows:
// vertex shader out Data { vec3 normal; vec3 eye; vec3 lightDir; vec2 texCoord; } DataOut; ---------------------------- // fragment shader in Data { vec3 normal; vec3 eye; vec3 lightDir; vec2 texCoord; } DataIn;
Notice that the matching is done by the block’s name, Data
, not by the instance name, DataOut
in the vertex shader and DataIn
in the fragment shader.
To add a geometry shader to the pipeline we just need to declare an input block that matches the output of the vertex shader, and an output that matches the input of the fragment shader. Hence, in our geometry shader we can have something like
in Data { vec3 normal; vec3 eye; vec3 lightDir; vec2 texCoord; } DataIn[]; out Data { vec3 normal; vec3 eye; vec3 lightDir; vec2 texCoord; } DataOut;
Note that blocks with the same name must have the same members, or at least the same memory usage.
As in the location based matching, the usage of blocks allows for the insertion/removal of pipeline stages without affecting the code of the remaining stages. Furthermore, the block based matching has the advantage of being less error prone as it is much simpler than the location based approach. As long as we keep declaring the blocks with exactly the same members we are on the good path.
Prev: Uniform Blocks | Next: Spaces and Matrices |
3 Responses to “GLSL Tutorial – Inter shader communication”
Leave a Reply Cancel reply
This site uses Akismet to reduce spam. Learn how your comment data is processed.
Yes thanks. 🙂
Is this Code really right? In the last line… I never saw that in other languages when using structs.
layout(location = 0) out struct S{
vec3 normalOut;
mat3 aMatrix;
int a;
float b;
}s;
The last line decalres a variable of the struct type. Is this what you’re asking?
António