Shaders – The Basics

Last time we discussed the building blocks of CG life, the vertex and the pixel. This time we shall dive into something without which computer graphics would be stuck running on the CPU, slowing down your operating system, forever. I’m talking of course about the humble shader. More specifically, we’ll talk about the two most basic shaders: the vertex and fragment ones.

What are shaders?

You presumably know how simple C or C++ programs run on the CPU. Well, shaders are programs too, the difference being that they run on the GPU (Graphics Processing Unit). This frees up the CPU from the task of drawing hundreds, maybe thousands of polygons each frame, thus giving it the time to listen for pressed keys, running AI scripts, doing physics calculations (although with CUDA you can move all that stuff on the GPU too).

Shaders come in all shapes and sizes, so to speak. There are a few types, illustrated in the following image:

Shader pipeline from OpenGL Superbible 6th Ed.

Rendering pipeline from OpenGL Superbible 6th Ed.

As you can see, here, we are focusing on the beginning and end of the programmable pipeline, the vertex and fragment shaders. Vertex and fragment shaders are the most important ones because without them we can’t draw anything. In the pre-shaders days this functionality was fixed. Shaders allow us to customize how and when the pixels get coloerd. Tessellation and Geometry shaders are optionals and they can be skipped during the rendering process. Vertex fetch, Rasterization and Framebuffer operations can’t be directly programed. Before we move on, let’s talk about some basic interactions between shaders with the CPU or other shaders.

Between shaders, you can pass information through in/out variables. This operation has to follow certain rules for it to be successful:

  • The name and type of the in variable from the destination shader must be the same as its corresponding out variable from the sender shader
  • An out variable can be sent only to the next existing shader in the pipeline (for ex., you can’t send a variable directly from the vertex to the fragment shader if you have a tessellation shader between them)

From the CPU, shaders receive information through buffers, each value corresponding to one or multiple vertices (accessed only by the vertex bufer) and through uniform variables which, can be accessed by any shader directly.

Our shaders will be written in the GLSL, which is similar to C++, however there are other possibilities (HLSL, fx, etc.)

Reading and Loading Shaders

Before they can be used, you need to read the files where the shaders are kept, compile them and integrate them into your program. To do this, we need to create a C++ class which will deal with these operations and can be easily created and used within the main file without taking up a lot of lines.

First, create a folder called Core in your project. In this folder we are going to create our classes that are required for building our scene. The first class is Shader_Loader. To create a class you need a header file (Shader_Loader.h) and a code file (Shader_Loader.cpp).

This is the .h file:

#pragma once

#include "../Dependencies/glew/glew.h"
#include "../Dependencies/freeglut/freeglut.h"
#include <iostream>

namespace Core
{

 class Shader_Loader
{
      private:

          std::string ReadShader(char *filename);
          GLuint CreateShader(GLenum shaderType,
                              std::string source,
                              char* shaderName);

       public:

           Shader_Loader(void);
          ~Shader_Loader(void);
          GLuint CreateProgram(char* VertexShaderFilename,
                               char* FragmentShaderFilename);

 };
}

As you can see, the header file is used for defining the class structure and what it contains (methods, variables, constructors, etc.). It’s necessary to include the GLEW and FreeGLUT libraries, because we need the variable types GLuint and GLchar, not to mention important functions like glLinkProgram and glAttachShader. The GLuint variables will hold the shader and program handles (basically empty objects to hold these entities) which will be used for integration. OpenGL confusingly uses the name “program” – a program is a container that holds the shaders added to it (vertex, fragment, tessellation, geometry).

Shader_Loader is the class constructor (a function which is called when the class is instanced) and ~Shader_Loader is the destructor. ReadShader reads and returns the contents of a file. CreateShader method creates and compiles a shader (vertex or fragment). CreateProgram method uses ReadShader to extract the shader contents and  to create both shaders and load them into the program which is returned to be used in rendering loop.

Next up, here is the .cpp file:

#include "Shader_Loader.h" 
#include<iostream>
#include<fstream>
#include<vector>

using namespace Core;

Shader_Loader::Shader_Loader(void){}
Shader_Loader::~Shader_Loader(void){}

std::string Shader_Loader::ReadShader(char *filename)
{

    std::string shaderCode;
    std::ifstream file(filename, std::ios::in);

    if(!file.good())
    {
       std::cout<<"Can't read file "<<filename<<std::endl;
       std::terminate();
    }

   file.seekg(0, std::ios::end);
   shaderCode.resize((unsigned int)file.tellg());
   file.seekg(0, std::ios::beg);
   file.read(&shaderCode[0], shaderCode.size());
   file.close();
   return shaderCode;
}

