Creating your first vertex and fragment shader using GLSL

Before we can render images using OpenGL, we need to first understand the basics of the GLSL. In particular, the concept of shader programs is essential in GLSL. Shaders are simply programs that run on graphics processors (GPUs), and a set of shaders is compiled and linked to form a program. This concept emerges as a result of the increasing complexity of various common processing tasks in modern graphics hardware, such as vertex and fragment processing, which necessitates greater programmability of specialized processors. Accordingly, the vertex and fragment shader are two important types of shaders that we will cover here, and they run on the vertex processor and fragment processor, respectively. A simplified diagram illustrating the overall processing pipeline is shown as follows:

Creating your first vertex and fragment shader using GLSL

The main purpose of the vertex shader is to perform the processing of a stream of vertex data. An important processing task involves the transformation of the position of each vertex from the 3D virtual space to a 2D coordinate for display on the screen. Vertex shaders can also manipulate the color and texture coordinates. Therefore, vertex shaders serve as an important component of the OpenGL pipeline to control movement, lighting, and color.

A fragment shader is primarily designed to compute the final color of an individual pixel (fragment). Oftentimes, we implement various image post-processing techniques, such as blurring or sharpening, at this stage; the end results are stored in the framebuffer, which will be displayed on screen.

For readers interested in understanding the rest of the pipeline, a detailed summary of these stages, such as the clipping, rasterization, and tessellation, can be found at https://www.opengl.org/wiki/Rendering_Pipeline_Overview. Additionally, a detailed documentation of GLSL can be found at https://www.opengl.org/registry/doc/GLSLangSpec.4.40.pdf.

Getting ready

At this point, we should have all the prerequisite libraries, such as GLEW, GLM, and SOIL. With GLFW configured for the OpenGL core profile, we are now ready to implement the first simple example code, which takes advantage of the modern OpenGL pipeline.

How to do it...

To keep the code simple, we will divide the program into two components: the main program (main.cpp) and shader programs (shader.cpp, shader.hpp, simple.vert, and simple.frag). The main program performs the essential tasks to set up the simple demo, while the shader programs perform the specialized processing in the modern OpenGL pipeline. The complete sample code can be found in the code_simple folder.

First, let's take a look at the shader programs. We will create two extremely simple vertex and fragment shader programs (specified inside the simple.vert and simple.frag files) that are compiled and loaded by the program at runtime.

For the simple.vert file, enter the following lines of code:

#version 150 
in vec3 position;
in vec3 color_in;
out vec3 color;
void main() {
  color = color_in;
  gl_Position = vec4(position, 1.0);
}

For the simple.frag file, enter the following lines of code:

#version 150 
in vec3 color;
out vec4 color_out;
void main() {
  color_out = vec4(Color, 1.0);
}

First, let's define a function to compile and load the shader programs (simple.frag and simple.vert) called LoadShaders inside shader.hpp:

#ifndef SHADER_HPP
#define SHADER_HPP
GLuint LoadShaders(const char * vertex_file_path,const char * fragment_file_path);
#endif

