Open In App

Rendering a Triangle using OpenGL(using Shaders)

Improve
Improve
Like Article
Like
Save
Share
Report

In this article we’ll see how to render a triangle using OpenGL. A triangle is probably the simplest shapes you can draw in OpenGL after points and lines and any complicated geometry that you make will me made up of number of triangles joined together.
We’ll be using the programmable pipeline, so we’ll be writing simple shader programs as well and compiling them and using them later for rendering. Now this will make our program a bit lengthy but the cool thing is we have do this only once and then reuse the code we’ve already written. This is actually true for most of the OpenGL code and hence we tend to write most of OpenGL codes in different functions and reuse them multiple times later.
The libraries that we’ll be using for this is Glew and Glut and Ç++ as our programming language.
Setting up the Environment 
 

  1. Download the latest glew and glut header, library and dll files.
  2. In Visual Studio create a Win 32 console application project.
  3. Go to your project properties.
  4. Import and include header and library for all configuration.
  5. Put the dll files where your source files are.

The complete Program(Explanation follows after the program) 
 

CPP




// CPP program to render a triangle using Shaders
#include <GL\freeglut.h>
#include <GL\glew.h>
#include <iostream>
#include <string>
 
std::string vertexShader = "#version 430\n"
                           "in vec3 pos;"
                           "void main() {"
                           "gl_Position = vec4(pos, 1);"
                           "}";
 
std::string fragmentShader = "#version 430\n"
                             "void main() {"
                             "gl_FragColor = vec4(1, 0, 0, 1);"
                             "}";
 
// Compile and create shader object and returns its id
GLuint compileShaders(std::string shader, GLenum type)
{
 
    const char* shaderCode = shader.c_str();
    GLuint shaderId = glCreateShader(type);
 
    if (shaderId == 0) { // Error: Cannot create shader object
        std::cout << "Error creating shaders";
        return 0;
    }
 
    // Attach source code to this object
    glShaderSource(shaderId, 1, &shaderCode, NULL);
    glCompileShader(shaderId); // compile the shader object
 
    GLint compileStatus;
 
    // check for compilation status
    glGetShaderiv(shaderId, GL_COMPILE_STATUS, &compileStatus);
 
    if (!compileStatus) { // If compilation was not successful
        int length;
        glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &length);
        char* cMessage = new char[length];
 
        // Get additional information
        glGetShaderInfoLog(shaderId, length, &length, cMessage);
        std::cout << "Cannot Compile Shader: " << cMessage;
        delete[] cMessage;
        glDeleteShader(shaderId);
        return 0;
    }
 
    return shaderId;
}
 
// Creates a program containing vertex and fragment shader
// links it and returns its ID
GLuint linkProgram(GLuint vertexShaderId, GLuint fragmentShaderId)
{
    GLuint programId = glCreateProgram(); // create a program
 
    if (programId == 0) {
        std::cout << "Error Creating Shader Program";
        return 0;
    }
 
    // Attach both the shaders to it
    glAttachShader(programId, vertexShaderId);
    glAttachShader(programId, fragmentShaderId);
 
    // Create executable of this program
    glLinkProgram(programId);
 
    GLint linkStatus;
 
    // Get the link status for this program
    glGetProgramiv(programId, GL_LINK_STATUS, &linkStatus);
 
    if (!linkStatus) { // If the linking failed
        std::cout << "Error Linking program";
        glDetachShader(programId, vertexShaderId);
        glDetachShader(programId, fragmentShaderId);
        glDeleteProgram(programId);
 
        return 0;
    }
 
    return programId;
}
 
// Load data in VBO and return the vbo's id
GLuint loadDataInBuffers()
{
    GLfloat vertices[] = { // vertex coordinates
                           -0.7, -0.7, 0,
                           0.7, -0.7, 0,
                           0, 0.7, 0
    };
 
    GLuint vboId;
 
    // allocate buffer space and pass data to it
    glGenBuffers(1, &vboId);
    glBindBuffer(GL_ARRAY_BUFFER, vboId);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
 
    // unbind the active buffer
    glBindBuffer(GL_ARRAY_BUFFER, 0);
 
    return vboId;
}
 
// Initialize and put everything together
void init()
{
    // clear the framebuffer each frame with black color
    glClearColor(0, 0, 0, 0);
 
    GLuint vboId = loadDataInBuffers();
 
    GLuint vShaderId = compileShaders(vertexShader, GL_VERTEX_SHADER);
    GLuint fShaderId = compileShaders(fragmentShader, GL_FRAGMENT_SHADER);
 
    GLuint programId = linkProgram(vShaderId, fShaderId);
 
    // Get the 'pos' variable location inside this program
    GLuint posAttributePosition = glGetAttribLocation(programId, "pos");
 
    GLuint vaoId;
    glGenVertexArrays(1, &vaoId); // Generate VAO
 
    // Bind it so that rest of vao operations affect this vao
    glBindVertexArray(vaoId);
 
    // buffer from which 'pos' will receive its data and the format of that data
    glBindBuffer(GL_ARRAY_BUFFER, vboId);
    glVertexAttribPointer(posAttributePosition, 3, GL_FLOAT, false, 0, 0);
 
    // Enable this attribute array linked to 'pos'
    glEnableVertexAttribArray(posAttributePosition);
 
    // Use this program for rendering.
    glUseProgram(programId);
}
 