GLuint Shader_Loader::CreateShader(GLenum shaderType, std::string
                                   source, char* shaderName)
{

     int compile_result = 0;

     GLuint shader = glCreateShader(shaderType);
     const char *shader_code_ptr = source.c_str();
     const int shader_code_size = source.size();

     glShaderSource(shader, 1, &shader_code_ptr, &shader_code_size);
     glCompileShader(shader);
     glGetShaderiv(shader, GL_COMPILE_STATUS, &compile_result);

     //check for errors
     if (compile_result == GL_FALSE)
     {

           int info_log_length = 0;
           glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &info_log_length);
           std::vector<char> shader_log(info_log_length);
           glGetShaderInfoLog(shader, info_log_length, NULL, &shader_log[0]);
           std::cout << "ERROR compiling shader: " << shaderName << std::endl << &shader_log[0] << std::endl;
           return 0;
     }
     return shader;
}

 GLuint Shader_Loader::CreateProgram(char* vertexShaderFilename,
                                     char* fragmentShaderFilename)
{

     //read the shader files and save the code
     std::string vertex_shader_code = ReadShader(vertexShaderFilename);
     std::string fragment_shader_code = ReadShader(fragmentShaderFilename);

   GLuint vertex_shader = CreateShader(GL_VERTEX_SHADER, vertex_shader_code, "vertex shader");
   GLuint fragment_shader = CreateShader(GL_FRAGMENT_SHADER, fragment_shader_code, "fragment shader");

    int link_result = 0;
    //create the program handle, attatch the shaders and link it
    GLuint program = glCreateProgram();
    glAttachShader(program, vertex_shader);
    glAttachShader(program, fragment_shader);

    glLinkProgram(program);
    glGetProgramiv(program, GL_LINK_STATUS, &link_result);
    //check for link errors
    if (link_result == GL_FALSE)
    {

       int info_log_length = 0;
       glGetProgramiv(program, GL_INFO_LOG_LENGTH, &info_log_length);
       std::vector<char> program_log(info_log_length);
       glGetProgramInfoLog(program, info_log_length, NULL, &program_log[0]);
       std::cout << "Shader Loader : LINK ERROR" << std::endl << &program_log[0] << std::endl;
       return 0;
     }
   return program;
 }

Of course we must import the header file and a couple of C++ libraries. For the time being, we can leave the constructor and destructor alone. As for ReadShader, it’s basically just reading a file.

CreateShader encapsulates all relevant operations required to create a shader. The following functions are called:

  • glCreateShader(shader_type) – it creates an empty shader object (handle) of the wanted type
  • glShaderSource(shader, count, shader_code, length) – it loads the shader object with the code; count is usually set to 1, because you normally have one character array, shader_code is the array of code and length is normally set to NULL (thus, it will read code from the array until it reaches NULL) , however here I set the actual length.
  • glCompileShader(shader) – compiles the code
  • glGetShaderiv(shader, GLenum ,GLint ) – Check for errors and output them to the console

You do this operation chain for any shaders you have, to create their objects.

Then in CreateProgram, you create the program through glCreateProgram() and attach the shaders to it with glAttatchShader(). Finally, you finish it off by linking the program with glLinkProgram(). We also need to check if the shader was linked properly using glGetProgramiv() and output possible errors to the console. Checking and catching shader and linking program errors is extremely important, helping you saving time and many headaches.

To use this program (these shaders) the following method needs to be added to the render loop when we begin to draw.

glUseProgram(program);

Vertex and Fragment Shaders

The first and most important of all shaders, they have the critical job of processing every vertex and returning its position on-screen through a series of transformations (we’ll get to them in a later tutorial). Along with the vertices, the vertex shader also receives arrays of data associated with each vertex (offset, normal, texture coordinate, etc.) in buffers. We won’t go into how this is done, because for the time being we don’t need it.

The only predefined variable you need to remember at this point when it comes to the vertex shader is gl_Position, in which the final on-screen position of the current vertex is saved (after the chain of transformations).

The fragment shader’s primary responsibility is determining the color of each fragment. The only output from this shader is the color vector, without which all objects would be colored completely black.

I haven’t given examples of these shaders because the main focus of this tutorial was to provide you with the necessary tools to add shaders to your program and to explain what they are. Starting with the next tutorial, you will learn how to actually write shaders and to draw a simple triangle.

So far, the project folder structure should look like this:

folders1_sha


Tagged under:

I'm an engineer, currently employed at a financial software company. My interests include gaming, LPing and, of course, reviewing, but also game dev and graphics. Also, in the past I've dabbled in amateur photography, reviewing movies and writing short stories and blog posts. I am also a huge Song of Ice and Fire fan, but that's beside the point. Youtube Channel, Deviantart , Google + , Twitter

blog comments powered by Disqus