Introduction

First of all, you shall have read the Getting Started section, so that you have the tutorial code and can build and run the first tutorial. Most of what we do in this tutorial is covered in the first lecture, so make sure to have looked into it. If you need more help on OpenGL, you can find more information through:

  • The OpenGL specification (pdf). This is the reference manual for all things OpenGL.
  • The OpenGL Reference Pages. Here you can find detailed descriptions of all OpenGL API functions.
  • The OpenGL wiki. A community maintained wiki with lots of good information on different concepts.
  • A LAB assistant. The friendly people running around the lab rooms. Names and more info at the footer of the website.

In this tutorial we will start familiarizing ourselves with OpenGL. To do this, there is a simple OpenGL application that you will study. Look at the source-code (lab1_main.cpp). Make sure you read the comments. At the end of this tutorial you should understand all of this as you will need it for future tutorials.

Learn to debug

Start State

First of all we are going to start the program without debugging (Ctrl+F5) to see what it shows. It should show a white triangle over a blue background, as in the image on the right, but it's showing a black background instead.

Notice that in the output terminal window appears text reporting an OpenGL error. To debug it we will start the program with the debugger attached to it, so let's stop it first now.

Start the program but this time with the debugger attached by pressing F5. As soon as it starts you will be informed that your program has triggered a breakpoint. Press the "break" button and we will use the debugger to find out what went wrong.

Exception breakpoint

The little yellow arrow tells you at which line the debugger noticed that something was wrong. This time the program broke because an OpenGL error occured. Look at the console window that starts with your application and you will see that you have actually written out that there was an error, before breaking.

Now find the window called "Call Stack". If you read this list bottom-up, you can see the functions your program has been in before causing the error. Apparently, main() called display(), which called some internal driver stuff that eventually lead to a crash. Double click the "display()" line.

Callstack

This will take you to the OpenGL call that crashed the code. Apparently, some moron has given the wrong input to a function. Stop the program (Shift-F5), fix this bug (you should find the fix in the next line) and run the program again to make sure everything works.

Checking that you found the error and fixed it, choose what function call was the problem:

Click here to see solution (But try yourself first! You have to understand each step.)

Interactive UI elements

To get some interactivity in the tutorials we use the library ImGui to create an overlay with UI elements. These elements, e.g. buttons and sliders, can be used to control various settings and parameters in the coming tutorials. To showcase the basic principles of how it works three sliders have been added to this tutorial. Use them to control the background color of the window. Enable this overlay by uncommenting the gui() call, marked with the comment // Task 1 in the code, which is right after the call to display() in the main loop.

Run the program again. You should now see a new little window where you can change the color of the background. Look at the gui() function and see that you understand how this happens. You can move, resize minimize this window, and your application will remember this for the next time you run the program.

Click here to see solution (But try yourself first! You have to understand each step.)

Learn how a triangle is drawn

The program we will work with currently draws a single white triangle over a blue background. Begin by taking a look at the source code.

Draw calls

The code initializes vertex buffer objects in three steps, as shown in the left column below. In the right column, fill in the correct description of what the function does from the drop-down menu.

Function Description
glGenBuffers()
glBindBuffer()
glBufferData()

Colors

Now, your first task is to change the color of the triangle. If you only change the values in the appropriate vertex buffer object, you'll notice that nothing happens. The reason is that the vertex- and fragment-shaders are not complete yet. We'll fix this in the following steps! Take a look at the simple.vert file. The vertex color is declared as the attribute color in the beginning of this shader, but from then on it is ignored.

We'll need to pass the vertex color value on to the fragment shader, so declare a second output from the vertex shader (marked with // Task 3, before main()) like this:

out vec3 color;

Then, set this output variable in the main() function:

color = in_color;

Now, open the fragment shader (simple.frag) and tell it to expect a variable color from the vertex shader:

in vec3 color;

And then change this line that currently sets all fragments to be white:

fragmentColor = vec4(1.0, 1.0, 1.0, 1.0);

to instead use the color passed in from the vertex-shader:

fragmentColor.rgb = color;

There! Now make sure your vertex buffer object has some fun colors in it and run the program again.

Click here to see solution (But try yourself first! You have to understand each step.)

Shader attribute layout

Somehow the data from your colorBuffer vertex buffer object ends up in the `color' attribute in the vertex shader (simple.vert). Similarly, the positionBuffer vertex buffer object data ends up in the `position' attribute. The chart below shows how the program links these different parts together.

VAO diagram

Objects here are either buffer objects (BO) or vertex array objects (VAO). Specify the type of the objects by writing BO or VAO in the chart in the "Type:" fields. The table below lists three OpenGL calls. Each call corresponds to a link in the chart. Identify which link by writing the numbers 1-2 in the table!

Link Number OpenGL Call / Code
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glVertexAttribPointer(1, 3, GL_FLOAT, false/*normalized*/, 0/*stride*/, 0/*offset*/ );
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glVertexAttribPointer(0, 3, GL_FLOAT, false/*normalized*/, 0/*stride*/, 0/*offset*/ );
glEnableVertexAttribArray(0);

