Saturday, May 18, 2013

Modern OpenGL tutorial: Introduction to shader GLSL (OpenGL shader language)

Shader are compulsory on Modern OpenGL and most important. Shader are a little program they run in GPU i.e for OpenGL, shader run in rendering hardware. There are many shader language available but we use GLSL (OpenGL Shader Language) which is very similar to C but not C. For beginner, shader can be new things so just think it is a little program written in different language that run in  your graphics card. Without shader nothing can be displayed on the screen. Shader takes vertex data as an input, process it and return output value to display on the screen it is vertex processing shader which is called vertex shader. In this program we also use another shader called fragment shader which is used to output the color pixel of fragment. How shader takes input and vertex are processed you will understand later.
Lets start digging the code and I will explain related theory side by side in short so that you can learn quickly and start to write you own program.You can also download the complete source code of this tutorials.
In this whole tutorials series we are using four library:

  • GLFW for creating window, 
  • GLEW to acess OpenGL API function at run time , 
  • GLM for doing some math for us like rotation, translation etc and 
  • GLSL as shader language.
Download
All the code in this series of articles is available from github: https://github.com/smokindinesh/Modern-OpenGL-Series  You can download a zip of all the files from that page, or you can clone the repository if you are familiar with git.

Lets start explaining code from the funciton MainAppFunc() our program starts form here.Code for initializing GLFW and GLEW is given below.


// initialise GLFW
if(!glfwInit())
        throw std::runtime_error("glfwInit failed");

    // open a window with GLFW
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
    glfwWindowHint(GLFW_RESIZABLE, GL_TRUE);
    MainWindow = glfwCreateWindow((int)SCREEN_SIZE.x, (int)SCREEN_SIZE.y,"Intro OpenGL with Shader",NULL,NULL);
    if(!window)
        throw std::runtime_error("glfwOpenWindow failed. Can your hardware handle OpenGL 4.2?");

    // GLFW settings
    glfwMakeContextCurrent(MainWindow);
    // initialise GLEW
    glewExperimental = GL_TRUE; //stops glew crashing on OSX :-/
    if(glewInit() != GLEW_OK)
        throw std::runtime_error("glewInit failed");

    // print out some info about the graphics drivers
    std::cout << "OpenGL version: " << glGetString(GL_VERSION) << std::endl;
    std::cout << "GLSL version: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << std::endl;
    std::cout << "Vendor: " << glGetString(GL_VENDOR) << std::endl;
    std::cout << "Renderer: " << glGetString(GL_RENDERER) << std::endl;

    // make sure OpenGL version 4.2 API is available
    if(!GLEW_VERSION_4_2)
        throw std::runtime_error("OpenGL 4.2 API is not available.");

I am using OpenGL version 4.2 and GLSL version 4.2 so your hardware must support it. In this tutorials we are going to draw a simple triangle. Vertex data to draw traingle is given below.

// Put the three triangle verticies into the VBO
    GLfloat vertexData[] = {
        //  X     Y     Z
         0.0f, 0.8f, 0.0f,
        -0.8f,-0.8f, 0.0f,
         0.8f,-0.8f, 0.0f,
    };
This data is input for the vertex shader. We are using C++ program which compile at CPU and linking the shader program which run at GPU. Therefore, we have to transfer these vertex data form C++ program to GPU so that vertex shader takes this data as a input , process it and return vertex data as a output to render the triangle on screeen. VBO (Vertex Buffer Object) and VAO (Vertex Array Object) are used to transfer data form cpp program to GPU (graphics card).
Vertex Buffer Object (VBO):
It helps to upload the vertex, color data to the GPU memory. Since memory reside on GPU rather than system memory (CPU), it can be used directly to render the scene which increase the rendering performance. This is the most important feature of VBO. VBO are "buffer" of video  memory they just upload the bunch of bytes containing any binary data , it doesn't know what kind of data it has. In this tutorial we are just uploading three 3D point of triangle.
Vertex Array Object (VAO):
VBO doesn't know what type of data it has. VAO describe what type of data does VBO contain because VAO is connection between VBO and shader.
Coder for creating VBO and VAO


// make and bind the VAO
    glGenVertexArrays(1, &gVAO);
    glBindVertexArray(gVAO);

    // make and bind the VBO
    glGenBuffers(1, &gVBO);
    glBindBuffer(GL_ARRAY_BUFFER, gVBO);


    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);

    // unbind the VBO and VAO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

