dokipen

Modern OpenGL: Part 3, Hello Triangle

There is quite a lot to learn and absorb in this chapter. Lets get stuck in!

Goals in this chapter

  • Draw our first triangle
  • Learn about vertex and fragment shaders
  • Passing Data from a vertex shader to a fragment shader
  • out and in qualifiers
  • How attributes are interpolated across triangles
  • compiling and linking shader programs
  • the glDrawArrays() function

Drawing a Triangle

That’s why we are here isn’t it? To draw triangles? Well drawing triangles themselves isn’t much fun. But you can make other things out of triangles. So we must walk before we can run.

OpenGL draws triangle by drawing 3 vertices at a time.

We can draw triangles by calling the glDrawArrays() function. We have to tell it what type of geometry to draw (in this case GL_TRIANGLES), an offset paramter (which we will cover in a later chapter) and how many vertices to draw.

When we ask OpenGL to draw 3 vertices when the mode is set to GL_TRIANGLES, it will connect them up and fill in the middle with pixels.

Here we are seeing 3 vertices that we have requested to be drawn by calling the glDrawTriangles function. We haven’t yet specified where those vertices are positioned in space yet (we will get to that very soon) but OpenGL assumes that they form a triangle and will interpret them as such and ‘fill in’ the middle for us.

The glDrawTriangles function just instructs OpenGL to draw triangles and doesn’t require that the positions be provided by your app yet. You can think of it like you are writing a ‘blank check’ where you specify how many vertices you want, but the positions will be specified at a later time. How? We will get to that soon.

If we want to draw more than 1 triangle we can simply draw more vertices. 3 for every triangle.

vertices 0, 1 & 2 form the first triangle, and vertices 3, 4 & 5 form the second. notice how the triangles are defined by the counter clockwise order of the points
You might be wondering, why points 0 & 3, and 2 & 4 are separate points if they share the same position. Isn’t that a waste telling opengl to draw an extra vertex there? You are right! But for now we are going to draw two separate triangle and move on to reusing vertices in chapter 13.

Setting Vertex Positions

We need a way to tell OpenGL where these vertices are, so it knows where to draw them. We need a way to process each vertex and output a position. We do that by using Shader Programs. These are actual programs that we write in a language called GLSL (GL Shading Language). They have a main function in which we set the position of the vertex. What does this program look like? Is it a big for loop that sets each vertex position one after the other? No actually, it is a program that each vertex get run on individually. That’s right, a little program is run for each vertex that sets the position for that vertex.

What does one of these programs look like?

Vertex Shader

#version 460 core

void main(){
    gl_Position = vec3(1.f, 2.71f, 3.1459f);
}

Here we see that it looks very much like other c languages. We have the #version 460 core line which tells opengl which version of the shader language we want this to be. New features are added in newer versions so we want to make sure that a certain feature set is available to us.

Then the main() function is what will be executed for each vertex. We set a special variable call gl_Position. This is the in-built variable that we have to write to to tell OpenGL where this particular vertex is positioned. OpenGL can then go on and draw the triangle using that position for that vertex. In this case, if we ran this program over all 3 vertices, then the vertices would all be at a single point and the triangle would have no area and hence wouldn’t be visible. That’s not good. So we need a way to tell each individual vertex to have it’s own unique position.

One way we could do that is to declare an array directly in the shader program itself.

#version 460 core

const vec4 positions[] = vec4[]( vec4(-0.5f, -0.7f,    0.0, 1.0), 
                                 vec4( 0.5f, -0.7f,    0.0, 1.0),    
                                 vec4( 0.0f,  0.6888f, 0.0, 1.0));

void main(){
    gl_Position = ???
}

But how do we index into that array for that particular vertex? Luckily in this situation, OpenGL provides us with a read-only built-in integer variable called gl_VertexID which is the number of the vertex being drawn. If we use that to index into the array, then each vertex will run the program, and get the right value written to gl_Position.

#version 460 core

const vec4 positions[] = vec4[]( vec4(-0.5f, -0.7f,    0.0, 1.0), 
                                 vec4( 0.5f, -0.7f,    0.0, 1.0),    
                                 vec4( 0.0f,  0.6888f, 0.0, 1.0));   

void main(){
    gl_Position = vertices[gl_VertexID];  
}