The function glVertexAttribPointer() has the following signature:

void glVertexAttribPointer(GLuint index,
							GLint size,
							GLenum type,
							GLboolean normalized,
							GLsizei stride,
							const GLvoid * offset_pointer);

The first three arguments are pretty straightforward to understand. The other three aren't used in these labs but a simplified description for each is:

  • normalized, for integer inputs, it either normalizes them so they become values between -1 and 1, or transforms the actual value to float.
  • stride is distance between the end of the attribute for one vertex and the start of the next one. 0 means they are tightly packed.
  • offset_pointer indicates that the attribute for the first vertex begins N bytes from the beginning of the bound buffer. It's represented with a pointer due to backwards compatibility.

Shader initialization

The vertex- and fragment-shaders are linked together into a single program object, which is used (glUseProgram()) to draw the triangle. The chart below illustrates the process. Complete the chart by filling the "Created with:" fields with the name of the OpenGL function that is used to create the objects. Next associate the numbers 1-4 and the letters A-C with the correct OpenGL calls in the table below.

Shader compilation diagram

Link (number/letter) OpenGL Call/Code
 glLinkProgram()
 glCompileShader()
 glCompileShader()
 glShaderSource()
 glShaderSource()
 glAttachShader()
 glAttachShader()

To reiterate: the purpose of this tutorial is to understand how the triangles end up on screen. If you are working in a group, it may be a good idea to interrogate one another, let one explain to the other(s) what each line does, and the person listening should make sure that he or she is satisfied that the answer really explains what is going on. And again, if the OpenGL specification fails to help, ask an assistant!

Shader linking

The vertex shader (simple.vert) outputs a color, which is passed to the fragment shader. How is the color output from the vertex shader linked to the color input of the fragment shader? (pick one of the following three alternatives)

More triangles!

Now, your task is to draw a second triangle. This shall be accomplished by creating a new vertex array object in the initGL() function and then draw that object in the display() function. The second triangle must not cover the existing triangle, you are otherwise free to place both triangles as you please. For reference, see the next diagram to see a reference for the screen-space coordinate system we are currently using.

Coordinate system

Next, you shall add a third triangle to the scene. This time, you shall not create a new VAO. Instead, simply add position and color data to the previous VAO. Again, this triangle may not overlap any of the previous triangles.

The final result could look something like the picture below.

Three triangles

Click here to see solution (But try yourself first! You have to understand each step.)

Shader execution

How many times are the vertex- and fragment-shader's main() functions executed when a single frame is rendered? (refer to image above)

Vertex Shader
Fragment Shader

Using Uniforms to change color

Let's now make it so we can change the color of the first triangle without having to recompile every time.

In the gui function in lab1_main.cpp copy the following code right after the similar call already present there:

ImGui::ColorEdit3("triangle color", &g_triangleColor.x);

There are several ways that information can be used as input to shaders. The most straightforward is by using vertex buffers, as we have done until now, to give independent values for each of the vertices in the geometry. If we want all the vertices in a rendered geometry to use the same value for some property, we can use uniforms.

Uniforms can be thought of as a constant value that can be changed from the CPU code in a draw-call basis. That is, they have the same value for all shaders executed as a result of a specific draw call (glDrawArrays or similar), but can be changed from one draw-call to the next: you can set a uniform to some color, call glDrawArrays for one triangle, set the uniform to a different color and call glDrawArrays for a second triangle, and each will receive the specified color in the shaders.

In this case, we want to use the value of g_triangleColor for the color of the triangle.

First of all we need to receive the color in the shader. In the simple.frag shader add the following line right before void main(), after the // Task 5 comment:

uniform vec3 triangleColor = vec3(1, 1, 1);

And include in the color calculation by multiplying it with the color, inside the main function.

For this to work well, the single triangle should have its vertex colors defined to be white, in the cpp initialisation code.

If you run the application now, the GUI should have a new color field that you can change, but the color of the triangles should stay the same as it was. After all, we have set up the GUI to change a variable in the CPU, and we have set up the shader so it has a uniform input, but we have not sent the value of the CPU variable to the uniform.

Now find the draw call (glDrawArrays) that renders the single triangle, in the display function. When it's executed, OpenGL will send the current render state to the gpu, so we need to set the uniform before this function call. Add the code:

labhelper::setUniformSlow(shaderProgram, "triangleColor", g_triangleColor);

The setUniformSlow collection of functions take the shader program (it must be the active one), find the uniform with the given name, and set its value to the one given as the third parameter.

When you run the application now, you can change the "triangle color" property in the GUI and you will see that the color for all triangles change. To limit the change on only the one triangle we are interested in changing, you need to set the uniform for the other triangles, right before that draw call, so that their color only depends on the vertex colors.

labhelper::setUniformSlow(shaderProgram, "triangleColor", glm::vec3(1, 1, 1));

Color selection of the triangle

Click here to see solution (But try yourself first! You have to understand each step.)
---

When done, show your result to one of the assistants. Have the finished program running and be prepared to explain what you have done.