First four line of code just make and bind the VAO and VBO. Fifth line glBufferData upload the vertexData on the newly created VBO. Next line of code setup the VAO. glEnableVertexAttribArray(0) enable the variable in the shader program having index zero. Next glVertexAttribPointer describes what type of data VBO has. First argument "0" index to the shader variable, second argument "3" says each vertices has  3 number, third agrument GL_FLOAT says point are floats type, fourth argument GL_FLSE says points doesn't has to normalized.The firth argument,0, would be used if there was a gap in between each point. Setting this to zero means there are no gaps in our data , sixth argument , NULL, means that our data starts right at the first byte of the VBO.
Now its time to look our shader. We use two type of shader vertex shader for vertex processing and fragment shader for processing pixel color of fragment.
Vertex Shader:
#version 420 core
in vec3 vPosition;
void main()
{
gl_Position = vec4(vPosition,1.0);
}
At first we have to specify the version of the shader. I am using 4.2 version. The second line "in vec3 vPosition" is the input shader variable vec3 means have there vector x,y,z. This is the variable where the vertex data we upload using VBO is assign. Here is only one input variable so it has index 0. Now you may became clear that we upload the data from C++ program using VBO and that data is input for the shader. That input vertex data is process here return as outpur for rendering on screeen.The forth line "gl_Position" = vec4(vPosition,1.0)" means sending the output vertex  to the variable gl_Position. Here we input the vertex in variable vPosition and without doing anything we assign the output straight to gl_Position.The variable gl_Position is a global defined by OpenGL, and is used to store the output of the vertex shader. All vertex shaders must set teh gl_Position variable.Finally we have to understand that vertex shader is used to transform the point x,y and z into different point.
Fragment Shader: 
#version 420 core
out vec4 color;
void main()
{
color = vec4(1.0,1.0,1.0,1.0);
}
Second line "out vec4 color" is just the output fragment varible that hold the pixel color. Here we just output  color to the color variable  using line "color =vec4(1.0,1.0,1.0,1.0)" all RBGA value is 1 so color of the triangle is white.
Main purpose of the fragment shader is to calculate the color of the pixel that is drawn.
Compiling and linking the shader:
Code of the Function to load the shader is given below
GLuint loadShader(char *shaderFile, GLenum type)
{
    std::ifstream in(shaderFile);
    std::string src= "";
    std::string line="";
    while(std::getline(in,line))
    src += line + "\n";
    std::cout << src;
    GLuint shader;
    GLint compiled;
    shader = glCreateShader(type);

    const char* source = src.c_str();
    glShaderSource(shader,1,&source,NULL);
    glCompileShader(shader);
    if(!shader)
    {
        std::cerr << "Could not compile the shader";
        return 0;
    }
    return shader;
}
This function just load the GLSL shader code . We have two shader program vertex and fragment saved in file vertex.glsl and fragment.glsl.
Now code to creating program, linking shader and compile shader is given below

GLuint createShaderProgram()
{
    GLuint vertexShader,fragmentShader;
    GLint linked;

    vertexShader = loadShader("vertex.glsl",GL_VERTEX_SHADER);
    fragmentShader = loadShader("fragment.glsl",GL_FRAGMENT_SHADER);
    if(!vertexShader || !fragmentShader) return 0;

    programId=glCreateProgram();
    if(!programId)
    {
        std::cerr << "could not create the shader program";
        return 0;

    }
    glAttachShader(programId,vertexShader);
    glAttachShader(programId,fragmentShader);

    glBindAttribLocation(programId,0,"vPosition");
    glLinkProgram(programId);
    glGetProgramiv(programId,GL_LINK_STATUS,&linked);
    if(!linked)
    {
        std::cerr << "could not link the shader";
        return 0;
    }
    glUseProgram(programId);

 return programId;
}

Now code for rendering function is given below.

// draws a single frame
static void Render(GLFWwindow* MainWindow) {
    // clear everything
    glClearColor(0, 0, 0, 1); // black
    glClear(GL_COLOR_BUFFER_BIT);

    // bind the VAO (the triangle)
    glBindVertexArray(gVAO);

    // draw the VAO
    glDrawArrays(GL_TRIANGLES, 0, 3);

    // unbind the VAO
    glBindVertexArray(0);

    // swap the display buffers (displays what was just drawn)
    glfwSwapBuffers(MainWindow);
}
Output:
Next Chapter: Drawing Basic Shapes

No comments:

Post a Comment