2. Basic Shapes in OpenGL#

In this lab we will be creating our first graphics application in OpenGL.

If all has gone to plan you should be looking at a boring window with a grey background shown in Fig. 2.1. Familiarise yourself with the source files. For now, this contains the main C++ program Lab02_Basic_shapes.cpp in the source/ folder, the header file shader.hpp in the headers/ folder and associated code file shader.cpp in the source/ folder.

../_images/02_hello_window.png

Fig. 2.1 The “hello window” example (boring isn’t it)#

You can terminate your application by pressing the escape key or simply closing the window.


2.1. Define a triangle#

As you will probably agree, creating a plain grey window isn’t the most interesting of applications. What would make it much more exciting is to draw simple shapes in the window. The simplest shape, and one which we use extensively in computer graphics, is a triangle. We are going to draw the triangle from Fig. 2.2.

../_images/02_opengl_window.svg

Fig. 2.2 The vertices of our triangle.#

OpenGL expects the \(x\), \(y\) and \(z\) co-ordinates of all vertices to be between \(-1\) and \(1\) where the \(x\) and \(y\) axes point to the right and up respectively and the \(z\)-axis points out from the screen. For now we are going to draw a triangle with vertex co-ordinates \((-0.5,-0.5,0)\), \((0.5,-0.5,0)\) and \((0,0.5,0)\) for the bottom-left, bottom-right and top vertices respectively.

The first change we are going to make to our program is to define an array containing the triangle vertices. Enter the following code into the Lab02_Basic_shapes.cpp file after the window has been created.

// Define vertices
const float vertices[] = {
    // x     y     z
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

2.1.1. Vertex Array Object (VAO)#

Now that we have the co-ordinates we need to send these to the GPU. OpenGL does this using a Vertex Array Object (VAO) which is a container for the vertex attributes and buffer objects that contain the data for the vertices. To create a VAO enter the following into your Lab02_Basic_shapes.cpp file after the vertices array.

// Create the Vertex Array Object (VAO)
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

The glGenVertexArrays() function generates a VAO and glBindVertexArray() binds it.

2.1.2. Vertex Buffer Object (VBO)#

We now need to define a Vertex Buffer Object (VBO) which is used to store the vertex co-ordinates. Enter the following after we’ve created the VAO.

// Create Vertex Buffer Object (VBO)
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

Here, after creating and binding the vertex buffer we bind the VBO using glBindBuffer() and copy the data from the vertices array using the glBufferData() function.


2.2. Shaders#

Now we have defined our triangle vertices and created the VAO and VBO we now need to tell OpenGL how to display the triangle. This is done using a shader program that OpenGL uses to tell it how to display each pixel in our window. The shader programs are written in GLSL (OpenGL Shader Language) which is a language similar to C.

../_images/02_shaders.svg

A basic shader program consists of two separate programs: a vertex shader and a fragment shader. The vertex shader is called by OpenGL once for each vertex and calculates the position of the current vertex and stores it in a special GLSL vector called gl_Position.

The gl_Position values are passed to the rasteriser which determines the fragments that forms the shape defined by the vertices. The fragment shader is called once for each fragment and is used to determine the colour of the fragment that is sent to the display.

The shaders are compiled by the application at runtime, we need to write the vertex and fragment shaders and tell OpenGL which shaders we want to use.

2.2.1. Vertex shader#

Open the file vertexShader.glsl in the Lab02_Basic_shapes project in the project explorer. At the moment this is a blank file so enter the following program in this.

#version 330 core

layout(location = 0) in vec3 position;

out vec3 Colour;

void main()
{
    // Output vertex position
    gl_Position = vec4(position, 1.0);
}

This is the GLSL program for a simple vertex shader. It takes in a single 3-element vector position that contains the \((x,y,z)\) co-ordinates of a vertex and outputs the 4-element vector gl_Position containing the these co-ordinates. Note that the individual elements of a vector in GLSL can be accessed using vector.x, vector.y and vector.z so we could have used the following instead.

gl_Position = vec4(position.x, position.y, position.z, 1.0)

You may be wondering why gl_Position is a 4-element vector with an additional 1 and not a 3-element vector, don’t worry about this for now it will be explained later on.

2.2.2. Fragment shader#

Open the file fragmentShader.glsl from the project explorer and enter the following program.

#version 330 core

out vec3 colour;

void main()
{
    colour = vec3(1.0f, 0.0f, 0.0f); // RGB
}

This fragment shader outputs a single 3-element vector called colour which defines the colour of the fragment using the RGB colour model. Each colour in the visible spectrum can be defined using a combination of the three primary colours, red, green and blue. The amount of each of the primary colours is given by a value in range 0 to 1. Here we have defined the colour vector using red = 1, blue = 0, green = 0 so our fragment (and all fragments in the triangle) will be rendered in red.

2.2.3. Shader program#

We now need to combine the vertex and fragment shaders into a single shader program. To do this we are going use the function LoadShaders() written by contributors of opengl-tutorial.org. In the Lab02_Basic_shapes.cpp file enter the following after you have created the VBO.

// Compile shader program
unsigned int shaderID;
shaderID = LoadShaders("vertexShader.glsl", "fragmentShader.glsl");

This code creates a program object which will be referred to by the integer shaderID.


2.3. Draw the triangle#

Now that we have created the VAO and VBO and written the shaders we can now draw the triangle. The commands used to render a frame are contained in a while loop known as a render loop. This loop will continue until the window is closed or the escape key is pressed.

After clearing the window using glClear(), we need to instruct OpenGL to use our shader program, enter the following code

// Use the shader program
glUseProgram(shaderID);

Now we need to bind the VBO to the VAO and tell OpenGL where to find this data. Add the following code after we use the shader program.

// Send the VBO to the shaders
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0,         // attribute
                      3,         // size
                      GL_FLOAT,  // type
                      GL_FALSE,  // normalise?
                      0,         // stride
                      (void*)0); // offset

