Tutorials
Articles
OpenGL Demos
Games
OpenGL Misc
MSG Board
About
Donate
Links
Home
Megabyte Softworks
C++, OpenGL, Algorithms




Current series: OpenGL 3.3
(Return to list of OpenGL 3.3 tutorials)

Download (128 KB)
6434 downloads. 13 comments
3.) Shaders Are Coming

Changelog

8.7.2013 - Now compiled with glew version 1.9.0 and the executable is in bin folder now. Also all function names begin with uppercase letter.

</Changelog>

Welcome to third OpenGL 3.3+ tutorial. Here we will dive into shaders and learn something about Vertex Array Objects (VAOs). This tutorial is gonna be pretty long but after understanding it, you will feel a lot more comfortable with new OpenGL.

- Shaders:

If you're new to OpenGL and rendering stuff, it may seem, that shaders are something, that can create shades. But actually, shaders are arbitrary programs, that somehow process vertices and data we send to it, and in the end, they produce the final image. There are currently three basic shader types - vertex shaders (process vertices), fragment shaders (process each pixel, or fragment) and geometry shaders (they can produce extra vertices and tesellation, more on that in later tutorials). We will focus on the first two in this tutorial.

In old OpenGL, there was a fixed transformation process. You had some input vertices, they have been sent through modelview and projection matrix, and were output in the window coordinates, and then colored appropriately. But now, you can (and you must) program this process all by yourself. One shader itself is like one source file of final program, you can have multiple vertex shader files, which will get compiled and finally linked together into a shader program. It's like a single .cpp file in this project. We will write a vertex shader, that will take as input some vertex attributes, in case of this tutorial it is vertex position and vertex color, and will calculate the final position of vertex. But in this tutorial, we are not going to use any matrices yet - they will come in next tutorial, where basic transformation operations will be introduced - translation, rotation and scaling (and also terms like model, world and eye-space coordinates will be explained). In this tutorial, our vertex shader will only output the incoming position (no change), and fragment shader will color the polygons smoothly, interpolating colors between vertices. So let's get into it.

- Shader Class:

It would be nice to create a C++ class that will handle shaders, and thus we will do it. This class will provide routines for loading and using shaders. Here is how it looks:


class CShader
{
public:
   bool loadShader(string sFile, int a_iType);
   void deleteShader();

   bool isLoaded();
   UINT getShaderID();

   CShader();

private:
   UINT uiShader; // ID of shader
   int iType; // GL_VERTEX_SHADER, GL_FRAGMENT_SHADER...
   bool bLoaded; // Whether shader was loaded and compiled
};

The most important function from this class is loadShader. It takes filename and type of shader as parameter. Other functions are pretty easy with only few lines, so I won't explain them, just look at them in .cpp file. Here let's have a look at loadShader:


bool CShader::loadShader(string sFile, int a_iType)
{
   FILE* fp = fopen(sFile.c_str(), "rt");
   if(!fp)return false;

   // Get all lines from a file

   vector<string> sLines;
   char sLine[255];
   while(fgets(sLine, 255, fp))sLines.push_back(sLine);
   fclose(fp);

   const char** sProgram = new const char*[ESZ(sLines)];
   FOR(i, ESZ(sLines))sProgram[i] = sLines[i].c_str();
   
   uiShader = glCreateShader(a_iType);

   glShaderSource(uiShader, ESZ(sLines), sProgram, NULL);
   glCompileShader(uiShader);

   delete[] sProgram;

   int iCompilationStatus;
   glGetShaderiv(uiShader, GL_COMPILE_STATUS, &iCompilationStatus);

   if(iCompilationStatus == GL_FALSE)return false;
   iType = a_iType;
   bLoaded = true;

   return 1;
}

In the first few lines of code, we read whole file into a vector of strings, line by line (vector from STL is a dynamic array, for those who don't know). After that, we call glCreateShader. It has one parameter - shader type. We can put there GL_VERTEX_SHADER, GL_FRAGMENT_SHADER or GL_GEOMETRY_SHADER. This function returns an unsigned int, that is a name or ID or handle (call it whatever you want) of our shader, by which we can reference it in OpenGL functions. Next function is glShaderSource. With this function we pass source code of shader to GPU for compilation. First parameter is our shader ID, then comes number of lines of code (you can see it's ESZ(sLines), ESZ is my macro defined in common_header.h, and it stands for element size - returns size of any STL container. Note: I know that this is tutorial and I shouldn't be using macros, that decrease code readability, but it's my programming habits, and I don't want to change them, even in tutorials. Some of them are very handy and decrease final source length. Of course, you don't have to use them in your projects, feel free to rewrite it if you want to use this code in your projects). Next parameter is actual source - it is a pointer to C null terminated strings, and since C null terminated string is a pointer to char, it's a double pointer to char. That's why we needed to convert our vector of strings into const char** in previous lines. Last parameter is simply NULL (and in my projects it always will be). It's a pointer to array of lengths, in case we wouldn't have null terminated strings. But I don't see a reason to create additional array with string lengths and I think this parameter is rather useless.

