Writing Shader Code within the source code of the “normal” application is very tiring. You also don’t have syntax highlighting because you have to provide it in a string variable, not to mention debugging. Therefore a GLSL Shader is often written in its own file and loaded into the program at runtime. But how can you do this in C?
Loading a GLSL Shader from File in C is like reading any other Textfile and putting its content into a char* variable. You use the provided standard library functions for file and memory manipulation. You can then continue working with the loaded Shader.
/* Loads the content of a GLSL Shader file into a char* variable */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* get_shader_content(const char* fileName)
{
FILE *fp;
long size = 0;
char* shaderContent;
/* Read File to get size */
fp = fopen(fileName, "rb");
if(fp == NULL) {
return "";
}
fseek(fp, 0L, SEEK_END);
size = ftell(fp)+1;
fclose(fp);
/* Read File for Content */
fp = fopen(fileName, "r");
shaderContent = memset(malloc(size), '\0', size);
fread(shaderContent, 1, size-1, fp);
fclose(fp);
return shaderContent;
}
For the rest of this article, we’ll look at how this raw data becomes a finished shader and how it works within the OpenGL Rendering Pipeline. We’ll also see examples of each step on the way from a text file to a finished shader.
In this Article...
What is an OpenGL Shader
If you came here you probably know very well what a shader is, but for the sake of completeness, let’s go over its definition. According to Wikipedia, a Shader is defined as follows:
In computer graphics, a shader is a computer program that calculates the appropriate levels of light, darkness, and color during the rendering of a 3D scene – a process known as shading.
https://en.wikipedia.org/wiki/Shader, visited in November 2022
A Shader in OpenGL is a program that has its own language (GLSL) and is executed at a certain stage in the OpenGL Rendering Pipeline. These two aspects are described in more detail in the next paragraphs.
The OpenGL Rendering Pipeline
As I mentioned in my article about using OpenGL in C, the OpenGL Rendering Pipeline describes the stages which each vertex passes before it is rendered to the screen. The following gives a very brief introduction to each stage of the pipeline.
This is of course a very superficial description, as there are much more complex functions and sequences within the stages.
Vertex Specification
In OpenGL you put all the vertices you want to render into a Vertex Array Object (VAO) which is essentially a buffer. You have to make sure that you provide the correct number of vertices yourself.
For example, each triangle requires three vertices. If you provide six vertices, two triangles are generated, but if you only provide five vertices, only one triangle can be created.
The first three coordinates specify the position of the vertex on the screen. As all z-coordinates are zero, these are 2D-coordinates. Also, three colour coordinates are provided in the RGB format. One vertex array is generated and its id saved for later use. Through binding this vertex array becomes active.
void vertex_specification() {
GLuint id;
/* Triangles Vertices */
GLfloat vertices[] =
{
/* COORDINATES COLOURS */
-0.5f, -0.2886750f, 0.0f, 0.8f, 0.30f, 0.02f,
0.5f, -0.2886750f, 0.0f, 0.8f, 0.30f, 0.02f,
0.0f, 0.5773500f, 0.0f, 1.0f, 0.60f, 0.32f,
-0.25f, 0.1443375f, 0.0f, 0.9f, 0.45f, 0.17f,
0.25f, 0.1443375f, 0.0f, 0.9f, 0.45f, 0.17f,
0.00f, -0.2886750f, 0.0f, 0.8f, 0.30f, 0.02f,
};
glGenVertexArrays(1, &id);
glBindVertexArray(id);
}
After you are done with a vertex array, you can and should deactivate (free) it.
/* At Program Clean Up, do not forget to free all Vertex Array Objects */
glBindVertexArray(0);
Vertex Shader
At the Vertex Shader stage, each vertex from the Vertex Array Object (VAO) is an input to the shader where it is processed in some way and then turned into an output vertex.
An example for a Vertex Shader is provided below.
Tessellation
This is an optional stage. Vertices which form a surface are called patches. In Tessellation these patches can be divided into smaller patches, usually triangles or squares. If you skip this stage, the default Tessellation values are used.
Geometry Shader
The Geometry Shader is another optional stage. It is also a program written in GLSL. The input here is a set of primitives (Points, Triangles, etc.). This shader allows versatile manipulation of the input. You could provide very few vertex data and still generate a complex output if you do it correctly.
Vertex Post Processing
Many operations in this stage are the setup for the upcoming two stages, Primitive Assembly and Rasterization. Possible operations in this stage are Transform Feedback and Clipping.
Primitive Assembly
In this stage the vertex data is transferred into base primitives. If you provide 12 triangle list vertices then 4 triangle base primitives will be generated here and passed on to the Rasterization.
What is also done here is the so-called face culling. Because of the direction of the vectors, the primitives point in a specific direction. If this is not in the rendered area, the primitives can be discarded.
Rasterization
Rasterization is the process of transforming primitives into fragments. Lets look at the official definition of a fragment in the Khronos documentation:
A Fragment is a collection of values produced by the Rasterizer. Each fragment represents a sample-sized segment of a rasterized Primitive. The size covered by a fragment is related to the pixel area, but rasterization can produce multiple fragments from the same triangle per-pixel, depending on various multisampling parameters and OpenGL state. There will be at least one fragment produced for every pixel area covered by the primitive being rasterized.
https://www.khronos.org/opengl/wiki/Fragment, visited in November 2022
This means that the input vertex data runs through interpolation of each pixel in the area covered by the primitive. It is determined which pixels are occupied by which primitives, so that all primitives are adjacent but still do not share any pixels.
Fragment Shader
This shader takes the fragments from the last stage as input and allows for further manipulation. It will process the fragments in regards to colour and depth.
An example for a Fragment Shader is provided below.
Tests & Blending
The last stage is performing some tests on the generated fragments to determine whether they are visible or not. Also blending is performed here, , which means that the colours of overlapping fragments are blended at the appropriate places.
OpenGL Shader and GLSL (OpenGL Shader Language)
The OpenGL Shader Language (GLSL) is a high level programming language that enables the programmer to perform graphics operations within the OpenGL Rendering Pipeline. Its raw model was the C Language.
It was first released on 04/30/2004 with OpenGL 2.0. Its purpose is to give programmers the opportunity to do complex operations without necessarily knowing the ARB Assembly Language which is normally used for these purposes.
The next sections provide examples of shaders written in GLSL, namely the Vertex Shader and the Fragment Shader.
GLSL Vertex Shader
This is an example of a Vertex Shader. It takes the vertices stored in the VAO, processes them and passes them on to the next stage in the Rendering Pipeline (Vertex Post Processing).
#version 330 core
layout (location = 0) in vec3 pos;
layout (location = 1) in vec3 colorIn;
out vec3 colorOut;
void main()
{
gl_Position = vec4(pos.x, pos.y, pos.z, 1.0);
colorOut = colorIn;
}
The first line determines the version of the shader which in turn determines which OpenGL Version this script is for. In this case #version 330 core means OpenGL 3.3 with core functionality. The alternative would be compatibility if you have to use deprecated OpenGL functionality (not recommended!).
The next two lines tell the shader where in the VAO certain vectors are stored. In this specific example, we have a vec3 position and then a vec3 colour. This matches with our example of vertex specification above.
In the main function, two things are achieved:
First the position of the vector is set to a position in OpenGL space. We only provide three dimensions, so we transform it to the expected (x,y,z,w) coordinates.
Tip: In our Vertex Specification, every z-value is 0.0, so we could only provide x and y to the shader and add the fixed 0.0 here.
Second we only pass the colour values to the output variable because the colour is only needed in the fragment shader.
GLSL Fragment Shader
Here we have an example of a Fragment Shader. As described above, it can manipulate colour and depth of the fragments which come as input from the preceding Rasterization stage. The output of the Fragment shader is passed to the Tests & Blending stage.
#version 330 core
out vec4 FragColor;
in vec3 colorOut;
void main()
{
FragColor = vec4(colorOut, 1.0f);
}
The first line is again the version specification, similar to the one in the Vertex Shader.
In the next line the vec4 output variable is declared. We will use it in the main function where we simply pass through the colours we got from the Vertex Shader (which in turn got it directly from the VAO).
If two vertices have different colour, this shader automatically creates a colour gradient. An example for this effect is seen in the title image of this article.
Compile GLSL Shaders
As we have learned, a Shader is a program, written in a programming language. Therefore we have to compile it. This code snippet shows you how you can call the loading function which loads our shader source code from a file and the compiles the program.
void compile_shader(GLuint* shaderId, GLenum shaderType, const char* shaderFilePath)
{
GLint isCompiled = 0;
/* Calls the Function that loads the Shader source code from a file */
const char* shaderSource = get_shader_content(shaderFilePath);
*shaderId = glCreateShader(shaderType);
if(*shaderId == 0) {
printf("COULD NOT LOAD SHADER: %s!\n", shaderFilePath);
}
glShaderSource(*shaderId, 1, (const char**)&shaderSource, NULL);
glCompileShader(*shaderId);
glGetShaderiv(*shaderId, GL_COMPILE_STATUS, &isCompiled);
if(isCompiled == GL_FALSE) { /* Here You should provide more error details to the User*/
printf("Shader Compiler Error: %s\n", shaderFilePath);
glDeleteShader(*shaderId);
return;
}
}
Linking GLSL Shaders
When both, the Vertex Shader and the Fragment Shader are compiled successfully, they have to be linked together into a Shader Program. This is done by attaching each shader to a program and then call the glLinkPogram method on it.
After the linking process you should detach and delete the shaders in order to free memory. This is done in the last lines of this function.
void link_shader(GLuint vertexShaderID, GLuint fragmentShaderID)
{
GLuint programID = 0;
GLint isLinked = 0;
GLint maxLength = 0;
char* infoLog = malloc(1024);
programID = glCreateProgram();
glAttachShader(programID, obj->vertexShaderID);
glAttachShader(programID, obj->fragmentShaderID);
glLinkProgram(programID);
glGetProgramiv(programID, GL_LINK_STATUS, &isLinked);
if(isLinked == GL_FALSE) {
printf("Shader Program Linker Error\n");
glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &maxLength);
glGetProgramInfoLog(programID, maxLength, &maxLength, &infoLog[0]);
printf("%s\n", infoLog);
glDeleteProgram(programID);
glDeleteShader(vertexShaderID);
glDeleteShader(fragmentShaderID);
free(infoLog);
return;
}
glDetachShader(programID, vertexShaderID);
glDetachShader(programID, fragmentShaderID);
glDeleteShader(vertexShaderID);
glDeleteShader(fragmentShaderID);
free(infoLog);
}
Loading GLSL Shader Programs
Now you have your shaders compiled and linked into a shader program. When you want to use it, you still have to activate it. This is done by the following function call:
/* Loading a compiled and linked Shader program to use it */
glUseProgram(programID);
When all is done and your program is not needed anymore, you should (as always) clean up by clearing your program:
/* Unload Shader program after rendering */
glUseProgram(0);
A GLSL Shader Class in C
There are no classes in C, but as I described in this article, you can build class like behaviour into your C program. You can even implement inheritance and polymorphism.
With this background knowledge, one can consider encapsulating various functionalities in a shader class. You could access loading, compiling and linking functions (even across projects) and concentrate fully on the actual shader programming.
You could also provider helper classes for better debugging and/or testing of your shaders in regards to functionality and performance.
Key Takeaways
- You can load a GLSL Shader from File in C like any other Textfile
- You have to compile the Shaders and link them into a Shader Program in order to use them
- Vertex Shaders determine the Position of a Vertex
- Fragment Shaders determine the Colour and Depth of a Shader
- Some Stages of the OpenGL Rendering Pipeline are mandatory, some are fixed and some are optional
- Even in C you can build something like a Shader class to encapsulate frequently used functionality
This article was first published by Marco Lieblang on moderncprogramming.com. If you are reading this somewhere else, it may be plagiarism.