Rendering Triangle with OpenGL and hard work

Rendering Triangle with OpenGL and hard work

I am a sucker for good computer graphics. In the past, I have been using libraries like processing by the Processing Foundation while following tutorials by The Coding Train. Still, recently I have developed a keen interest in wanting to learn rendering objects on my screen from scratch.

One may ask, But why do such a thing when there are already so many good libraries like SDL2 or frameworks like p5.js to do creative coding to your heart's content? You may have different reasons for wanting to learn OpenGL but for me learning OpenGL comes downs to wanting to understand how to program the GPU. It helps you understand how the GPU stores and processes the data, and ultimately provide an actual image that you see on your screen.

Today we are going to see how to render a triangle which is the most basic shape you can render on your screen. You can think of it as the "Hello World" program of OpenGL.

OpenGL is an API( Application Programming Interface ), Every GPU has its implementation of OpenGL which comes with the drivers of the GPU. The implementation is based on the OpenGL specifications which define the different functions that the OpenGL API will have and what the outcomes of those functions will be. How those functions are implemented is not a concern of the specification.

💡
We will be using Modern OpenGL hence if you are here to learn to draw a triangle in legacy OpenGL then this is not the blog for you.

To Render a triangle we need to follow three steps:

  • Create OpenGL context and Window.
  • Generate and Bind an array buffer with our vertices data.
  • Create a Shader Program.

Creating an OpenGL Window:

As we talked about earlier that OpenGL functions live in our computer's GPU as .dll files but to make our lives easier and be able to use the code we write on multiple platforms we use GLEW ( OpenGL Extension Wrangler ). GLEW just makes using the functions a lot easier by providing function pointers to all the functions we need.

The second ingredient after GLEW that we will use to render a window is GLFW. GLFW is a library that helps you to manage windows and its different parameters like size, title, events, etc.

To get GLEW, visit this link.

For GLFW you can either get the pre-compiled binaries or compile them from the source. Both binaries and source code can be downloaded from here.

Once both GLEW and GLFW are linked and loaded we are going to use the starter template which can be found on the GLFW website to create a basic window.

#include <GLFW/glfw3.h>

int main(void)
{
    GLFWwindow* window;

    /* Initialize the library */
    if (!glfwInit())
        return -1;

    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT);

        /* Swap front and back buffers */
        glfwSwapBuffers(window);

        /* Poll for and process events */
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

The following code makes a window of 640 x 480 with the title "hello world". When you run this code you will get a blank black window as seen in the image below.

Now let us move on to initializing GLEW. First, we need to include the glew.h header file like this.

#include<GL/glew.h>
💡
glew.h should be included before glfw3.h otherwise we will have errors.

then  we initialize glew using glewInit();

GLenum err = glewInit();
    if (GLEW_OK != err) {
        std::cout << "We have a problem with glew init";
    }
💡
Make sure you call glewinit() after glfwMakeContextCurrent(window);

Now we have a window with glew initialized and we can move on to the next step.


Vertex Buffers And Vertex Attributes:

Moving on to the actual triangle now we need to provide our GPU with some data to work on. We do the same using Buffers. Buffer Objects are used to store an array of unformatted memory and are allocated in the GPU.

In our case, we will use a buffer to store the vertices data of our triangle.

First, let us declare an array of 3 vertices with each vertex having 2 position values.

 float positions[6] = {
        -0.5, -0.5,
         0.0,  0.5,
         0.5, -0.5
    };

each value is of type float and in total, we have 6 values.

Creating a buffer is a 3 step process.

  • Generate a buffer
  • Bind the buffer to manipulate it
  • Provide the data to the buffer

To generate the buffer we use glGenBuffers() this function takes 2 parameters first is the number of buffers we want to generate and the second is a pointer where the id for each buffer will be stored.

unsigned int buffer;
glGenBuffers(1, &buffer);

buffer is an unsigned integer here that stores the id to the buffer.

💡
You can learn more about the functions at docs.gl

We then bind our buffer and provide the data using the following two methods.

glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW);

Here GL_ARRAY_BUFFER is our type of buffer and 6*sizeof(float) refers to the size of our positions array.

GL_STATIC_DRAW is used to tell our GPU that this value will be set once and used often. Other types of data are Dynamic and Stream where the amount of times the data is changed and used varies.

Now that we have created and stored data in a buffer we need to tell our GPU what the data in the buffer means so that the GPU can act on the data accordingly.

For that, we need what we call a Vertex Attribute Pointer or VertexAttribPointer

💡
A vertex is not only positional data but also can consists of different attributes like color, normal, etc. positions being one of those attributes.
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), 0);

This tells that we have 2 floats for each vertex with each vertex having a size of 2*sizeof(float).

💡
It is highly recommended to read about this function here. Because a full explanation of the function in this blog will take too long.

Do not forget to activate the VertexAttribPointer using the following function.

glEnableVertexAttribArray(0);

Now our vertex data exists in the GPU and it knows how to access its different attributes in our case the two float values indicate the position of the vertex. Let's Move on to writing Shaders to tell our GPU what the vertices are.


Creating a Shader Program:

A very good indepth explaination for what shaders are in this context can be found in this article.

In short, Shader take inputs and transform them into outputs that we then use to draw the final image. A good example would be we can have a geometrical shape with two different shaders that change the look of the shape. One can make it opaque and rough while other makes it look transparent and smooth.

There are two types of shaders in each shader program

  • Vertex Shader
  • Fragment Shader

Vertex shader deals with manipulation of vertices which we can think of as corner points in a shape whereas Fragment shader handles how each pixel between two vertecies look like.

We need to create both these shaders and then compile and combile them into a single shader program.

A very Simple vertex and fragment shader program is as follows.

VertexShader:

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

FragmentShader:

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

Let us compile our first vertex shader

We need to pass the code for the vertex shader as a c style string so we use the following snippet.

const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";

Now we pass this to glCreateShader() that takes the type of shader as input and returns us the id to the vertex shader that we just created.

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

Then we provide our newly created shader with the source to the vertex shader and hit the compile button or should I say Call The Compile Function.

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

We are not doing error checking for now but it is recommened and we might tackel that in the future.

For now we move on to making the fragment shader same way as we compiled the vertex shader.

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

Now that we have both vertex and fragment shader we need to combine them into one program so that we can use that program to render our triangle.

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

First we create the program using the above code and then we attach both our shaders to the program using the below code.

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);

Everything is now setup and the final step is to let OpenGL know what we want rendered in the main render loop  which is the while loop that we haven't touched so far.

glDrawArrays(GL_TRIANGLES, 0, 3);

glDrawArrays() is the single command which we need to write in order to let OpenGL know that we want to render GL_TRIANGLES here the second argument tells that we start from 0 intex up to 3 count.

Did you get the Triangle on your screen?