After we uploaded source code of shader, we can compile it. That's what glCompileShader function does. Parameter is nothing else than the shader handle. Then we free previously created strings and check for compilation status. If everything went good, we set bLoaded to true and we have successfully compiled shader file.

.

Other functions of this class are pretty easy, just have a look at them. Like I said before, shader itself isn't enough, compiled shader must be added to program, which will link all of them together in the end to create a shader program. And that's what counts :)

- Shader Program Class:

Again, we are going to create a C++ class that will wrap shader program for easy usage (Note: OpenGL is low-level API, and it is good to wrap this low-level functionality into higher level classes, because it increases code readability and also ease of use, and in bigger projects, it's absolutely neccessary):


class CShaderProgram
{
public:
   void createProgram();
   void deleteProgram();

   bool addShaderToProgram(CShader* shShader);
   bool linkProgram();

   void useProgram();

   UINT getProgramID();

   CShaderProgram();

private:
   UINT uiProgram; // ID of program
   bool bLinked; // Whether program was linked and is ready to use
};

There are several functions, some of which are one-liners. So instead of explaining each function, I will explain process of creating shader program. First, you must call function glCreateProgram(), which returns shader program handle (it's in createProgram function of our class). After that, we must add compiled shader objects into this program. That's what glAttachShader function does (AddShaderToProgram wraps this, with check whether shader is compiled). After we added all shaders into a program, we must link them together (it's like linking .obj files when compiling cpp code). This is done by glLinkProgram function, in our class it's linkProgram, which also checks for linkage status. If everything went good, now we can use shader program for handling incoming vertices and data. glUseProgram does exactly that (useProgram in our class).

Now we are finally ready to use shaders. We need to write two shaders - vertex for vertices handling, and fragment shader, that will color our scene. In this tutorial, our vertex shader won't change incoming position, it will just pass it further to rendering pipeline, and it will also pass incoming vertex color into fragment shader. But first, we must look at how we will communicate and work with shaders. So first we must look at InitScene function, which uploads vertex positions into GPU. So here it is:


float fTriangle[9]; // Data to render triangle (3 vertices, each has 3 floats)
float fQuad[12]; // Data to render quad using triangle strips (4 vertices, each has 3 floats)
float fTriangleColor[9];
float fQuadColor[12];

UINT uiVBO[4];
UINT uiVAO[2];

CShader shVertex, shFragment;
CShaderProgram spMain;

void initScene(LPVOID lpParam)
{
   glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

   // Setup triangle vertices
   fTriangle[0] = -0.4f; fTriangle[1] = 0.1f; fTriangle[2] = 0.0f;
   fTriangle[3] = 0.4f; fTriangle[4] = 0.1f; fTriangle[5] = 0.0f;
   fTriangle[6] = 0.0f; fTriangle[7] = 0.7f; fTriangle[8] = 0.0f;

   // Setup triangle color

   fTriangleColor[0] = 1.0f; fTriangleColor[1] = 0.0f; fTriangleColor[2] = 0.0f;
   fTriangleColor[3] = 0.0f; fTriangleColor[4] = 1.0f; fTriangleColor[5] = 0.0f;
   fTriangleColor[6] = 0.0f; fTriangleColor[7] = 0.0f; fTriangleColor[8] = 1.0f;
 
   // Setup quad vertices
 
   fQuad[0] = -0.2f; fQuad[1] = -0.1f; fQuad[2] = 0.0f;
   fQuad[3] = -0.2f; fQuad[4] = -0.6f; fQuad[5] = 0.0f;
   fQuad[6] = 0.2f; fQuad[7] = -0.1f; fQuad[8] = 0.0f;
   fQuad[9] = 0.2f; fQuad[10] = -0.6f; fQuad[11] = 0.0f;

   // Setup quad color

   fQuadColor[0] = 1.0f; fQuadColor[1] = 0.0f; fQuadColor[2] = 0.0f;
   fQuadColor[3] = 0.0f; fQuadColor[4] = 1.0f; fQuadColor[8] = 0.0f;
   fQuadColor[6] = 0.0f; fQuadColor[7] = 0.0f; fQuadColor[5] = 1.0f;
   fQuadColor[9] = 1.0f; fQuadColor[10] = 1.0f; fQuadColor[11] = 0.0f;

   glGenVertexArrays(2, uiVAO); // Generate two VAOs, one for triangle and one for quad
   glGenBuffers(4, uiVBO); // And four VBOs

   // Setup whole triangle
   glBindVertexArray(uiVAO[0]);

   glBindBuffer(GL_ARRAY_BUFFER, uiVBO[0]);
   glBufferData(GL_ARRAY_BUFFER, 9*sizeof(float), fTriangle, GL_STATIC_DRAW);
   glEnableVertexAttribArray(0);
   glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

   glBindBuffer(GL_ARRAY_BUFFER, uiVBO[1]);
   glBufferData(GL_ARRAY_BUFFER, 9*sizeof(float), fTriangleColor, GL_STATIC_DRAW);
   glEnableVertexAttribArray(1);
   glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);

   // Setup whole quad
   glBindVertexArray(uiVAO[1]);

   glBindBuffer(GL_ARRAY_BUFFER, uiVBO[2]);
   glBufferData(GL_ARRAY_BUFFER, 12*sizeof(float), fQuad, GL_STATIC_DRAW);
   glEnableVertexAttribArray(0);
   glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

   glBindBuffer(GL_ARRAY_BUFFER, uiVBO[3]);
   glBufferData(GL_ARRAY_BUFFER, 12*sizeof(float), fQuadColor, GL_STATIC_DRAW);
   glEnableVertexAttribArray(1);
   glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);

   // Load shaders and create shader program

   shVertex.loadShader("data\\shaders\\shader.vert", GL_VERTEX_SHADER);
   shFragment.loadShader("data\\shaders\\shader.frag", GL_FRAGMENT_SHADER);

   spMain.createProgram();
   spMain.addShaderToProgram(&shVertex);
   spMain.addShaderToProgram(&shFragment);

   spMain.linkProgram();
   spMain.useProgram();
}   

You may have noticed few new functions. They are concerning vertex array objects:

- Vertex Array Objects:

VAOs store multiple bindings between vertex attributes and data. Explained straightforwardly - imagine you have a 3D model with stored vertices, texture coordinates, normals and vertex colors. Now if you would like to render this model in new OpenGL, you would have to set all these four pointers and then call some rendering function, like glDrawArrays. Using VAOs, you can do this in single function call. Of course, you must set it on VAO setup, but you do this only once, and then you just change VAOs with single function call to change what you render. Pretty useful :) So as you can see, we create two VAOs, one for triangle, and one for quad, with glGenVertexArray function. This generates VAOs handles. Then we call glBindVertexArray, which takes VAO handle as parameter and makes it active (bind). If we set data pointers to specific vertex attributes now, we will have them stored in VAOs. So let's say, that first vertex attribute (with ID 0) is vertex position. We upload it into vertex buffer object, and then call glEnableVertexArrayObject with 0 as parameter to tell OpenGL, that this attribute (position) will be used when rendering object. After that, we just call glVertexAttribPointer, which tells OpenGL how should the data be treated. First parameter is vertex attribute ID (it's 0). Second is number of components per attribute. In the case of position it's 3, X, Y and Z attribute. Third parameter is data type (float in our case). Fourth parameter is whether data should be normalized (now I'm not exactly sure what it does, but better leave it and set it to false). Fifth parameter is stride - byte offset between two consecutive attributes (it's 0, since they are tightly packed). And last, sixth parameter is a pointer to the first component. In this case, pointer to the first component starts at stary of array, so it's pointer is zero.

Edit 14.05.2012: We're always setting pointers to the VBO that has been bound previously. Important thing is, that all these pointers are to CURRENTLY BOUND VBO, so when we setup VAO, we just need to have appropriate VBO with wanted data bound before calling glVertexAttribPointer. Then, when we bind VAO, all these data linkages are set and we can do rendering.

We will repeat this procedure for triangle color, which is vertex attribute with ID 1, and then repeat both these steps (setting position and color) for our quad.

In the end of InitScene function there are calls to functions from our CShader and CShaderProgram classes. The last function says that we're goiing to use program created by us.

The last thing that is left in this tutorial is the code of shaders itself. Let's examine vertex shader first.

- Vertex Shader:

If you'd open up file data\shaders\shader.vert, it looks like that:


#version 330

layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec3 inColor;

smooth out vec3 theColor;

void main()
{
   gl_Position = vec4(inPosition, 1.0);
   theColor = inColor;
}

This probably needs some explanation. First line specifies target version of GLSL. With each new OpenGL release, version of GLSL is same as version of OpenGL, so there's no need to worry. Since we use OpenGL 3.3, version is 330, for OpenGL 4.2 it would be 420 and so on. With next two lines, we associate shader variables (called inPosition and inColor), with vertex attributes, that we set from program. We have stored position as vertex attribute 0, and color as vertex attribute 1. That's why we set it like that. Keyword in says it's the variable that comes to shader. Then we can see another variable called theColor. It has two qualifiers - smooth and out. Out means that it's an output variable from vertex shader into next stage of pipeline (fragment shader in our case). Smooth means, that the values should be interpolated between vertices (it's like in old OpenGL calling glShadeModel with parameter GL_SMOOTH). Then comes main function. It's what is called when shader is executed (entry point). In this tutorial, we just set the built variable gl_Position to our incoming position. gl_Position holds homogeneous vertex position (in eye-space coordinates, correct me if I'm wrong). And we also set the output variable theColor to our incoming color, so it can be procesed by fragment shader later.
That's the end of vertex shader, let's look at fragment shader:

- Fragment Shader:

Contents of fragment shader are stored in data\shaders\shader.frag:


#version 330

smooth in vec3 theColor;
out vec4 outputColor;

void main()
{
   outputColor = vec4(theColor, 1.0);
}

Again, first line is the target version. Then we define two variables. First is named theColor. First thing you should notice is that the name of this variable is the same as in vertex shader. In order to identify output variable from previous shader (vertex shader in this case) in fragment shader as input variable (qualifier in), we must name it the same way.

Secondly, we must also preserve qualifier smooth to keep it consistent with output from vertex shader. This way, we have stored interpolated color in this variable. And the fragment shader is called for each pixel, so in each call this value will differ. There's also second vaiable with out qualifier. If you define out variable in fragment shader, it's the one that is sent to framebuffer in the end (I don't know about multiple out variables in fragment shader, I will examine it sometimes, and then probably update this info). Lastly we can see main function again. It's called for each pixel. Here we just send interpolated color to the pixel. As you can see, we constructed vec4 type (which is a vector in homogeneous coordinates), with vec3 (theColor) and a 1.0. GLSL has many constructors for each object type, so it's easy to work with similar, but not the same (vec3 and vec4) data types.

After doing all this stuff, we come to result - nicely colored triangle and quad. Yay!

Last change in code is addition of ReleaseScene function in RenderScene.cpp and also setting it as a release function, when creating OpenGL context.It releases shader program and shaders.

If you have made it this far, then congratulations. This tutorial was a long one. If you also paid attention, then you should be able to write first shader programs. If not, you can always read it again :) In the next tutorial, we will go 3D and start with basic transformations - translation, rotation and scaling.



Download (128 KB)
6434 downloads. 13 comments
 
Name:

E-mail:
(Optional)
Entry:

Enter the text from image:



Smileys




player on 24.02.2015 07:03:32
Nice work mate. Best tutorial on opengl till date.
Calmarius on 03.12.2013 14:38:25
So does this mean in OpenGL3 you have to write 50-100 lines of boilerplane and learn a new language because you want a single garden variety Gouraud shaded triangle? Isn't there a simpler way?
Mowei on 07.11.2013 08:06:13
The following two lines need to added in ReleaseScene(...) in order to avoid runtime error during wglDeleteContext function call. I tested it with Intel HD 4000.
glDeleteVertexArrays(2, uiVAO);
glDeleteBuffers(4, uiVBO);
Void on 21.01.2013 14:21:25
Great tutorials, but please, learn how, and more importantly, when, to use, commas!
aeqwa on 28.12.2012 11:51:35
Thanks you !
DutchDude on 11.12.2012 23:40:54
Thanks a lot! Great work.

I'm new to OpenGL3+ and there's a severe lack of decent tutorials on the subject. This is just what I was looking for.
Bob on 22.10.2012 19:25:46
Wonderful tutorial! However if you think the use of macros like this:

FOR(i, ESZ(sLines))sProgram[i] = sLines[i].c_str();

increases the readability of the program, you need to lay off the crack pipe.

Well done!
Michal Bubnar (michalbb1@gmail.comn) on 29.12.2012 23:00:44
Yea, I know it, but I mentioned it somewhere in first or second tutorial, that I'll be using macros like this FOR to simplify my code with being aware that it may decrease readability....
sigEleven (aidevelopment@gmail.com) on 02.08.2012 00:49:42
Great tutorial, but where is the drawscene function? I had to go dig through the source to find it. :(
Michal Bubnar (michalbb1@gmail.com) on 02.08.2012 08:40:36
It's always in the same file - renderScene.cpp, as it was in previous.
termus on 28.02.2012 12:48:48
thanks this is the best tutorial about shaders I found so far in the internet
_aspx_ on 12.02.2012 19:50:51
"... and then call glEnableVertexArrayObject with 0 as parameter to tell OpenGL, that this attribute (position) will be used when rendering object."
Did you mean "glEnableVertexAttribArray" function here? This is above the vertex shader description
Michal Bubnar (michalbb1@gmail.com) on 15.02.2012 14:22:16
Yes, you're right, thank you for reporting that mistake, I corrected it
Jump to page:
1