This diagram is a rough approximation of what is happening. We call glDrawArrays, tell it to draw 3 vertices and because it is told that the mode is GL_TRIANGLES, then we get a single triangle for those 3 vertices. Then each vertex will run the shader program that we wrote and get its position set by indexing into the array. The triangle will get Rasterized which means the stage where the pixels are filled with colour values. But there are a couple of issues that we need to consider…

  1. What colour will the pixels be set to?
    • Right now we haven’t stated what value the filled pixels will have. Can we even call them pixels yet?
  2. What if we have more than 1 triangle or want the values to change?
    • If we draw 6 vertices, or 9, or any amount, that means that we need a massive array in our vertex shader to be able to index into. Also we can’t access that array at the moment. Its statically compiled into the shader program. That isn’t feasible. In the chapter 5 we will tackle this issue (spoiler alert, we will be looking at vertex buffers).

For now lets look at the first issue, which is “what colour do we want to set the pixels to be and how do we do that?”

Setting Colour of Pixels

So we have sent our vertex positions, and the rasterizer has filled in where the triangle is going to be. But what has it filled? How can we colour the pixels if there aren’t any yet? Well that where fragments come in. You can think of fragments as like pixels that haven’t been born yet. They are a placeholder until we set what the colour is and then there are various process to get them turned into a pixel (outside the scope of this chapter).

So we have our fragments which are the tiny blank canvases which wil become our pixels, we just need to tell OpenGL what colour they should be. We do that with some thing called a fragment shader.

Fragment Shader – https://www.khronos.org/opengl/wiki/Fragment_Shader

Just like Vertex shaders which ran a little program over every vertex, we can run a little program over every fragment to set it’s colour. Just like with vertex shaders, we do that by setting attributes. The attribute that we set though is no longer a built-in like gl_Position was. We have to define an ‘out’ attribute of type vec4 that we will write to.

#version 460 core

out vec4 finalColor;

void main(){
    finalColor= vec4(1.0f, 1.0f, 1.0f, 1.0f);
}

This example shader sets the output colour of the fragment to pure white with an alpha value of 1 (fully opaque).

The values that we write to out attributes to set fragments are sent to temporary locations called framebuffers. This is a slightly advanced topic and the order and type of output attributes from fragment shaders and how they attach to framebuffers won’t be tackled now. All you need to know for now is that if you define 1 attribute of vec4 type, it will get automatically taken care of for you to render to the main ‘colour’ part of the framebuffer.

This example sets the colour for every fragment the same colour. What if we want to set each fragment a different colour? Setting the colour for each individual fragment could get messy if we decide to do it like we did with the vertex shader where we created an array in the shader itself. There are a couple of reasons why. First, we don’t know ahead of time how many fragments there are as the developer of the application (hopefully you) can make the resolution of the window any arbitrary size. Secondly, the array would have to be quite large as there could be hundreds, thousands or even millions of fragments. Thirdly, how would each instance of the fragment program decide which entry in the array it would index into? Before we used gl_vertexID, but there is no such equivelent for fragment shaders. (we’ll actually there kind of is but I’m not going to dwell on it here. Message me if you want to know more).

The alternative is to specify colours on the vertex shader and have the fragment shader recieve those colours as an attribute. So lets go back to our vertex shader and create an out attribute.

const char* vertexShaderSource = R"(
    #version 460 core
    out vec3 colour;

    const vec4 vertices[] = vec4[]( vec4(-0.5f, -0.7f,    0.0, 1.0), 
                                    vec4( 0.5f, -0.7f,    0.0, 1.0),    
                                    vec4( 0.0f,  0.6888f, 0.0, 1.0));   

    const vec3 colours[] = vec3[](  vec3( 1.0, 0.0, 0.0), 
                                    vec3( 0.0, 1.0, 0.0),    
                                    vec3( 0.0, 0.0, 1.0));   

    void main(){
        colour = colours[gl_VertexID];
        gl_Position = vertices[gl_VertexID];  
    }
)";

Line 3: Here we are explicitly telling OpenGL that for every vertex, we are setting a value that will be sent out of the vertex shader. In this case it is of type vec3 and we give it a name of our choosing, ‘colour’.

Lines 9-11: We are using the same trick as before by creating an array directly in the vertex shader.

Line 14: Then we use the same gl_vertexID built in variable to index into that array to pick a particular value for that vertex.