Next, we will create the shader.cpp file to implement the LoadShaders function and two helper functions to handle file I/O (readSourceFile) and the compilation of the shaders (CompileShader):

  1. Include prerequisite libraries and the shader.hpp header file:
    #include <iostream>
    #include <fstream>
    #include <algorithm>
    #include <vector>
    #include "shader.hpp"
  2. Implement the readSourceFile function as follows:
    std::string readSourceFile(const char *path){
      std::string code;
      std::ifstream file_stream(path, std::ios::in);
      if(file_stream.is_open()){
        std::string line = "";
        while(getline(file_stream, line))
        code += "
    " + line;
        file_stream.close();
        return code;
      }else{
        printf("Failed to open "%s".
    ", path);
        return "";
      }
    }
  3. Implement the CompileShader function as follows:
    void CompileShader(std::string program_code, GLuint shader_id){
      GLint result = GL_FALSE;
      int infolog_length;
      char const * program_code_pointer = program_code.c_str();
      glShaderSource(shader_id, 1, &program_code_pointer , NULL);
      glCompileShader(shader_id);
      //check the shader for successful compile
      glGetShaderiv(shader_id, GL_COMPILE_STATUS, &result);
      glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &infolog_length);
      if ( infolog_length > 0 ){
        std::vector<char> error_msg(infolog_length+1);
        glGetShaderInfoLog(shader_id, infolog_length, NULL, &error_msg[0]);
        printf("%s
    ", &error_msg[0]);
      }
    }
  4. Now, let's implement the LoadShaders function. First, create the shader ID and read the shader code from two files specified by vertex_file_path and fragment_file_path:
    GLuint LoadShaders(const char * vertex_file_path,const char * fragment_file_path){
      GLuint vertex_shader_id = glCreateShader(GL_VERTEX_SHADER);
      GLuint fragment_shader_id = glCreateShader(GL_FRAGMENT_SHADER);
      std::string vertex_shader_code = readSourceFile(vertex_file_path);
      if(vertex_shader_code == ""){
        return 0; 
      }
      std::string fragment_shader_code = readSourceFile(fragment_file_path);
      if(fragment_shader_code == ""){
        return 0; 
      }
  5. Compile the vertex shader and fragment shader programs:
      printf("Compiling Vertex shader : %s
    ", vertex_file_path);
      CompileShader(vertex_shader_code, vertex_shader_id);
      printf("Compiling Fragment shader : %s
    ",fragment_file_path);
      CompileShader(fragment_shader_code, fragment_shader_id);
  6. Link the programs together, check for errors, and clean up:
      GLint result = GL_FALSE;
      int infolog_length;
      printf("Linking program
    ");
      GLuint program_id = glCreateProgram();
      glAttachShader(program_id, vertex_shader_id);
      glAttachShader(program_id, fragment_shader_id);
      glLinkProgram(program_id);
      //check the program and ensure that the program is linked properly
      glGetProgramiv(program_id, GL_LINK_STATUS, &result);
      glGetProgramiv(program_id, GL_INFO_LOG_LENGTH, &infolog_length);
      if ( infolog_length > 0 ){
        std::vector<char> program_error_msg(infolog_length+1);
        glGetProgramInfoLog(program_id, infolog_length, NULL, &program_error_msg[0]);
        printf("%s
    ", &program_error_msg[0]);
      }else{
        printf("Linked Successfully
    ");
      }
    
      //flag for delete, and will free all memories
      //when the attached program is deleted
      glDeleteShader(vertex_shader_id);
      glDeleteShader(fragment_shader_id);
      return program_id;
    }

Finally, let's put everything together with the main.cpp file:

  1. Include prerequisite libraries and the shader program header file inside the common folder:
    #include <stdio.h>
    #include <stdlib.h>
    //GLFW and GLEW libraries
    #include <GL/glew.h>
    #include <GLFW/glfw3.h>
    #include "common/shader.hpp"
  2. Create a global variable for the GLFW window:
    //Global variables
    GLFWwindow* window;
  3. Start the main program with the initialization of the GLFW library:
    int main(int argc, char **argv)
    {
      //Initialize GLFW
      if(!glfwInit()){
        fprintf( stderr, "Failed to initialize GLFW
    " );
        exit(EXIT_FAILURE);
      }
  4. Set up the GLFW window:
      //enable anti-aliasing 4x with GLFW
      glfwWindowHint(GLFW_SAMPLES, 4);
      /* specify the client API version that the created context must be compatible with. */
      glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
      glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
      //make the GLFW forward compatible
      glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
      //use the OpenGL Core 
      glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  5. Create the GLFW window object and make the context of the specified window current on the calling thread:
      window = glfwCreateWindow(640, 480, "Chapter 4 - GLSL", NULL, NULL);
      if(!window){
        fprintf( stderr, "Failed to open GLFW window. If you have an Intel GPU, they are not 3.3 compatible. Try the 2.1 version of the tutorials.
    " );
        glfwTerminate();
        exit(EXIT_FAILURE);
      }
      glfwMakeContextCurrent(window);
      glfwSwapInterval(1);
  6. Initialize the GLEW library and include support for experimental drivers:
      glewExperimental = true; 
      if (glewInit() != GLEW_OK) {
        fprintf(stderr, "Final to Initialize GLEW
    ");
        glfwTerminate();
        exit(EXIT_FAILURE);
      }
  7. Set up the shader programs:
      GLuint program = LoadShaders("simple.vert", "simple.frag");
      glBindFragDataLocation(program, 0, "color_out");
      glUseProgram(program);
  8. Set up Vertex Buffer Object (and color buffer) and copy the vertex data to it:
      GLuint vertex_buffer;
      GLuint color_buffer;
      glGenBuffers(1, &vertex_buffer);
      glGenBuffers(1, &color_buffer);
      const GLfloat vertices[] = {
        -1.0f, -1.0f, 0.0f,
        1.0f, -1.0f, 0.0f,
        1.0f, 1.0f, 0.0f,
        -1.0f, -1.0f, 0.0f,
        1.0f, 1.0f, 0.0f,
        -1.0f, 1.0f, 0.0f
      };
      const GLfloat colors[]={
        0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f,
        1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f,
        1.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f
      };
    
      glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
      glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
      glBindBuffer(GL_ARRAY_BUFFER, color_buffer);
      glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
  9. Specify the layout of the vertex data:
      GLint position_attrib = glGetAttribLocation(program, "position");
      glEnableVertexAttribArray(position_attrib);
      glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
      glVertexAttribPointer(position_attrib, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
    
      GLint color_attrib = glGetAttribLocation(program, "color_in");
      glEnableVertexAttribArray(color_attrib);
      glBindBuffer(GL_ARRAY_BUFFER, color_buffer);
      glVertexAttribPointer(color_attrib, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
  10. Run the drawing program:
      while(!glfwWindowShouldClose(window)){
        // Clear the screen to black
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        // Draw a rectangle from the 2 triangles using 6 vertices
        glDrawArrays(GL_TRIANGLES, 0, 6);
        glfwSwapBuffers(window);
        glfwPollEvents();
      }
  11. Clean up and exit the program:
      //clean up the memories
      glDisableVertexAttribArray(position_attrib);
      glDisableVertexAttribArray(color_attrib);
      glDeleteBuffers(1, &vertex_buffer);
      glDeleteBuffers(1, &color_buffer);
      glDeleteVertexArrays(1, &vertex_array);
      glDeleteProgram(program);
      // Close OpenGL window and terminate GLFW
      glfwDestroyWindow(window);
      glfwTerminate();
      exit(EXIT_SUCCESS);
    }

Now we have created the first GLSL program by defining custom shaders:

How to do it...

How it works...

As there are multiple components in this implementation, we will highlight the key features inside each component separately, organized in the same order as the previous section using the same file name for simplicity.

Inside simple.vert, we defined a simple vertex shader. In the first simple implementation, the vertex shader simply passes information forward to the rest of the rendering pipeline. First, we need to define the GLSL version that corresponds to the OpenGL 3.2 support, which is 1.50 (#version 150). The vertex shader takes two parameters: the position of the vertex (in vec3 position) and the color (in vec3 color_in). Note that only the color is defined explicitly in an output variable (out vec3 color) as gl_Position is a built-in variable. In general, variable names with the prefix gl should not be used inside shader programs in OpenGL as these are reserved for built-in variables. Notice that the final position, gl_Position, is expressed in homogeneous coordinates.

Inside simple.frag, we defined the fragment shader, which again passes the color information forward to the output framebuffer. Notice that the final output (color_out) is expressed in the RGBA format, where A is the alpha value (transparency).

Next, in shader.cpp, we created a framework to compile and link shader programs. The workflow shares some similarity with conventional code compilation in C/C++. Briefly, there are six major steps:

  1. Create a shader object (glCreateShader).
  2. Read and set the shader source code (glShaderSource).
  3. Compile (glCompileShader).
  4. Create the final program ID (glCreateProgram).
  5. Attach a shader to the program ID (glAttachShader).
  6. Link everything together (glLinkProgram).

Finally, in main.cpp, we set up a demo to illustrate the use of the compiled shader program. As described in the Getting Started with Modern OpenGL section of this chapter, we need to use the glfwWindowHint function to properly create the GLFW window context in OpenGL 3.2. An interesting aspect to point out about this demo is that even though we defined only six vertices (three vertices for each of the two triangles drawn using the glDrawArrays function) and their corresponding colors, the final result is an interpolated color gradient.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.51.3