The three functions we’ve used here are:

  • glEnableVertexAttribArray() enables a generic vertex array so we can pass our triangle data to OpenGL;

  • glBindBuffer() binds our VBO to OpenGL;

  • glVertexAttribPointer() tells OpenGL how to interpret the data we are sending it. The input arguments are explained below.

Argument

Explanation

Attribute

A number that defines which vertex attribute we want to configure. In the vertex shader we used location = 0 for the vertex co-ordinates and since we are passing vertex co-ordinates, we set the attribute to 0.

Size

How many values does the vertex attribute have. Here we have (x,y,z) co-ordinates so this is 3.

Type

Our co-ordinates are floats.

Normalise

We have already set out vertex co-ordinates in NDC (i.e., in the range -1 to 1) so we set this to false.

Stride

The space between consecutive vertex attributes. Here one vertex immediately follows the next, so this is zero.

Offset

Where does the first data point appear in the buffer? For us this is at the beginning, so we set it to 0.

Now we instruct OpenGL to draw the triangle, add the following code.

// Draw the triangle
glDrawArrays(GL_TRIANGLES, 0, 3);

glDisableVertexAttribArray(0);

The glDrawArrays() command tells OpenGL to draw whatever data is defined in the VAO. The first argument GL_TRIANGLE tells OpenGL that we want to draw a triangle, the second argument 0 specifies that the first vertex starts at the 0 index in the buffer and the third argument 3 specifies that we have 3 vertices.

Once we have drawn the triangle we no longer need the VBO so we disable it using glDisableVertexAttribArray(0) where the 0 is the attribute number (the one used in the glEnableAttribArray() function).

Don’t get too excited just yet. As good programmers we should clean up after ourselves and not leave bits of data lying around. After the close of the do/while loop we de-allocate the vertex and buffer objects as well as deleting the shader program.

// Cleanup
glDeleteBuffers(1, &VBO);
glDeleteVertexArrays(1, &VAO);
glDeleteProgram(shaderID);

Compile and run your program. After all the syntax errors and bugs have been resolved (unless you are very lucky there will be at least one) you should be presented with a window within which is your red triangle that you have created.

../_images/02_hello_triangle.png

Fig. 2.3 The “hello triangle” example#


2.4. More colours#

After basking in the glory of your achievements for a few minutes the initial excitement may begin to wane, and your natural curiosity will cause you to wonder whether we can use more than one colour. Well of course, all we need to do is tell OpenGL what colours we want to use for each vertex.

Create an array that contains RGB colour data for each vertex.

// Define vertex colours
const float colours[] = {
    // R   G     B
    1.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f,
    0.0f, 0.0f, 1.0f
};

Here we have assigned the colour red to the first (bottom-left) vertex, green to the second (bottom-right) vertex and blue to the third (top) vertex. Like the vertex buffer, we need to create and bind a buffer for the colours.

// Create colour buffer
unsigned int colourBuffer;
glGenBuffers(1, &colourBuffer);
glBindBuffer(GL_ARRAY_BUFFER, colourBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(colours), colours, GL_STATIC_DRAW);