Receiving attributes in the fragment shader

So we are sending it out of the vertex shader, what happens to the attribute now? Well with the position attribute, OpenGL uses that information to know where to draw the triangle. For the colour attribute, by default it doesn’t do anything. We have to state in the the fragment shader that…

  • there is an in attribute that it will have to deal with
  • how we want that attribute to colour the pixels

The ‘in’ attribute qualifier

The first thing we do is state in the fragment shader that we are expecting an in attribute.

const char* fragmentShaderSource = R"(
    #version 460 core

    in vec3 colour;
    out vec4 finalColor;

    void main() {
        finalColor = vec4(colour, 1.0);
    }
)";

Here, we state the in attribute has type vec3 and has the name colour. You might notice this is named exactly the same as the out variable from the vertex shader. This is intentional. OpenGL will automatically detect that there is an out and in attribute on both the vertex and fragment shaders respectively and be able to figure out that they should be the source and target for each other.

There is another way to specify the association of the out and in attributes with special ‘layout qualifiers’. You don’t need to know about that just yet, but it’s worth planting a seed here so that later on I can introduce that concept and mention “remember when we associated attributes by name?! what were we thinking?!”

Attribute Interpolation

So we now have this attribute that is accessible in the fragment shader, but what value is it. And by that I mean, when the triangle is rasterized, all the fragments in the middle of the triangle are filled in. What vertex do they get the colour from? The nearest? When does it switch from one vertex colour to the next? The answer to this is attribute interpolation. Part of the triangle rasterization is that attributes that are specified per vertex are blended based on distance to the vertices. So if a fragment is in the middle of a triangle, then it will receive equal parts of each vertices colour (divided by 3 if it is in the middle only). If it is closer to one of the vertices, then it will receive a higher weighting of that vertices colour and if the fragment is exactly at the vertices position, then it will receive the full vertex colour.

TODO: insert illustration of attribute interpolation

The final code for chapter 2

Now that we know a little more about how shader programs work and how data flows between them and finally ends up on the screen, lets walk through the code for chapter 3 and see how it all fits together.

Here is the final code with the sections we haven’t changed collapsed (some parts have also been left expanded to help show the surrounding context).

… previous code (click to expand)
#include <array>
#include <chrono>     // current time
#include <cmath>      // sin & cos
#include <cstdlib>    // for std::exit()
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

using namespace gl;
using namespace std::chrono;

int main() {

    auto startTime = system_clock::now();

    const auto windowPtr = []() {
… previous code (click to expand)
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
        auto windowPtr = glfwCreateWindow(
            1280, 720, "Chapter 3 - Hello Triangle", nullptr, nullptr);
… previous code (click to expand)
        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }
        glfwSetWindowPos(windowPtr, 520, 180);

        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
  
    }();
 
    const char* vertexShaderSource = R"(
        #version 460 core
        out vec3 colour;

        const vec4 vertices[] = vec4[]( vec4(-0.5f, -0.7f,    0.0, 1.0), 
                                        vec4( 0.5f, -0.7f,    0.0, 1.0),    
                                        vec4( 0.0f,  0.6888f, 0.0, 1.0));   

        const vec3 colours[] = vec3[](  vec3( 1.0, 0.0, 0.0), 
                                        vec3( 0.0, 1.0, 0.0),    
                                        vec3( 0.0, 0.0, 1.0));   

        void main(){
            colour = colours[gl_VertexID];
            gl_Position = vertices[gl_VertexID];  
        }
    )";


This is the vertex shader that we will be telling opengl to use to

  • set the position of the vertices that will be used to rasterize the triangles
  • set the colour attribute that will be interpolated across the surface of the triangle.
    const char* fragmentShaderSource = R"(
        #version 460 core

        in vec3 colour;
        out vec4 finalColor;

        void main() {
            finalColor = vec4(colour, 1.0);
        }
    )";

This is the fragment shader that receives the pre-interpolated attribute that we can use to set the colour of the fragment. In this case we pass the attribute value directly into the final out colour by creating a vec4 value who’s first 3 arguments are specified by passing ina vec3 (our colour) and a float for the opacity to make a vec4.

    auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);