// Function that does the drawing
// glut calls this function whenever it needs to redraw
void display()
{
    // clear the color buffer before each drawing
    glClear(GL_COLOR_BUFFER_BIT);
 
    // draw triangles starting from index 0 and
    // using 3 indices
    glDrawArrays(GL_TRIANGLES, 0, 3);
 
    // swap the buffers and hence show the buffers
    // content to the screen
    glutSwapBuffers();
}
 
// main function
// sets up window to which we'll draw
int main(int argc, char** argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
    glutInitWindowSize(500, 500);
    glutInitWindowPosition(100, 50);
    glutCreateWindow("Triangle Using OpenGL");
    glewInit();
    init();
    glutDisplayFunc(display);
    glutMainLoop();
    return 0;
}


Output 
Now when you run the program you should have a red triangle on the screen that should look something like the image below.
 

main() Function 
Main function sets up our window. We initialize glut, glew, specify windows size, its position on the screen, specify display mode(which just tells what color space will be used by the window and will it use single or double buffer), set windows title, set the callback method that glut will use to draw stuff on this window and finally make the window visible.
loadDataInBuffers() Function 
A triangle is made up of three coordinate points. These points(stored as floating point array) needs to be sent to graphics processor memory. 
The way we refer to a location in system memory is relatively easy, just get a pointer to the memory location but using your GPU’s memory is not that straightforward. 
We do this by something known as buffer objects
Buffer objects are the way you upload your data to the V-RAM and refer to it later. 
There are usually three steps that you need to follow to get your data into the video memory. 
 

  1. Generate a unsigned integer identifier for your buffer object.
  2. Bind that buffer object to a target.
  3. Specify the size of the buffer and optionally load data into it.

Remember that OpenGL expects vertex coordinates in the range of [-1, 1]. Anything outside this range is clipped. However, we are free to choose our own coordinate system in-fact, it is a very common practice to define our own coordinate system and define our objects according to this coordinate system and later change it to the OpengGL coordinate system.
But in this post we’ll travel down the easier road and specify our triangle’s coordinates in [-1, 1] range. FYI coordinates in these range are known as normalized device coordinate(NDC).
Shaders 
Throughout the rendering pipeline the input data(here vertex coordinates) go through various stages. We control these stages using something called Shaders 
Shaders are programs that execute on GPU. Shaders in OpenGL are written in a special language commonly known as GLSL(OpenGL shading language) which you will come to notice is very similar to C and C++. Shaders give you direct control over the graphics pipeline. GLSL was formally included into the OpenGL 2.0 core in 2004 by the OpenGL ARB.
The two shaders that you will need all the time are
 

  1. Vertex Shader: These shaders are executed once for every vertex(in case of a triangle it will execute 3 times) that the GPU processes. So if your scene consist of one million vertices, the vertex shader will execute one million times once for each vertex. The main job of a vertex shader is to calculate the final positions of the vertices in the scene. It does so by writing a vector(this vector has 4 component) to a special output variable known as gl_Position(only available in vertex shader). The input to the vertex shader is specified using an in type qualifier and output with an out qualifier before the variable. The input and output variable must have global scope.
  2. Fragment Shader: After your geometry has passes through other intermediate stages if finally reaches to the rasterizer. Rasterizer divides your geometry into fragments. Then the fragment shader runs for each fragment in your geometry(or triangle). The job of the fragment shader is to determine the final color for each fragment. It writes the final color to a special output variable gl_FragColor(only available to fragment shader). The value passed to the gl_FragColor is a vector containing rgba values for that fragment.

The shader code is stored as strings global variable. We’ll link this variable to the data in our openGL program later. The vertex position are passed to gl_Position without modification.
Note: It is a common practice to put your shader codes in different files and later store that files contents in string variables.
After you done writing your shader you need to create shader objects, attach their corresponding source code, compile them and check for errors if there are.
compileShaders() Function
This function compiles the shader code, checks for error and creates shader object and returns its id. 
We’ll call this function once for vertex and fragment shader
linkProgram() Function 
Shaders that you write for rendering needs to clubbed together into a shader program. 
This function clubs these shader objects into a shader program, links them(that sort of creates an executable file for that program) and check for errors that may occur in this process.
init() Function
It uses all the above functions and puts everything together. 
We use what is known as VAO(Vertex Array Objects)
VAO: A Vertex Array Object (VAO) is an OpenGL Object that stores all of the state needed to supply vertex data. It stores the format of the vertex data as well as the Buffer Objects providing the vertex data arrays. Note that VAOs do not copy, freeze or store the contents of the referenced buffers, if you change any of the data in the buffers referenced by an existing VAO, those changes will be seen by users of the VAO.
A shader program assigns to each of the input variable of vertex shader(aka vertex attributes) and uniform variables(variables whose values do not change for the primitive we are rendering) a position that we need to know if we want to link them to their data source.
display() Function 
Remember we haven’t yet told GPU to start rendering. display Function is what gives this command to the GPU. 
Glut calls this callback function whenever it needs to draw stuff to the screen.
 



Last Updated : 02 Jul, 2021
Like Article
Save Article
Previous
Next
Share your thoughts in the comments
Similar Reads