Introduction
A shader is a small program developed by you with two purposes:
- To provide the final space transformation of a 3D model to the rendering pipeline.
- To provide coloring information to each pixel.
A shader is written in a special graphics language called OpenGL Shading Language (GLSL).
Long ago, OpenGL existed as a static, configurable API. This paradigm prevented any development in creative graphical effects. OpenGL solved this problem by making two stages in the OpenGL Rendering Pipeline programmable. These two stages are:
- Per-Vertex Processing: responsible for providing the final space transformation of a 3D model to the rendering pipeline.
- Per-Fragment Processing: responsible for providing coloring information to the pixels.
These two stages are no longer static but programmable. These stages are now controlled by programs known as Shaders.
Shaders responsible for the Per-Vertex Processing and Per-Fragment Processing stages are known as Vertex Shaders and Fragment Shaders, respectively.
Note: the responsibility for each stage did not change. Only their paradigm shift from static to programmable.
Linking data between OpenGL and Shaders
To understand OpenGL, think of it as operating on two fronts. The Client-side and the Server-side. The OpenGL API operates on your application side, i.e., the client side. Whereas, shaders operate in the GPU side, i.e., the server side.
One of the responsibility of OpenGL is to link data stored in buffers to shaders' Qualifiers known as Attributes and Uniforms.
Linking buffer data to Attributes?
How does data flow from the client-side to the server-side of OpenGL? This flow occurs thanks to OpenGL Buffers. OpenGL Buffers accept data in your application and transports it to the GPU. However, once in the GPU, this data has no way of knowing to whom it belongs to. Your application must query the location of the attribute in the shader which will use this data. Once this location is known, data in the buffer can be linked to the attribute.
How to link buffer data to attributes?
Listing 1 shows an example of how buffer-attribute linking occurs.
In line 1 we load the data found in vertexData to an OpenGL buffer. In line 2 we query for the location of an attribute in the shader declared as position (see listing 2, line 1). After enabling the attribute location to accept data (line 3), we link the data in the buffer to the location of the attribute (line 4).
For a more in-depth example of how to load data in OpenGL Buffers, please see this post
Listing 1. Loading data into buffer
//Vertex data of 3D model
float vertexData[250]={1.0,0.4,0.9,1.0,....};
//..assume an OpenGL buffer has been created and bound
//1. Load data in the buffer
glBufferData(GL_ARRAY_BUFFER,sizeof(vertexData),vertexData,GL_STATIC_DRAW);
//2. Get the location of the shader attribute called "position"
GLuint positionLocation=glGetAttribLocation(programObject, "position");
//3. Enable the attribute location
glEnableVertexAttribArray(positionLocation);
//4. Link the buffer data to the shader attribute locations.
glVertexAttribPointer(positionLocation,3, GL_FLOAT, GL_FALSE, 0, (const GLvoid *)0);
Listing 2. A Vertex Shader with attribute
//1. Attribute declared as "position"
attribute vec4 position;
//2. Uniform declared as "modelViewProjectionMatrix"
uniform mat4 modelViewProjectionMatrix;
void main()
{
gl_Position = modelViewProjectionMatrix * position;
}
Linking data to Uniforms?
Linking data to Uniforms is very similar to linking buffer data to attributes. We query for the location of the uniform in the shader. Once we know the location, we provide data to the uniform. However, data linked to uniforms are usually not found in buffers. Instead, they are data residing in your application.
How to link data to Uniforms?
Listing 3 shows an example of how data-uniform linking occurs.
Line 1 queries for the location of the uniform in the shader declared as modelViewProjectionMatrix (see listing 2, line 2). Once the location is known, matrix data is provided to this Uniform through the function glUniformMatrix4fv (line 3).
Listing 3. Obtaining the location of a uniform
//1. Get location of uniform "modelViewProjectionMatrix"
modelViewProjectionUniformLocation = glGetUniformLocation(programObject,"modelViewProjectionMatrix");
//2. Provide matrix data "MVPSpace" to the Uniform
glUniformMatrix4fv(modelViewProjectionUniformLocation, 1, 0, MVPSpace.m);
Shaders Qualifiers
Variables in GLSL can take in different behaviors. Some variables can only receive data, others can only provide data. Some of these variables can only be used in Vertex Shaders, while other variables can only be used in Fragment Shaders. To differentiate these type of variables, GLSL uses Type Qualifiers. Type qualifiers used in shaders are as follows:
- Attributes
- Uniforms
- Varying
Attributes
An Attribute is a shader qualifier which can receive data from OpenGL Buffers and whose value may change frequently.
An attribute can only be declared and defined in the vertex shader.
Listing 4, line 1 shows the declaration of an attribute called position.
Listing 4. Example of Qualifiers in shaders
//1. Attribute declared as "position"
attribute vec4 position;
//2. Uniform declared as "modelViewProjectionMatrix"
uniform mat4 modelViewProjectionMatrix;
//3. declaration of varying qualifier
varying vec2 vTextureCoords.
void main()
{
gl_Position = modelViewProjectionMatrix * position;
}
Uniforms
A Uniform is a shader qualifier whose value may rarely change. In contrast to attributes which can only be declared in Vertex Shaders, uniforms can be declared in Vertex Shaders and Fragment Shaders. You can think of uniforms as global variables which can be seen by both shader types.
Listing 4, line 2 shows an example of a uniform qualifier.
Varying
As you may recall, attributes can only be declared in Vertex Shaders. However, there are times when attribute data needs to be used in Fragment Shaders. In this cases, special type of qualifers called Varying are used. They take attribute data from vertex shaders to fragment shaders.
Listing 4, line 3 shows an example of a varying-qualifier.
Developing a Shader
We are now in the position to develop our own shaders. We are going to write source code for a vertex and fragment shader.
This is a hands-on project. Download this template XCode project and code along.
Open up the project and open up the Shader.vsh file. This file will contain our vertex shader source code.
If you have not read about the GLSL language, you may want to read it first before continuing.
Our vertex shader will simply receive vertex data through the position attribute. It will also receive a Model-View-Projection matrix through a uniform. It will then transform the vertex positions by this matrix and set it as the output of the shader.
Type what is shown in listing 5.
As line 1 shows, we first declare an attribute called position. We also declare a uniform called modelViewProjectionMatrix (line 2). Within the main function, we transform each position vertex by the model-view-Projection matrix. We provide the result to the output of the shader gl_Position (line 3).
Listing 5. Vertex Shaders
//1. declare an attribute called "position"
attribute vec4 position;
//2. declare a uniform that contains the model-view-projection matrix
uniform mat4 modelViewProjectionMatrix;
void main()
{
//3. transform every position vertex by the model-view-projection matrix and set this to the output of the shader
gl_Position = modelViewProjectionMatrix * position;
}
Now let's write the fragment shader. Open up file Shader.fsh and type what is shown in listing 6.
The output of our fragment shader is gl_FragColor. In listing 6, we set this output to be equal to the color red (line 2).
Listing 6. Fragment Shaders
precision highp float;
void main()
{
//1. set a color to red
vec3 color=vec3(1.0,0.0,0.0);
//2. paint each fragment with color "red"
gl_FragColor = vec4(color,1.0);
}
If you run the project you will see nothing.
Why?
Because in order to use a shader you need to compile, attach and link the shader before it becomes operational. In simpler terms, we need to transfer our shaders to the GPU.
Compiling, attaching and linking a shader
The seven steps to compile, attach and link a shader are as follows:
- Create one or more shader objects by using glCreateShader.
- Provide source code for these shaders by calling glShaderSource.
- Compile each of the shaders with glCompileShader.
- Create a program object by calling glCreateProgram.
- Attach the shader objects by calling glAttachShader.
- Link the program object by calling glLinkProgram.
- To use the program object as part of OpenGL’s current state call glUseProgram.
Open up file Character.mm, go to method loadShaders() and type what is shown in listing 7.
Listing 7. Compiling, attaching and linking a shader
void Character::loadShaders(const char* uVertexShaderProgram, const char* uFragmentShaderProgram){
// Temporary Shader objects
GLuint VertexShader;
GLuint FragmentShader;
//1. Create shader objects
VertexShader = glCreateShader(GL_VERTEX_SHADER);
FragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//2. Load both vertex & fragment shader files
//Usually you want to check the return value of the loadShaderFile function, if
//it returns true, then the shaders were found, else there was an error.
if(loadShaderFile(uVertexShaderProgram, VertexShader)==false){
glDeleteShader(VertexShader);
glDeleteShader(FragmentShader);
fprintf(stderr, "The shader at %s could not be found.\n", uVertexShaderProgram);
}else{
fprintf(stderr,"Vertex Shader was loaded successfully\n");
}
if(loadShaderFile(uFragmentShaderProgram, FragmentShader)==false){
glDeleteShader(VertexShader);
glDeleteShader(FragmentShader);
fprintf(stderr, "The shader at %s could not be found.\n", uFragmentShaderProgram);
}else{
fprintf(stderr,"Fragment Shader was loaded successfully\n");
}
//3. Compile both shader objects
glCompileShader(VertexShader);
glCompileShader(FragmentShader);
//3a. Check for errors in the compilation
GLint testVal;
//3b. Check if vertex shader object compiled successfully
glGetShaderiv(VertexShader, GL_COMPILE_STATUS, &testVal);
if(testVal == GL_FALSE)
{
char infoLog[1024];
glGetShaderInfoLog(VertexShader, 1024, NULL, infoLog);
fprintf(stderr, "The shader at %s failed to compile with the following error:\n%s\n", uVertexShaderProgram, infoLog);
glDeleteShader(VertexShader);
glDeleteShader(FragmentShader);
}else{
fprintf(stderr,"Vertex Shader compiled successfully\n");
}
//3c. Check if fragment shader object compiled successfully
glGetShaderiv(FragmentShader, GL_COMPILE_STATUS, &testVal);
if(testVal == GL_FALSE)
{
char infoLog[1024];
glGetShaderInfoLog(FragmentShader, 1024, NULL, infoLog);
fprintf(stderr, "The shader at %s failed to compile with the following error:\n%s\n", uFragmentShaderProgram, infoLog);
glDeleteShader(VertexShader);
glDeleteShader(FragmentShader);
}else{
fprintf(stderr,"Fragment Shader compiled successfully\n");
}
//4. Create a shader program object
programObject = glCreateProgram();
//5. Attach the shader objects to the shader program object
glAttachShader(programObject, VertexShader);
glAttachShader(programObject, FragmentShader);
//6. Link both shader objects to the program object
glLinkProgram(programObject);
// Make sure link had no errors
glGetProgramiv(programObject, GL_LINK_STATUS, &testVal);
if(testVal == GL_FALSE)
{
char infoLog[1024];
glGetProgramInfoLog(programObject, 1024, NULL, infoLog);
fprintf(stderr,"The programs %s and %s failed to link with the following errors:\n%s\n",
uVertexShaderProgram, uFragmentShaderProgram, infoLog);
glDeleteProgram(programObject);
}else{
fprintf(stderr,"Shaders linked successfully\n");
}
// These are no longer needed
glDeleteShader(VertexShader);
glDeleteShader(FragmentShader);
//7. Use the program
glUseProgram(programObject);
}
The first step to compile a shader is to create a shader object. A shader object is a data structure that stores the source code for a shader. A shader object is created with a call to glCreateShader. We are going to create two shader objects for each source code (line 1).
Once a shader object has been created, it can accept source code for the vertex/fragment shader. The source code is provided to the shader object with a call to glShaderSource. Line 2 shows a call to a method named loadShaderFile(). This is a helper method that loads in the source code written in listing 5 and 6. If you notice, this method calls another method called loadShaderSrc(), which ultimately calls glShaderSource.
In line 2a, we check if the source code for each shader was loaded successfully.
The shader objects are then compiled with a call to glCompileShader (line 3). We check if the objects were compiled successfully in line 3a.
Once the shader object has been compiled. It must be attached to a Program Object. A Program Object is a container for shader objects and is created with the function glCreateProgram as shown in line 4.
The shader object is then attached to the program object with a call to glAttachShader (line 5). Once the shader object is attached to the program object, the shader object is linked with glLinkProgram (line 6). We check if the link was successful as shown in line 6a.
Finally, a call to glUseProgram is required to make the program object part of OpenGL’s current state (line 7).
Run the project. You should now see several messages on the output window of Xcode mentioning that the shaders were loaded, compiled and linked successfully.
Figure 1.
You should also see the silhouette of our character. Play around with the color values in the fragment shader (listing 6) and change the color of the 3D model.
Figure 2. Silhouette of a 3D model
Adding ligthing to a scene
Shaders can do many things. Aside from coloring a fragment. They also provide lighting to a scene. I will talk more in depth on ligthing a scene in later posts. However, it helps to see how ligthing is implemented using shaders.
Modify the vertex shader file Shader.vsh to what is shown in listing 8.
Listing 8. Ligthing vertex shader
//1. declare attributes
attribute vec4 position;
attribute vec3 normal;
//2. declare a varying qualifier
varying lowp vec4 colorVarying;
//3. declare a uniform that contains the model-view-projection matrix and another for the normal matrix
uniform mat4 modelViewProjectionMatrix;
uniform mat3 normalMatrix;
void main()
{
//4. Lighting operations
vec3 eyeNormal = normalize(normalMatrix * normal);
vec3 lightPosition = vec3(3.0, 0.5, 0.1);
vec4 diffuseColor = vec4(0.5, 0.5, 0.5, 1.0);
float nDotVP = max(0.0, dot(eyeNormal, normalize(lightPosition)));
colorVarying = 3.0*diffuseColor * nDotVP;
//5. transform every position vertex by the model-view-projection matrix and set this to the output of the shader
gl_Position = modelViewProjectionMatrix * position;
}
Line 1 simply declares two attributes: position and normal. Each representing the vertices' position and normal coordinates, respectively.
Since it is not possible to declare attributes in the fragment shader, we need to declare a varying qualifier that will take the data from the vertex to the fragment shader (line 2).
Line 3 simply declares our model-view-projection matrix and a normal matrix.
Line 4 performs the lighting operations on the scene. We then transform every position vertex by the transformation matrix. We provide the result to the shader output (line 5).
Now modify the fragment shader file Shader.fsh as shown in listing 9.
Line 1 shows the declaration of the same varying qualifier which was declared in the vertex shader. The data in this varying qualifier is provided to the output of the fragment shader (line 2).
Listing 9. Ligthing fragment shader
//1. declare a varying qualifer
varying lowp vec4 colorVarying;
void main()
{
//2. set the color of the fragment to colorVarying
gl_FragColor = colorVarying;
}
Run the code. You should now see the 3D model with ligthing applied to the scene.
Figure 3. 3D model with scene ligthing
Adding texture to a 3D model
Aside from coloring a fragment and providing ligthing, shaders are also used to apply textures to 3D models. If you have not done so, I strongly recommend for you to read how to apply textures to a 3D model.
We are going to modify our vertex and fragment shaders and make them capable of applying a texture to a model.
Figure 3. 3D model with texture
Modify the vertex Shader.vsh file to what is shown in listing 10.
Listing 10. Vertex shader for applying a texture
//1. declare attributes
attribute vec4 position;
attribute vec2 texCoord;
//2. declare varying type which will transfer the texture coordinates to the fragment shader
varying mediump vec2 vTexCoordinates;
//3. declare a uniform that contains the model-View-projection matrix
uniform mat4 modelViewProjectionMatrix;
void main()
{
//4. recall that attributes can't be declared in fragment shaders. Nonetheless, we need the texture coordinates in the fragment shader. So we copy the information of "texCoord" to "vTexCoordinates", a varying type.
vTexCoordinates=texCoord;
//5. transform every position vertex by the model-view-projection matrix
gl_Position = modelViewProjectionMatrix * position;
}
We start the shader by declaring attributes representing the position of our vertices and the coordinates of the texture (line 1).
The texture coordinates have no use in the vertex shader. These coordinates will be used in the fragment shader. Since it is not possible to declare attributes in the fragment shader, we need to declare a varying qualifier that will take the data from the vertex to the fragment shader (line 2).
Line 3 declares the model-view-projection matrix.
Within the main function, we set the texCoord data to the varying qualifier vTexCoordinates (line 4). We then transform every position vertex by the transformation matrix. We provide the result to the shader output (line 5).
Now modify the fragment Shader.fsh file to what is shown in listing 11.
Listing 11. Fragment shader for applying a texture
//1. declare a uniform sampler2d that contains the texture data
uniform sampler2D TextureMap;
//2. declare varying type which will transfer the texture coordinates from the vertex shader
varying mediump vec2 vTexCoordinates;
void main()
{
//3. Sample the texture using the Texture map and the texture coordinates
mediump vec4 color=texture2D(TextureMap,vTexCoordinates.st);
//4. Apply the sample color of the texture to the output of the shader.
gl_FragColor = color;
}
Line 1 shows the declaration of a uniform sampler2D called TextureMap. A sampler2D uniform contains the data of our texture. Line 2 shows the declaration of the same varying qualifier which was declared in the vertex shader.
Within the main function, the texture is sampled (line 3) and the resulting color is provided to the output of the fragment shader (line 4).
Source Code
You can get the source code for this part of the project here.
Questions?
So, do you have any questions? Is there something you need me to clarify? Did this project help you? Please let me know. Add a comment below and subscribe to receive our latest game development projects.
Note:
If you are using a newer Xcode version, you may get the following error:
"Couldn't decode the image. decoder error 29: first chunk is not the header chunk"
If you are getting this error message, the settings in Xcode is preventing the loading of png images.
To fix this, click on the project name, and select "Build Settings". Search for "Compress PNG Files". Set this option (debugger/Release) to NO.
Right below this option, you should see "Remove Text Metadata From PNG FIles". Set this option to NO.
When you build and run the project, the error mentioned above should go away and the png images should show up.
If you need more help, please visit my support page or contact me.
Update:
In newer Xcode versions, you may get this error while running the project demos:
"No such file or directory: ...xxxx-Prefix.pch"
This error means that the project does not have a PCH file. The fix is very simple:
In Xcode, go to new->file->PCH File.
Name the PCH file: 'NameOfProject-Prefix' where "NameOfProject" is the name of the demo you are trying to open. In the OpenGL demo projects, I usually named the projects as "openglesinc."
So the name of the PCH file should be "openglesinc-Prefix"
Make sure the file is saved within the 'NameOfProject' folder. i.e, within 'openglesinc' folder.
Click create and run the project.