When we were learning about shaders in the middle of this chapter, we didn’t say how to actually get the source code into OpenGL and tell it to use that program. That is what we will do here.

  1. We first have to create a shader object by calling the glCreateShader function and passing an argument GL_VERTEX_SHADER which tells opengl that this object is going to be a, you guessed it, vertex shader. This isn’t the object that will do the work, its just a variable that represents the shader and allows us to uniquely reference it by this c++ variable (its actually just an integer id).
  2. We will associate our source code with that shader object by calling glShaderSource and passing it an address to a c-string.
  3. We have to compile the shader by calling glCompileShader. This is similar to how you compile c++ programs, its just that the OpenGL driver compiles the shader while your c++ program is running.
    auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);

We have to do the same for the fragment shader.

    auto program = glCreateProgram();
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);

The shader program isn’t ready just yet. We have to create a program object by calling glCreateProgram. Then we have to attach the vertex and fragment shaders to this program by calling glAttachShader(). This is all very boilerplate-y isn’t it. Is that a word? I don’t know.

    glLinkProgram(program);
    glUseProgram(program);

Finally we are almost ready to use the program. The second to last thing we need to do is to link the program. Compiling is the process of taking the source code and turning it into machine code, but there is one final step to produce and executable, and that is to link the separate vertex and fragment shaders. This is where all attributes are linked up and any final errors in the shaders are found.

Then we need to tell OpenGL to actually use the program. If we don’t it won’t know how to process the vertices and colour the fragments.

    // in core profile, at least 1 vao is needed
    GLuint vao;
    glCreateVertexArrays(1, &vao);
    glBindVertexArray(vao);

This is one step that I wish we didn’t have to do at this stage, as we haven’t learnt about VAO’s yet and dont need to know about them at this stage. But unfortunately, we need to have one of these things in our application for OpenGL to be able to draw things. I hate asking people to do this, but ignore this for now. We will revisit in chapter 5. Just copy and paste it into your application.

For those dying to know, VAO’s are Vertex Array Objects and they store bits of information about vertex data, type, format and location for us.
… previous code (click to expand)
    std::array<GLfloat, 4> clearColour;

    while (!glfwWindowShouldClose(windowPtr)) {

… previous code (click to expand)
        auto currentTime =
            duration<float>(system_clock::now() - startTime).count();
        clearColour = {std::sin(currentTime) * 0.5f + 0.5f,
                       std::cos(currentTime) * 0.5f + 0.5f, 0.2f, 1.0f};

        glClearBufferfv(GL_COLOR, 0, clearColour.data());

        glDrawArrays(GL_TRIANGLES, 0, 3);

And now we have told OpenGL what shader program to use, we just have to tell it to draw a triangle by calling the glDrawArrays() function with 3 vertices and you should see a triangle!

… previous code (click to expand)
        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

    glfwTerminate();
}

Thats the end of this chapter! Phew! That was actually a lot of information and you have just ingested a massive bulk of what OpenGL is all about. No doubt you may have experienced an bug here and there due to typos or mismatches attributes in your shader. If something did go wrong in your c++ program code, hopefully the c++ compiler was able to give you some kind of indication as to where or what the error was. But how do we know if our shader is compiling and linking correctly? You might be getting a black triangle or even no triangle at all and not know why. That can be VERY frustrating, let me tell you (you probably know). I’ve been away at 2am many times trying to get opengl to render a triangle and not know why it isn’t working.

In the next chapter, we will see how we can easily enable some built in debugging features in OpenGL so that if something does go wrong, we can at least be notified about it and even maybe be told what is wrong. Catch you later.

Dokipen

Modern OpenGL: Part 2 Window Creation

Now we get to the fun part!

This chapter will see us…

  • create a window in our operating system,
  • start a render loop
  • clear the background colour

Headers

Lets start with the headers that we need to include

#include <chrono> // current time
#include <cmath> // sin & cos
#include <array>
#include <cstdlib>    // for std::exit()

#include <fmt/core.h> // for fmt::print(). implements c++20 std::format

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

We start with some standard library headers. We use…

  • <chrono> to be able to query the current time
  • <cmath> so we can feed that time into the sin and cos functions to get some animated values.
  • <array> to store some colour values and
  • <cstdlib> for exiting the program if our window isn’t initialized correctly

Then more interestingly we will use…

  • <fmt/core.h>

…to be able to print formatted text to the console. This deviates from using c++ iostreams and std::cout to output from our program. The fmt library is being standardized into c++ and part of it is already in c++20. We use the library here which is c++11 compatible and has the fmt::print() function which we will use extensively.

