Beginning OpenGL Development: Abstracting Shaders

In my last post, I showed how we can draw a triangle with a single giant file of C++ and OpenGL code. As I’ve said this gets unweidly and hard to refactor. This post will mark the start of a series that shows how I abstracted different parts of the code from the previous post with the end goal of being able to only focus on the code that matter to what we want to render, without being bogged down by setup and details.

The general overview of what I do here is basically to move the shader code to their own files, and then load them when we need to compile them and attach them to our GL program.

I’m still very much a novice at OpenGL programming, but I’ve been told that it is generally not advisable to continually load and compile shaders while your GL program is in it’s main loop, but sometimes it’s unavoidable. When it is unavoidable, this technique may not be very performant as it involves going back to disk to load a file and compile it.

Note: I have not been able to find how anyone else does this, and in every example and tutorial I’ve seen, the shaders are always written directly in the code. If anyone has a better way of doing this, please let me know!

Anyway, this technique is very simple, I save the shader files to separate files, and just use a small C++ function to load them from disk into a string. Then we can use then like in our previous post: pass them into GL for compiling and attaching. The second part of this post, I will show how I create a simple shader class to handle all that GL stuff for us.

So, let’s get our shaders into separate files:

These are the exact same shaders as in the previous post, but saved in vertex.shader and fragment.shader for the vertex and fragment shaders, respectively.

We then have a utility function that will read a file and return the contents of that file as a C string.

At this point, we can already clean up the code from our previous post:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  // Our code changes from:
  static const char * vs_source[] =
  {
      "#version 410 core                                                 \n"
      "                                                                  \n"
      "void main(void)                                                   \n"
      "{                                                                 \n"
      "    const vec4 vertices[] = vec4[](vec4( 0.25, -0.25, 0.5, 1.0),  \n"
      "                                   vec4(-0.25, -0.25, 0.5, 1.0),  \n"
      "                                   vec4( 0.25,  0.25, 0.5, 1.0)); \n"
      "                                                                  \n"
      "    gl_Position = vertices[gl_VertexID];                          \n"
      "}                                                                 \n"
  };

  static const char * fs_source[] =
  {
      "#version 410 core                                                 \n"
      "                                                                  \n"
      "out vec4 color;                                                   \n"
      "                                                                  \n"
      "void main(void)                                                   \n"
      "{                                                                 \n"
      "    color = vec4(0.0, 0.8, 1.0, 1.0);                             \n"
      "}                                                                 \n"
  };

  // To this:

  GLchar* vs_source = Util::fileToBuffer("vertex.shader");
  GLchar* fs_source = Util::fileToBuffer("fragment.shader");

How simple is that?! We’ve abstracted our shader code so that it’s not in our C++ code. This gives us a few benefits:

  1. No giant messy string with formatting characters in our C++ code.
  2. We can run our shader code easily through some sort of syntax checker or linter.
  3. We can modularize shaders and put them in their own packages if we need to re-use them for different projects or whatever.

Finally, I’ve written a wrapper class for dealing with the management of shaders:

Most of the code in the CPP file is self-explanatory and taken directly out from the code in our main.cpp file from last time.

And voila! After we modify our main.cpp file to use our Shader class, it looks like:

The main lines to note here are between 69 to 79.

And of course the Makefile has changed. You can find the full source code for this post here. You can follow my progress for the project on Github.

That’s it. This was a long post, but not too much concepts to grasp. The next post will again be abstracting out more things for OpenGL into more re-useable chunks.

Comments