From Zero to Triangle : [Part 2]

Pratyush SoniPratyush Soni
6 min read

Now as we know how to create a window, we are pretty much ready to get our very first triangle on the screen.

If you missed my last blog where I discussed about how to bring a window on screen using the GLFW library here is the link.

So, let's continue with the triangle!

To make the GPU render the triangle we must write code to make it understand what we are trying to do. These types of code are known as shaders.

We will be mainly taking a look at 2 types of shaders:

  • Vertex Shader: For rendering vertices of the triangle.

  • Fragment Shader: For rendering or filling the area encapsulated by the vertices with coloured pixels.

These two shaders work together to render the object on the screen. We will use GLSL (OpenGL Shading Language) to write these shaders.


Creating Shaders Source Code:

Vertex Shader

This is the part where we will define how our program will interpret the vertex information that we are going to pass into it.

In this case we are going to have a vertex with co-ordinates in X,Y,Z axis.

Code:

  • ๐Ÿ”ด
    #version 330 core

    Here we are in the first line defining the version of the GLSL we are using.

  • ๐Ÿ”ด
    layout (location = 0) in vec3 aPos;

    Here we are telling that the aPos is vec3 type vector (i.e., it holds 3 values) and it resides in the location 0 of the vector component.

  • ๐Ÿ”ด
    void main(){ gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); }

    Here gl_position is a pre-defined vector where we are going to pass on the vertex aPos vectors values. 3 arguments passed are the X,Y and Z co-ordinate and 4th argument is the perspective which is not so relevant for the basics.

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";

Once we are done with defining our vertex shader, we will continue with our fragment shader.

Fragment Shaders

Fragment shaders tell the GPU to fill the encapsulated area with pixel.

Code:

  • ๐Ÿ”ด
    out vec4 FragColor;

    In GLSL we use out keyword to declare the output values and in CG we use RGBA format for the colour values.

  • ๐Ÿ”ด
    FragColor = vec4(0.69f, 0.282f, 0.71f, 1.0f);

    This is the colour output that we desire (Orchid Purple, one of my favourite colours).

const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
"   FragColor = vec4(0.69f, 0.282f, 0.71f, 1.0f);\n"
"}\n\0";

Compiling Shaders:

After creating the shaders, we must link and compile them.

Code:

  • ๐Ÿ”ด
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader);

    Here we are creating vertex shader using glCreateShader(GL_VERTEX_SHADER)
    Now we pass the source code in the shader.

  • ๐Ÿ”ด
    int success; char infolog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infolog); cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infolog << endl; return -1; }

    As a fail safe we use this system that returns the compile status of the shader by glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); and gets the error log by glGetShaderInfoLog(vertexShader, 512, NULL, infolog);.

unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); 
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

int success;
char infolog[512];

glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
    glGetShaderInfoLog(vertexShader, 512, NULL, infolog);
    cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infolog << endl;
    return -1;
}

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

glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success) {
    glGetShaderInfoLog(fragmentShader, 512, NULL, infolog);
    cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infolog << endl;
    return -1;
}

We can see in this code the process of compilation in Fragment shader and Vertex shader is the same.


Creating Shader Program:

The vertex shader and the fragment shader won't work individually, they must get linked together in a shader program.

Code:

  • ๐Ÿ”ด
    unsigned shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram);

    As usual we create shader program by glCreateProgram() and attached both the shaders to it using glAttachShader(). After that we link both the shaders to the program using gLinkProgram().

  • ๐Ÿ”ด
    glDeleteShader(vertexShader); glDeleteShader(fragmentShader);

    Once shaders are linked to the shader program, we don't need them anymore as we will work with the program itself so we can delete it using glDeleteShader().

unsigned shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);

if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infolog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infolog << std::endl;
    }

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

Creating Vertex Array and Buffer Objects:

When we need to perform tasks on GPU, we need to first send the data to it from the CPU which is a comparatively slow process. To tackle that we send the data in bulk of data to GPU and store it there if the storage is available.

For that we make a VBO (Vertex Buffer Object) that can hold all the vertex data we need to send to the GPU.

the rules to read the data in GPU is stored in VAO (Vertex Array Object).

Code:

  • ๐Ÿ”ด
    unsigned int VBO, VAO; glGenVertexArrays(1,&VAO); glGenBuffers(1, &VBO);

    By this segment we create the object and generate the id for the buffer object.

  • ๐Ÿ”ด
    glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO);

    By this segment we are binding the VAO to the vertex attribute calls and VBO to GL_ARRAY_BUFFER so whenever we reference these calls the Data is stored in VBO and the attribute is stored in the VAO.

  • ๐Ÿ”ด
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    By this we are storing the data in the VBO. First argument is used to access the VBO, second argument is specifying the size of the data we are going to store,
    third argument is actual array containing the data and last argument is to tell how to manage the given data.

  • ๐Ÿ”ด
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 sizeof(float), (void)0);

    Here we are passing the rule to read the data given in the data passed to VBO, such as how many values we are passing and how to handle the data.

  • ๐Ÿ”ด
    glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);

    After modifying VAO and VBO we can unbind it as we don't have to edit it anymore.

    unsigned int VBO, VAO;
    glGenVertexArrays(1,&VAO);
    glGenBuffers(1, &VBO);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);

    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

Finally the Triangle:

If you made it this far now there is only a few lines of code left to have a sweet sweet triangle on the screen.

We need to add the following lines of code in the main while loop of the program.

Code:

glUseProgram(shaderProgram);
glBindVertexArray(VAO); 
glDrawArrays(GL_TRIANGLES, 0, 3);

Finally, our work is done. However, we can perform some cleaning like freeing the memory.

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

Voila, you will get a triangle on screen like this one.

See how good it looks!!

Source code

By making this triangle I learned following things:

  • How OpenGL graphic pipeline works.

  • How it treats its objects.

  • The role of different object and how the "context" works.

It was a fun experience learning and then explaining it through this blog. Hopefully you find this helpful. With this the "From Zero to Triangle" series ends.

0
Subscribe to my newsletter

Read articles from Pratyush Soni directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pratyush Soni
Pratyush Soni

Game Developer | Programmer | Artist