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:
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.
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.
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
):
shader.hpp
header file:#include <iostream> #include <fstream> #include <algorithm> #include <vector> #include "shader.hpp"
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 ""; } }
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]); } }
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; }
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);
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:
#include <stdio.h> #include <stdlib.h> //GLFW and GLEW libraries #include <GL/glew.h> #include <GLFW/glfw3.h> #include "common/shader.hpp"
//Global variables GLFWwindow* window;
int main(int argc, char **argv) { //Initialize GLFW if(!glfwInit()){ fprintf( stderr, "Failed to initialize GLFW " ); exit(EXIT_FAILURE); }
//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);
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);
glewExperimental = true; if (glewInit() != GLEW_OK) { fprintf(stderr, "Final to Initialize GLEW "); glfwTerminate(); exit(EXIT_FAILURE); }
GLuint program = LoadShaders("simple.vert", "simple.frag"); glBindFragDataLocation(program, 0, "color_out"); glUseProgram(program);
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);
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);
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(); }
//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:
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:
glCreateShader
).glShaderSource
).glCompileShader
).glCreateProgram
).glAttachShader
).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.
3.16.51.3