An intro to modern OpenGL. Chapter 2.2: Shaders

« Chapter 2.1 | Table of Contents | Chapter 2.3 »

Buffers and textures contain the raw materials for an OpenGL program, but without shaders, they are inert lumps of bytes. If you recall from our overview of the graphics pipeline, rendering requires a vertex shader, which maps our vertices into screen space, and a fragment shader, which colors in the rasterized fragments of the resulting triangles. Shaders in OpenGL are written in a language called GLSL (GL Shading Language), which looks a lot like C. In this article, we'll lay out the shader code for our "hello world" program and then write the C code to load, compile, and link it into OpenGL.

The vertex shader

Here is the GLSL source code for our vertex shader, from hello-gl.v.glsl:

#version 110

attribute vec2 position;

varying vec2 texcoord;

void main()
{
    gl_Position = vec4(position, 0.0, 1.0);
    texcoord = position * vec2(0.5) + vec2(0.5);
}

I'll summarize what the shader does, then give a little more detail about the GLSL language. The shader first assigns the vertex's screen space position to gl_Position, a predefined variable that GLSL provides for the purpose. In screen space, the coordinates (–1, –1) and (1, 1) correspond respectively to the lower-left and upper-right corners of the framebuffer; since our vertex array's vertices already trace that same rectangle, we can directly copy the x and y components from each vertex's position value as it comes out of the vertex array. gl_Position's other two vector components are used in depth testing and perspective projection; we'll look at them closer next chapter when we get into 3d math. For now, we just fill them with their identity values zero and one. The shader then does some math to map our screen-space positions from screen space (–1 to 1) to texture space (0 to 1) and assigns the result to the vertex's texcoord.

Much like C, a GLSL shader starts executing from the main function, which in GLSL's case takes no arguments and returns void. GLSL borrows the C preprocessor syntax for its own directives. The #version directive indicates the GLSL version of the following source code; our #version declares that we're using GLSL 1.10. (GLSL versions are pretty tightly tied to OpenGL versions; 1.10 is the version that corresponds to OpenGL 2.0.) GLSL does away with pointers and most of C's sized numeric types, keeping only the bool, int, and float types in common, but it adds a suite of vector and matrix types up to four components in length. The vec2 and vec4 types you see here are two- and four-component vectors of floats, respectively. A type name can also be used as a constructor function for that type; you can construct a vector from either a single scalar value, which will be repeated into all the components of the vector, or from a combination of vectors and scalars, whose components will be strung together to form a larger vector. GLSL's math operators and many of its builtin functions are defined on these vector types to do component-wise math. In addition to numeric types, GLSL also supplies special sampler data types for sampling textures, which we'll see in the fragment shader below. These basic types can be aggregated into array and user-defined struct types.

A vertex shader communicates with the surrounding graphics pipeline using specially-declared global variables in the GLSL program. Its inputs come from uniform variables, which supply values from the uniform state, and attribute variables, which supply per-vertex attributes from the vertex array. The shader assigns its per-vertex outputs to varying variables. GLSL predefines some varying variables to receive special outputs used by the graphics pipeline, including the gl_Position variable we used here.

The fragment shader

Now let's look at the fragment shader source, from hello-gl.f.glsl:

#version 110

uniform float fade_factor;
uniform sampler2D textures[2];

varying vec2 texcoord;

void main()
{
    gl_FragColor = mix(
        texture2D(textures[0], texcoord),
        texture2D(textures[1], texcoord),
        fade_factor
    );
}

In a fragment shader, some things change slightly. varying variables become inputs here: Each varying variable in the fragment shader is linked to the vertex shader's varying variable of the same name, and each invocation of the fragment shader receives a rasterized version of the vertex shader's outputs for that varying variable. Fragment shaders are also given a different set of predefined gl_* variables. gl_FragColor is the most important, a vec4 to which the shader assigns the RGBA color value for the fragment. The fragment shader has access to the same set of uniforms as the vertex shader, but cannot declare or access attribute variables.

Our fragment shader uses GLSL's builtin texture2D function to sample the two textures from uniform state at texcoord. It then calls the builtin mix function to combine the two texture values based on the current value of the uniform fade_factor: zero gives only the sample from the first texture, one gives only the second texture's sample, and values in between give us a blend of the two.

Now that we've looked over the GLSL shader code, let's jump back into C and load the shaders into OpenGL.

Storing our shader objects

static struct {
    /* ... fields for buffer and texture objects */
    GLuint vertex_shader, fragment_shader, program;
    
    struct {
        GLint fade_factor;
        GLint textures[2];
    } uniforms;

    struct {
        GLint position;
    } attributes;

    GLfloat fade_factor;
} g_resources;

First, let's add some fields to our g_resources structure to hold the names of our shader objects and program object after we construct them. Like buffers and textures, shader and program objects are named by GLuint handles. We also add some fields to hold the integer locations that we'll need to reference our shaders' uniform and attribute variables. Finally, we add a field to hold the floating-point value we'll assign to the fade_factor uniform every frame.

Compiling shader objects