Then we need a library that will actually open a window for us. We are using GLFW so we will include…

  • <GLFW/glfw3.h>

the header along with a special #define GLFW_INCLUDE_NONE which will allow us to combine glfw with the next library…

  • <glbinding/gl/gl.h>
  • <glbinding/glbinding.h>

which gives us access to all of opengl’s functions (there are ways to do this manually but this library makes it really easy).

using namespace gl;
using namespace std::chrono;

Here we use some ‘using’ aliases to make our code a bit more readable and to also reduce the need to put gl:: in front of all of the OpenGL calls. glbinding actually exposes the opengl functions within it’s own namespace, so this effectively make our program look like its calling opengl functions directly.

int main() {

    auto startTime = system_clock::now();

So here we are in the entry point to our program. We start off by capturing the current time using std::chrono. This will allow us to do animation.

    const auto windowPtr = []() {
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

Next we start the initialization process. This is where we will be getting our program ready to be able to use OpenGL. First is calling the glfwInit() function to tell glfw to get ready to do its thing. If glfw failed to initialize then we exit the program. Rather than rip out this code into a separate function, I’ve put this section into a c++ immediately invoked lambda. This is pretty much like any other function, just that its declared ‘in place’. There is no reason to do this in this case as its code that it only runs once and could have been written without it but it indents the code and its a feature that we will use in future chapters.

      
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);

Here we are setting a hint to glfw that we will want it to create an OpenGL context that is a particular version. The context is the ‘instance’ of opengl that we send commands to. We want the latest and greatest so lets ask for 4.6

        auto windowPtr = glfwCreateWindow(
            1280, 720, "Chapter 1 - Window Creation", nullptr, nullptr);

Now we actually ask glfw to create our window! We give it the dimensions that we want our window to have as well as a title that will be displays in the title bar. It returns a raw pointer to a GLFWWindow.

        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }

If the pointer returned was null, then glfw had trouble creating the window and we terminate the glfw library and exit the program.

        glfwSetWindowPos(windowPtr, 520, 180);
        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
    }();

Then we instruct glfw to make our window be associated with the current opengl context.

Then we call on the glbindings library to initialize. This will use the glfwGetProcAddress function to create for us all of the opengl functions that the default gl header doesn’t expose. This has to reach into some deep guts of opengl and if we did all of that boilerplate manually, then it would look pretty ugly, so calling this one function really helps us out here. Also it can do this lazily so that it only does this when we call the functions on demand (thats what the false parameter does. If we wanted glbindings to do this work eagerly up front, we could call it with true).

    std::array<GLfloat, 4> clearColour;

