1. GLSL Setup

1. GLSL Setup tutorial illustration

Introduction to GLSL Setup

GLSL (OpenGL Shading Language) is a high-level shading language used to program the GPU for advanced graphical effects. Shaders allow you to customise how your graphics are rendered, providing immense flexibility and control over the rendering pipeline.

In this tutorial, we’ll dive into setting up GLSL shaders in OpenGL, covering the process of creating, compiling, and linking shaders. We’ll also introduce a reusable shader class to streamline shader management for future projects. By the end of this tutorial, you’ll understand how shaders fit into OpenGL and how to integrate them into your programs.

What are Shaders?

In traditional fixed-function OpenGL, the rendering process followed a predefined pipeline where you had limited control over rendering steps. With GLSL, you can replace programmable parts of that pipeline with your own code in the form of shaders.

Shaders are small programs that are executed by the graphics pipeline on the GPU. They are written in GLSL and come in different types. The three you are most likely to bump into while learning are:

  • Vertex Shaders: These process each vertex of your geometry. You can manipulate positions and prepare values such as normals, colours and texture coordinates for later stages.
  • Fragment Shaders: These calculate the output for generated fragments – commonly thought of as the pixels being drawn – allowing effects such as custom lighting, texturing and blending calculations.
  • Geometry Shaders: These are optional and operate on whole primitives after the vertex stage. They can emit additional geometry, although tessellation itself is handled by separate tessellation shader stages in newer OpenGL.

For now we only need a vertex shader and a fragment shader. No point throwing every shader stage at you before we have even managed to draw the teapot!

How Shaders Work in OpenGL

Shaders are written as GLSL source code, but we cannot just hand OpenGL a text file and hope for the best. There are a few steps between our source file and a shader program we can actually use:

  1. Source Code Loading: We load the GLSL source text into memory.
  2. Shader Creation: We create an OpenGL shader object for each stage we need.
  3. Compilation: We give each shader object its source and ask the OpenGL driver to compile it. The driver’s internal representation is implementation-dependent, so it is better not to think of this as one guaranteed “GPU bytecode” format.
  4. Linking: We attach the successfully compiled shaders to a program object and link them together.
  5. Binding: We activate the linked program with glUseProgram, and draw calls then use that program until we bind another one (or bind 0 in this compatibility example).

There is a bit of plumbing there, which is exactly why I do not want to inline all of this into every tutorial. We are going to put the boring shader-management work into one reusable class and then keep using it as the tutorials get more interesting.

Setting Up a Shader Class

To manage shaders effectively, we’ll create a reusable Shader class. This class handles loading, compiling, linking, and activating shaders, abstracting the setup so the rest of our rendering code does not turn into a giant pile of shader boilerplate.

Let’s start by defining its interface in a header file:

#ifndef __SHADER_H
#define __SHADER_H

#if defined(__MACH__) && defined(__APPLE__)
#include <OpenGL/gl.h>
#else
#include <GL/glew.h>
#include <GL/gl.h>
#endif

class Shader {
public:
    Shader();
    Shader(const char *vsFile, const char *fsFile);
    ~Shader();

    void init(const char *vsFile, const char *fsFile);
    void bind();
    void unbind();
    unsigned int id();

private:
    unsigned int shader_id;
    unsigned int shader_vp;
    unsigned int shader_fp;
};

#endif

What are we storing?

  • shader_vp: The OpenGL object for our vertex shader.
  • shader_fp: The OpenGL object for our fragment shader.
  • shader_id: The final linked shader program containing both stages.
  • init: Loads, compiles and links the shaders.
  • bind / unbind: Activates or deactivates the shader program for rendering.

Implementing the Shader Class

Here’s the implementation of the Shader class. I have included the file-loading helper here as well so the example is actually self-contained rather than mysteriously calling a textFileRead function that appeared out of thin air.

We are deliberately keeping error reporting simple in this first tutorial. In the next tutorial we will add proper compile/link status checks and print the shader logs, because sooner or later you will miss a semicolon in a shader and OpenGL will make you earn it.

#include "shader.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

static std::string textFileRead(const char *fileName) {
    std::ifstream file(fileName, std::ios::in);

    if (!file) {
        return std::string();
    }

    std::ostringstream contents;
    contents << file.rdbuf();
    return contents.str();
}