static GLuint make_shader(GLenum type, const char *filename)
{
    GLint length;
    GLchar *source = file_contents(filename, &length);
    GLuint shader;
    GLint shader_ok;

    if (!source)
        return 0;

OpenGL compiles shader objects from their GLSL source code and keeps the generated GPU machine code to itself. There is no standard way to precompile a GLSL program into a binary—you build the shader from source every time. Here we read our shader source out of a separate file, which lets us change the shader source without recompiling our C.

    shader = glCreateShader(type);
    glShaderSource(shader, 1, (const GLchar**)&source, &length);
    free(source);
    glCompileShader(shader);

Shader and program objects deviate from the glGen-and-glBind protocol that buffer and texture objects follow. Unlike buffer and texture functions, functions that operate on shaders and programs take the object's integer name directly as an argument. The objects don't need to be bound to any target to be modified. Here, we create a shader object by calling glCreateShader with the shader type (either GL_VERTEX_SHADER or GL_FRAGMENT_SHADER). We then supply an array of one or more pointers to strings of source code to glShaderSource, and tell OpenGL to compile the shader with glCompileShader. This step is analogous to the compilation stage of a C build process; a compiled shader object is akin to a .o or .obj file. Just as in a C project, any number of vertex and fragment shader objects can be linked together into a working program, with each shader object referencing functions defined in the others of the same type, as long as the referenced functions all resolve and a main entry point is provided for both the vertex and fragment shaders.

    glGetShaderiv(shader, GL_COMPILE_STATUS, &shader_ok);
    if (!shader_ok) {
        fprintf(stderr, "Failed to compile %s:\n", filename);
        show_info_log(shader, glGetShaderiv, glGetShaderInfoLog);
        glDeleteShader(shader);
        return 0;
    }
    return shader;
}

Also just like a C program, a block of shader source code can fail to compile due to syntax errors, references to nonexistent functions, or type mismatches. OpenGL maintains an info log for every shader object that contains errors or warnings raised by the GLSL compiler. After compiling the shader, we need to check its GL_COMPILE_STATUS with glGetShaderiv. If the compile fails, we display the info log using our show_info_log function and give up. Here's how show_info_log looks:

static void show_info_log(
    GLuint object,
    PFNGLGETSHADERIVPROC glGet__iv,
    PFNGLGETSHADERINFOLOGPROC glGet__InfoLog
)
{
    GLint log_length;
    char *log;

    glGet__iv(object, GL_INFO_LOG_LENGTH, &log_length);
    log = malloc(log_length);
    glGet__InfoLog(object, log_length, NULL, log);
    fprintf(stderr, "%s", log);
    free(log);
}

We pass in the glGetShaderiv and glGetShaderInfoLog functions as arguments to show_info_log so we can reuse the function for program objects further on. (Those PFNGL* function pointer type names are provided by GLEW.) We use glGetShaderiv with the GL_INFO_LOG_LENGTH parameter to get the length of the info log, allocate a buffer to hold it, and download the contents using glGetShaderInfoLog.

Linking program objects

static GLuint make_program(GLuint vertex_shader, GLuint fragment_shader)
{
    GLint program_ok;

    GLuint program = glCreateProgram();
    glAttachShader(program, vertex_shader);
    glAttachShader(program, fragment_shader);
    glLinkProgram(program);

If shader objects are the object files of the GLSL build process, then program objects are the finished executables. We create a program object using glCreateProgram, attach shader objects to be linked into it with glAttachShader, and set off the link process with glLinkProgram.

    glGetProgramiv(program, GL_LINK_STATUS, &program_ok);
    if (!program_ok) {
        fprintf(stderr, "Failed to link shader program:\n");
        show_info_log(program, glGetProgramiv, glGetProgramInfoLog);
        glDeleteProgram(program);
        return 0;
    }
    return program;
}

Of course, linking can also fail, due to functions being referenced but not defined, missing mains, fragment shaders using varying inputs not supplied by the vertex shader, and other reasons analogous to the reasons C programs fail to link. We check the program's GL_LINK_STATUS and dump its info log using show_info_log, this time using the program-specific glGetProgramiv and glGetProgramInfoLog functions.

Now we can fill in the last part of make_resources that compiles and links our shader program:

static int make_resources(void)
{
    /* make buffers and textures ... */
    g_resources.vertex_shader = make_shader(
        GL_VERTEX_SHADER,
        "hello-gl.v.glsl"
    );
    if (g_resources.vertex_shader == 0)
        return 0;

    g_resources.fragment_shader = make_shader(
        GL_FRAGMENT_SHADER,
        "hello-gl.f.glsl"
    );
    if (g_resources.fragment_shader == 0)
        return 0;

    g_resources.program = make_program(
        g_resources.vertex_shader,
        g_resources.fragment_shader
    );
    if (g_resources.program == 0)
        return 0;

Looking up shader variable locations

    g_resources.uniforms.fade_factor
        = glGetUniformLocation(g_resources.program, "fade_factor");
    g_resources.uniforms.textures[0]
        = glGetUniformLocation(g_resources.program, "textures[0]");
    g_resources.uniforms.textures[1]
        = glGetUniformLocation(g_resources.program, "textures[1]");

    g_resources.attributes.position
        = glGetAttribLocation(g_resources.program, "position");

    return 1;
}

The GLSL linker assigns a GLint location to every uniform value and vertex shader attribute. Structs and arrays of uniforms or attributes get further broken down, with each field getting its own location assigned. When we render using the program, we'll need to use these integer locations when we assign values to the uniform variables and when we map parts of the vertex array to attributes. Here, we use the functions glGetUniformLocation and glGetAttribLocation to look up these locations, giving them the variable, struct field, or array element name as a string. We then record those locations in our program's g_resources struct. With the program linked and the uniform and attribute locations on record, we are now ready to render using the program.

Next time, we render

I know I've left you hanging these last couple parts without a complete, working program to run. I'll fix that in the next and final part of this chapter, when we write the code that will actually set the graphics pipeline in motion and render our scene.

« Chapter 2.1 | Table of Contents | Chapter 2.3 »