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.
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.
If we want to draw more than 1 triangle we can simply draw more vertices. 3 for every triangle.

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…
- 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?
- 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).
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.
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.
- 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).
- We will associate our source code with that shader object by calling glShaderSource and passing it an address to a c-string.
- 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.
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