Shader::Shader()
    : shader_id(0), shader_vp(0), shader_fp(0) {
}

Shader::Shader(const char *vsFile, const char *fsFile)
    : shader_id(0), shader_vp(0), shader_fp(0) {
    init(vsFile, fsFile);
}

void Shader::init(const char *vsFile, const char *fsFile) {
    std::string vsText = textFileRead(vsFile);
    std::string fsText = textFileRead(fsFile);

    if (vsText.empty() || fsText.empty()) {
        std::cerr << "Either vertex shader or fragment shader file was not found." << std::endl;
        return;
    }

    const GLchar *vsSource = vsText.c_str();
    const GLchar *fsSource = fsText.c_str();

    shader_vp = glCreateShader(GL_VERTEX_SHADER);
    shader_fp = glCreateShader(GL_FRAGMENT_SHADER);

    glShaderSource(shader_vp, 1, &vsSource, NULL);
    glShaderSource(shader_fp, 1, &fsSource, NULL);

    glCompileShader(shader_vp);
    glCompileShader(shader_fp);

    shader_id = glCreateProgram();
    glAttachShader(shader_id, shader_vp);
    glAttachShader(shader_id, shader_fp);
    glLinkProgram(shader_id);
}

void Shader::bind() {
    glUseProgram(shader_id);
}

void Shader::unbind() {
    glUseProgram(0);
}

unsigned int Shader::id() {
    return shader_id;
}

Shader::~Shader() {
    if (shader_id != 0) {
        if (shader_vp != 0) glDetachShader(shader_id, shader_vp);
        if (shader_fp != 0) glDetachShader(shader_id, shader_fp);
    }

    if (shader_vp != 0) glDeleteShader(shader_vp);
    if (shader_fp != 0) glDeleteShader(shader_fp);
    if (shader_id != 0) glDeleteProgram(shader_id);
}

What is happening under the hood?

  • glCreateShader: Creates an OpenGL shader object for a particular stage.
  • glShaderSource: Associates our GLSL source text with that shader object.
  • glCompileShader: Asks the driver to compile the shader.
  • glCreateProgram: Creates the program object that will hold our shader stages together.
  • glAttachShader: Attaches each compiled shader object to the program.
  • glLinkProgram: Links the attached shader stages into the executable program we bind while drawing.

So our Shader object is not the shader itself in some magical C++ sense. It is simply a nice wrapper around the OpenGL object IDs and the steps we would otherwise have to repeat everywhere.

Main Program

The following program demonstrates how to use the Shader class to render a spinning teapot using GLSL.

#if defined(__MACH__) && defined(__APPLE__)
#include <OpenGL/gl.h>
#include <GLUT/glut.h>
#else
#include <GL/glew.h>
#include <GL/gl.h>
#include <GL/glut.h>
#endif

#include "shader.h"

Shader shader;
GLfloat angle = 0.0f;

void init() {
    glEnable(GL_DEPTH_TEST);
    shader.init("shader.vert", "shader.frag");
}

void display() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    gluLookAt(0.0, 0.0, 5.0,
    0.0, 0.0, 0.0,
    0.0, 1.0, 0.0);

    shader.bind();
    glRotatef(angle, 1.0f, 1.0f, 0.0f);
    glutWireTeapot(1.0);
    shader.unbind();

    glutSwapBuffers();
    angle += 1.0f;
}

void reshape(int w, int h) {
    if (h == 0) h = 1;
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60.0, (GLfloat)w / (GLfloat)h, 1.0, 100.0);
    glMatrixMode(GL_MODELVIEW);
}

int main(int argc, char **argv) {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
    glutInitWindowSize(800, 600);
    glutCreateWindow("GLSL Setup Tutorial");

#if !(defined(__MACH__) && defined(__APPLE__))
    glewInit();
#endif

    init();

    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutIdleFunc(display);

    glutMainLoop();
    return 0;
}

Shader Source Code

Our shaders are deliberately boring for the moment. That is a good thing – right now we only want to prove that the whole create / compile / link / bind path actually works before we start doing anything clever.

Vertex Shader (shader.vert)

void main() {
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

Fragment Shader (shader.frag)

void main() {
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

Source & downloads

Questions or corrections? If something doesn’t work or you spot something I’ve got wrong, leave a comment below. These tutorials have been around a long time, so useful corrections are always welcome!

  • March 25, 2010
  • 64