And where we draw the triangle, we also need to bind the colour information to the VAO so it can be sent to the shaders.

// Send the colour buffer to the shaders
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, colourBuffer);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);

You will notice that this code is very similar to the code used to send the VBO to the shader. Here we have use the attribute 1 for the colour buffer.

If you were to compile and run your program, you might be a little disappointed as your triangle is still red. Well of course, we haven’t told our shaders how to handle colours! Since our colours are associated with the vertices we need to modify the vertex shader to include the colours.

#version 330 core

layout(location = 0) in vec3 position;
layout(location = 1) in vec3 colour;

out vec3 fragmentColour;

void main()
{
    // Output vertex position
    gl_Position = vec4(position.x, position.y, position.z, 1.0);
    
    // Output vertex colour
    fragmentColour = colour;
}

Here our vertex shader is a little more sophisticated than before. We’ve added another attribute with location = 1 (the same attribute number used in the glVertexAttribPointer() function) for the colour data which is a 3-element vector. Also, since we need to pass the colour data to the fragment shader we need to output it from the vertex shader. We don’t need to do this for he vertex data as gl_Position is outputted automatically.

We also need to modify the fragment shader to take in the fragment colour calculated by the rasteriser and output it to the display.

#version 330 core

in vec3 fragmentColour;

out vec3 colour;

void main()
{
    colour = fragmentColour;
}

Compile and run your program and if everything has gone to plan you should be presented with your new triangle in all its colourful snazzy goodness. Notice how the pixels in between the three vertex pixels have been shaded a colour which are combinations of the three vertex colours red, green and blue. OpenGL has interpolated the colours across the triangle.

../_images/02_hello_snazzy_triangle.png

Fig. 2.4 Our snazzy triangle.#


2.5. Adding another triangle#

What could be better than one triangle? Well two triangles of course. Fortunately since we have done all of the grunt work in setting up the buffers for a single triangle adding another is a simple matter of defining the vertex co-ordinates and vertex colours for the additional triangle. Modify the vertices and colours arrays to the following.

// Define vertices
static const float vertices[] = {
    -0.9f, -0.5f, 0.0f,   // triangle 1
    -0.1f, -0.5f, 0.0f,
    -0.5f,  0.5f, 0.0f,
     0.1f, -0.5f, 0.0f,   // triangle 2
     0.9f, -0.5f, 0.0f,
     0.5f,  0.5f, 0.0f
};

// Define vertex colours
static const float colours[] = {
    1.0f, 0.0f, 0.0f,    // triangle 1 (red)
    1.0f, 0.0f, 0.0f,
    1.0f, 0.0f, 0.0f,
    0.0f, 0.0f, 1.0f,    // triangle 2 (blue)
    0.0f, 0.0f, 1.0f,
    0.0f, 0.0f, 1.0f,
};

Here the vertices array now defines six vertices for two triangles placed side-by-side. The colours array defines the first three vertices red and the second three set of vertices blue.

We also need to instruct OpenGL to draw two triangles instead of one. To do this we change the number of vertices we want to draw from 3 to the number of vertices we have. Since each vertex has 3 co-ordinates \((x, y, z)\) and each co-ordinate is a single float then we can calculate the number of vertices we have by dividing sizeof(vertices) by 3 * sizeof(float).

// Draw the triangles
glDrawArrays(GL_TRIANGLES, 0, sizeof(vertices) / (3 * sizeof(float)));

Compiling and running the executable results in the following.

../_images/02_two_triangles.png

Fig. 2.5 Two triangles#


2.6. Exercises#

Now that you’ve got to the stage where you can draw triangles to the screen and alter the colours lets see if you can do the following.

  1. Draw the original triangle but alter the vertex shader to achieve the following results:

   (a) the triangle is shifted by 0.5 to the right;

../_images/02_Ex1a.png

   (b) the triangle is drawn upside-down;

../_images/02_Ex1b.png

   (c) the triangle \(x\) and \(y\) co-ordinates are swapped.

../_images/02_Ex1c.png
  1. Use two triangles to draw a green rectangle where the lower-left corner has co-ordinates \((-0.5, -0.5, 0.0)\) and the upper-right corner has co-ordinates \((0.5, 0.5, 0.0)\).

../_images/02_Ex2.png
  1. Draw the Umbrella Corporation logo using 8 triangles.

../_images/02_Ex3.png