Designing a GLSL shader class
We will now have a look at how to set up shaders. Shaders are special programs that are run on the GPU. There are different shaders for controlling different stages of the programmable graphics pipeline. In the modern GPU, these include the vertex shader (which is responsible for calculating the clip-space position of a vertex), the tessellation control shader (which is responsible for determining the amount of tessellation of a given patch), the tessellation evaluation shader (which computes the interpolated positions and other attributes on the tessellation result), the geometry shader (which processes primitives and can add additional primitives and vertices if needed), and the fragment shader (which converts a rasterized fragment into a colored pixel and a depth). The modern GPU pipeline highlighting the different shader stages is shown in the following figure.
Note that the tessellation control/evaluation shaders are only available in the hardware supporting OpenGL v4.0 and above. Since the steps involved in shader handling as well as compiling and attaching shaders for use in OpenGL applications are similar, we wrap these steps in a simple class we call GLSLShader
.
Getting ready
The GLSLShader
class is defined in the GLSLShader.[h/cpp]
files. We first declare the constructor and destructor which initialize the member variables. The next three functions, LoadFromString
, LoadFromFile
, and CreateAndLinkProgram
handle the shader compilation, linking, and program creation. The next two functions, Use
and UnUse
functions bind and unbind the program. Two std::map
datastructures are used. They store the attribute's/uniform's name as the key and its location as the value. This is done to remove the redundant call to get the attribute's/uniform's location each frame or when the location is required to access the attribute/uniform. The next two functions, AddAttribute
and AddUniform
add the locations of the attribute and uniforms into their respective std::map
(_attributeList
and _uniformLocationList
).
class GLSLShader { public: GLSLShader(void); ~GLSLShader(void); void LoadFromString(GLenum whichShader, const string& source); void LoadFromFile(GLenum whichShader, const string& filename); void CreateAndLinkProgram(); void Use(); void UnUse(); void AddAttribute(const string& attribute); void AddUniform(const string& uniform); GLuint operator[](const string& attribute); GLuint operator()(const string& uniform); void DeleteShaderProgram(); private: enum ShaderType{VERTEX_SHADER,FRAGMENT_SHADER,GEOMETRY_SHADER}; GLuint _program; int _totalShaders; GLuint _shaders[3]; map<string,GLuint> _attributeList; map<string,GLuint> _uniformLocationList; };
To make it convenient to access the attribute and uniform locations from their maps , we declare the two indexers. For attributes, we overload the square brackets ([]) whereas for uniforms, we overload the parenthesis operation (). Finally, we define a function DeleteShaderProgram
for deletion of the shader program object. Following the function declarations are the member fields.
How to do it…
In a typical shader application, the usage of the GLSLShader
object is as follows:
- Create the
GLSLShader
object either on stack (for example,GLSLShader
shader;) or on the heap (for example,GLSLShader* shader=new GLSLShader();
) - Call
LoadFromFile
on theGLSLShader
object reference - Call
CreateAndLinkProgram
on theGLSLShader
object reference - Call
Use
on theGLSLShader
object reference to bind the shader object - Call
AddAttribute
/AddUniform
to store locations of all of the shader's attributes and uniforms respectively - Call
UnUse
on theGLSLShader
object reference to unbind the shader object
Note that the above steps are required at initialization only. We can set the values of the uniforms that remain constant throughout the execution of the application in the Use
/UnUse
block given above.
At the rendering step, we access uniform(s), if we have uniforms that change each frame (for example, the modelview matrices). We first bind the shader by calling the GLSLShader::Use
function. We then set the uniform by calling the glUniform{*}
function, invoke the rendering by calling the glDraw{*}
function, and then unbind the shader (GLSLShader::UnUse
). Note that the glDraw{*}
call passes the attributes to the GPU.
How it works…
In a typical OpenGL shader application, the shader specific functions and their sequence of execution are as follows:
glCreateShader glShaderSource glCompileShader glGetShaderInfoLog
Execution of the above four functions creates a shader object. After the shader object is created, a shader program object is created using the following set of functions in the following sequence:
glCreateProgram glAttachShader glLinkProgram glGetProgramInfoLog
Tip
Note that after the shader program has been linked, we can safely delete the shader object.
There's more…
In the GLSLShader
class, the first four steps are handled in the LoadFromString
function and the later four steps are handled by the CreateAndLinkProgram
member function. After the shader program object has been created, we can set the program for execution on the GPU. This process is called shader binding. This is carried out by the glUseProgram
function which is called through the Use
/UnUse
functions in the GLSLShader
class.
To enable communication between the application and the shader, there are two different kinds of fields available in the shader. The first are the attributes which may change during shader execution across different shader stages. All per-vertex attributes fall in this category. The second are the uniforms which remain constant throughout the shader execution. Typical examples include the modelview matrix and the texture samplers.
In order to communicate with the shader program, the application must obtain the location of an attribute/uniform after the shader program is bound. The location identifies the attribute/uniform. In the GLSLShader
class, for convenience, we store the locations of attributes and uniforms in two separate std::map
objects.
For accessing any attribute/uniform location, we provide an indexer in the GLSLShader
class. In cases where there is an error in the compilation or linking stage, the shader log is printed to the console. Say for example, our GLSLshader
object is called shader
and our shader
contains a uniform called MVP
. We can first add it to the map of GLSLShader
by calling shader.AddUniform("MVP")
. This function adds the uniform's location to the map. Then when we want to access the uniform, we directly call shader("MVP")
and it returns the location of our uniform.