We will want to clear the background of our window so we create a std::array of 4 floats which are the Red, Green, Blue and Alpha values that we want to clear. We don’t initialize it yet as we will be filling it our with live values inside our render loop.

    while (!glfwWindowShouldClose(windowPtr)) {

        auto currentTime =
            duration<float>(system_clock::now() - startTime).count();

        clearColour = {std::sin(currentTime) * 0.5f + 0.5f,
                       std::cos(currentTime) * 0.5f + 0.5f, 0.2f, 1.0f};

        glClearBufferfv(GL_COLOR, 0, clearColour.data());
        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

Here we are in our render loop. In each loop iteration we check for the condition of wether the window should close. glfw can tell us this based on wether the you or the user clicked on the close window button, or maybe the os closed the window for some reason.

Then we calculate the current time since the program started. We do that by taking the difference between the current time (which is measured from 1970 or something like that( and the time captured at the beginning of the program. Using that time, we set the value of the clear colour so that the colours sweep though various values as the sine and cosine waves propogate though time.

Then we actually clear the colour buffer using that colour we have just set.

Once the buffer is clear, we then tell glfw to swap the back buffer to show the image that we have just drawn with OpenGL

    glfwTerminate();
}

And finally we terminate glfw so it can wrap itself up. We now have our program displaying some pretty colours!

Modern OpenGL: Part 1 Introduction and Setup

Hello and welcome to my new blog. For the first series of posts, we are going to be exploring OpenGL. I’m hoping that the reason that you are here is because you already know what OpenGL is and that you are looking for something a little bit different. If you are not familiar with OpenGL, then it is a Graphics API (Application Programming Interface) that is a specification. All the OpenGL specification states are some functions and the expected behaviour they should invoke based upon a bunch of state. Any piece of hardware or software is allowed to implement OpenGL to result in something that when you call those function, you get an image out the other end. Its pretty old now as well. But don’t let that discourage you!

Why do we need another OpenGL tutorial?

Yes, there are hundreds of OpenGL tutorials out there, why should you consider following this one? Well my first answer to that would be… follow ALL of them. There are some great sites, books, videos and articles out there. I myself learned a lot from them. Why am I making this series then?

  1. I couldn’t find any tutorials that taught OpenGL with the Modern ‘Low Driver Overhead’ Approach. This is the tutorial I would have wanted when I was learning OpenGL. This series differs from some other resources out there in that it focuses on totally modern OpenGL and skips to the most performant style and skips some legacy stuff.
  2. There is no harm in there being another resource out there for people to learn from. I myself learn best when there are multiple resources that all explains things slightly differently that I can cross reference and fill in any gaps in my understanding from one resource or another. I have my own way of explaining things. The way I explain things may be slightly different to someone else and that might help something click in someone’s mind. I am inspired by other content creators (I can’t go without mentioning @JoeyDeVriez from learnopengl.com and an Yan ‘@TheCherno‘ Chernikov from The Cherno youtube channel), so I want to spread my own insights as well in the hope it might inspire others.
  3. The Cherno’s lessons are quite comprehensive and are great at showing of how various parts of opengl work. I wanted my course to be quite linear in structure so someone can follow in one straight path. Also I wanted to keep it as simple as possible and not have too much abstraction until quite further down the line, and even then I am thinking of a separate tutorial series for game engine architecture which would deal with a lot of the abstraction.
  4. This is also a way to ensure that I myself understand the material.

If I can’t explain it, then I don’t contain it!

Daniel ‘dokipen’ Elliott Jones

Main Goals

  • Simple and Clean Code – This means readable code with clear names and not introducing complexity for complexities sake.
  • Incremental Learning – We only just enough to get the point across and then once we have fully understood a topic, we move on.
  • Have fun – other wise what’s the point of us being on this ball in space?

The Tools we will use

  • C++ 11 using modern best practices, which means RAII, lambdas, standard library data structures and algorithms etc… (c++11, 14, 17 and now 20 are fine, its just that we wont really be using any features which distract us)
  • vcpkg to manage our dependencies (no-one likes spending hours building libraries and dealing with linclude directories and linking errors)
  • cmake to configure our project
  • Visual studio for the c++ compiler (well we don’t have to, but it keeps setup simple).
  • QtCreator as an IDE (I use this at work and is a pretty good IDE with debugging interface and code navigation)
  • GLFW3 as out windowing library
  • glm as our math types and operations library
  • glbinding for getting our OpenGL extensions. This is a modern alternative to GLEW and even has some awesome error checking helping functionality.
  • fmt lib as out text formatting and printing library. This library is already being standardized into c++20 so we should get used to using it. Think of it as a type-safe printf on steroids or c++’s answer to python’s string formatting.

What is ‘modern’ OpenGL?

There are two main goals for making OpenGL fast.

  1. Reducing Draw Calls – These are the functions you invoke to tell OpenGL to render something. Every time you do one of these, there is a cost. We will be looking at techniques that allow us to render lots of things with less draw calls.
  2. Reduce Binding – Binding is the action that you have to perform to make something ‘current’ or ‘active’ before you use them. For instance if you want to upload vertices, you have to ‘bind’ a vertex buffer to a particular binding point, before you can upload data to it or change parameters on it. ‘Binding’ is the way that OpenGL historically was designed due to it being a ‘State Machine’, where all operations depended on OpenGL being in a valid state. You would typically have to bind an object (like buffers and textures) to make them the ‘current’ object. Then any subsequent operations would apply to that object.

After watching some youtube videos from recent years, this is what ‘modern’ OpenGL means to me….

  1. ‘Direct State Access’ (DSA) functions
  2. Multi-Draw Indirect
  3. Texture Arrays (and Bindless Textures)
  4. Manual Synchronization of Data Upload

There are other aspects of course, but these stand out as being the top things that help performance. What are they I hear you ask? The four items listed above are features designed to reduce time spent in the OpenGL driver on two things, ‘Binding’ and ‘Validation’ (the checks behind the scenes that the driver would perform that would ensure that everything was ‘correct’ and that no errors would occur due to the user calling an operation while the wrong thing was bound.)

So what are those 4 things I mentioned?

Direct State Access

This is how we can avoid a lot of the issues around binding.

Like we have said previously, we would have to bind an object to be able to perform some operation (for example we would have to bind a buffer to be able to upload data to it, bind a texture to upload texture data or bind a shader program to be able to send uniform data to it). With Direct State Access, we can now perform operations directly on objects by using and id/name. Want to upload data to a buffer? Just do it!

It not only reduces the amount of binding you have to do, it also potentially reduces errors because it is harder to forget to bind something before. It can’t remove the need for binding, but can minimize it a lot.

Multi-Draw Indirect

Multi-Draw Indirect is a feature that allows users to bunch up a load of Draw Calls (a function you invoke to cause OpenGL to do some drawing) and submit them all at once. This can reduce the need for the driver to perform validation after each individual draw call. Even better, these draw calls can be built in parallel on the CPU to make the most of your cores.

Texture Arrays

A Texture Array is bascially like a standard texture except that is contains many ‘layers’ stacked on top of each other. Each layer can store a whole texture and it can be addressed by its layer index. This again can reduce the need to bind multiple things.

The only downside to Texture Arrays is that one texture array can only contains textures that are all the same size/format. That means that if you want to have textures of different sizes and formats in your program, then you will have to have a different texture array for each size and format combination. The limitation of OpenGL being able to bind only usually 80 minimum textures at once (which is why, as you’ll see, we use texture arrays to get around this limitation) means that we can have 80 texture arrays bound once and never have to rebind. Hopefully you can design your content pipeline around this constraint which will make life a lot easier on the OpenGL side. For example, we could support 512, 1024, 2048 and 4096 textures with the RGBA format which would take up 4 slots.

Manual Synchronization

The GPU is a very complex processor and doesn’t actually work in lockstep with your CPU. When you submit work to the GPU, it goes off and does work completely separately from the CPU with no guarantee as to when it will complete. You can choose to do simple waiting for the GPU to finish, but this effectively blocks the CPU and make it sit idle when it could be doing useful work. Adding in some code to indicate when the CPU and GPU should synchronize with each other can improve performance a lot, especially when you are uploading data to the CPU often.

Hardware Support for OpenGL 4.5+

Support for OpenGL 4.6 on hardware is okay. You may still have a graphics card or integrated graphics that does not support the full OpenGL 4.6 core feature set. I found myself recently trying to code against some older cards and had varying success. I was trying out a early 2013 Macbook Pro in BootCamp and found that the Nvidia 650m had full OpenGL 4.5+. But the Late 2014 Mac Mini only has 4.3. Also OSX is deprecating opengl and only has support for 4.1.

Luckily even some of the hardware that supports OpenGL 4.3 has what are known as ‘Extensions’. These are features of OpenGLthat are implemented semi-officially with the intention that they will become part of a later core version of OpenGL. So while 4.3 might be the core supported version, there might still be extensions supported by the driver for the above features. Bindless Textures isn’t actually core (and is one reason why I don’t fully endorse using them), but using them can be a great performance win and does have wide support on most recent graphics cards.

In this series, I will actually be targeting 4.5. This is because I am able to do some development on my macbook with doesn’t normally support opengl versions above 4.1. But with an ubuntu virtual machine with a piece of software called llvmpipe which is a software implementation as part of the mesa project, I am able to test my code anywhere I go. It is also a conformant implementation and gives great debug messages. In the future, I am looking forward using another piece of software called zink (also a mesa project) to run opengl 4.6 on top of vulkan on moltenvk on metal on mac.

Project setup

First we should install these….

  • Visual Studio 2019 Community Edition, along with its C++ Desktop workload
  • QtCreator
  • Cmake 3.18+
  • vcpkg

Visual Studio

I’m going to be installing visual studio to be able to use it’s compiler. It is possible to download the build tools separately, but I’ve had trouble with that before so installing the full thing seems to work good for me. Plus if you ever need to go in and use its editor, then you can. But I’m going to be using a different IDE in this series. Feel free to use whatever IDE you feel comfortable with. I myself like to use Visual Studio Code from time to time as well.

https://visualstudio.microsoft.com/vs/community/

QtCreator as our IDE

I’m choosing to use QtCreator as the IDE. It has the option to open an existing project which just consists of a folder of source files and a CMakeLists.txt (yes I know Visual Studio can do that too but it’s not been completely reliable all the time for me).

https://www.qt.io/download-qt-installer

https://www.qt.io/offline-installers

CMake

https://cmake.org/download/

Dependencies with vcpkg

I’ve chosen to use vcpkg for this project as it is simple enough that using the libraries from vcpkg will be enough. If this was a more serious project and I needed more control over the build setting, then I would probably build them manually. But this is so convenient and hopefully should be the same for if you are following this. I like to minimize c++ build shenanigans as much as possible. I’ve spent too many hours into the night trying to build boost and whatever other libraries to know that if you don’t have to then just dont.

I won’t tell you here how to install vcpkg, you can find that out easily from the github page. I’ll just tell you which dependencies we will need for now. As time goes on, we might add to this list.

https://github.com/microsoft/vcpkg

./vcpkg.exe install glfw3:x64-windows # for windowing
./vcpkg.exe install glbinding:x64-windows # for OpenGL extensions and easy error handling
./vcpkg.exe install stb:x64-windows # for texture loading
./vcpkg.exe install fmt:x64-windows # for c++20 style printing to the console

These are the step you can take to get a project up and running…..

  • Create a chapter1_HelloWorld.cpp file. This is going to contain our main function (the entry point into the program.
#include <fmt/core.h>  // for fmtprint() implements c++20 std::format

int main() {
    fmt::print("This is going to be fun!\n");
}

Store it in a src folder in your project directory and at that same level, create a CMakeLists.txt file which is going to configure our project for compiling and linking.

  • In the CMakeLists.txt file, add this content to set up the project. I won’t go into too many details now. Hopefully its a simple enough script to be self explanatory.
# The name of our project
project(OpenGLTutorial)

# we want to use a recent version of cmake
cmake_minimum_required(VERSION 3.18)

# makes sure we have dependencies on our machine. sets variables for us to be able to pass to the linker
find_package(OpenGL REQUIRED)
find_package(glbinding CONFIG REQUIRED)
find_package(glfw3 CONFIG REQUIRED)
find_package(glm REQUIRED)
find_package(fmt CONFIG REQUIRED)

# takes the files in the src directory and adds them to a variable called SRC_LIST
aux_source_directory(src/ SRC_LIST)

# tells cmake that we are making an exectutable program 
# whos source is from the files in the SRC_LIST variable
add_executable(chapter1 src/chapter1_HelloWorld.cpp)

# tells the compiler to use c++ 11 
set_property(GLOBAL PROPERTY CXX_STANDARD 11)

# create a variable which will be the libraries that we want the c++ compiler to link to
set(LIBRARIES fmt::fmt
    fmt::fmt-header-only
    glfw
    ${OPENGL_LIBRARIES}
    glbinding::glbinding
    glbinding::glbinding-aux
    ${STB_INCLUDE_DIRS}
    )

# which libraries our program must link against using the variable we previously set
target_link_libraries(chapter1 PRIVATE ${LIBRARIES})
  • Open QtCreator and go to the Tools -> Options menu item. Under the Kits section, you should see the MSVC compiler detected. I like to make the 64-bit one default. If you installed msys/mingw as part of the full Qt installer, then that might work for you as well.
  • Then choose to ‘Open’ a project.

Navigate to the directory where your CMakeLists.txt file is

  • To configure the project (which will invoke cmake for us) we should choose the Kit that we want to use. This also allows us to choose where we want the build files to go.
  • We also need to tell CMake where to get the libraries that we need to compile and link against. We can do that by passing it our vcpkg tool chain file. (If you have forgotten where that is, just go to the vcpkg folder, open a cmd/powershell terminal and type vcpkg integrate install and it should print the path for you. Paste that path into the ‘Initial CMake parameters window (which is found in the Build section).
DCMAKE_TOOLCHAIN_FILE=C:/yourPathToVcpkg/vcpkg/scripts/buildsystems/vcpkg.cmake

CMake should be invoked now and you should see in the General Messages window that the generating is done and now you can hit Ctrl + R to run the program. You will then get a printout in the Application Output tab….

Now we have everything up and running then please continue onto Part 2, where we will be creating a window!