Using GLSL with SDL

I have written this tutorial because I found that enabling and using extensions of OpenGL with SDL, and specifically using GLSL can be a little bit tricky. The documents I could find only provided partial information, and there were some non-obvious details that gave me a few problems when putting it all together. Much of the extension information was learned from this page. As I do not currently own machine that runs Linux, or the Mac OS X, I cannot verify the correctness of their corresponding code. (Please drop me an e-mail if you've tried it.) Please note that this guide was written in 2006, and may be out of date.

Including the Correct Files

When using OpenGL extensions with SDL, it is of vital importance that you define NO_SDL_GLEXT before including SDL\SDL_opengl.h. Failure to do this will make most of the later extensions inaccessible by the methods described in this document.

After including the SDL OpenGL header, you will want to include some version of glext.h, which contains type definitions for the various available extensions. (That particular version of it was found VIA the OpenGL Technical FAQ.)

In addition to these two files, the methods for obtaining pointers to the extension functions are different for each operating system. Win32 programmers may use the function wglGetProcAddress, whereas under X-Windows it is apparently glXGetProcAddressARB. For Mac OS X, I am told that the function pointers are already set up by including the proper headers. To make the differences transparent, I have used a macro to reassign the different functions to something I called uglGetProcAddress. (Thomas Harte has suggested that alternatively an SDL function SDL_GL_GetProcAddress can be used.) Source:

#define NO_SDL_GLEXT
#include <SDL\SDL_opengl.h>
#include "glext.h"

bool shading_enabled = false;

#ifdef __WIN32__
  #define uglGetProcAddress(x) wglGetProcAddress(x)
  #define WIN32_OR_X11
#elifdef __APPLE__
  #include <OpenGL/glu.h>
  #include <OpenGL/glext.h>
  void setupExtensions()
  { shading_enabled = true; }; // OS X already has these extensions
#elifdef __X11__
  #include <GL/glx.h>
  #include <GL/glxext.h>
  #define uglGetProcAddress(x) (*glXGetProcAddressARB)((const GLubyte*)(x))
  #define WIN32_OR_X11
#else
  void setupExtensions()
  { shading_enabled = false; }; // just fail otherwise?
#endif

Acquiring the Function Pointers

Next we need to write a definition for each extension function that we wish to use, then use the uglGetProcAddress macro created earlier to acquire these function pointers. If using the Mac OS X, these definitions must be omitted.

Each available extension has an associated function type defined in glext.h. If the function is named glAbc, that function type is called PFNGLABCPROC, which is the function name as all uppercase, prefixed with PFN and appended with PROC. I reccomend that the initial value of each of these function pointers be set to NULL, as it may help prevent coding errors.

A string that is a list of OpenGL extensions available to you can be reached at run-time by the function glGetString(GL_EXTENSIONS). This returns a char* to a string of extension names, each separated by a space. The function written below, findString, executes a search of this string, returning true if it is found. This code was derived from the GameDev.net tutorial Moving Beyond OpenGL 1.1, and I will not describe it here (e-mail me if it's confusing).

Once we have searched this string for the extensions we need and determined that they are present, it is time to call uglGetProcAddress for each needed function. After accomplishing this, it is prudent to check each of them against NULL, in case of failure. If everything has come out alright, the extensions are now ready to use. Specifically for GLSL, we should have the extensions: GL_ARB_shader_objects, GL_ARB_shading_language_100, GL_ARB_vertex_shader, and GL_ARB_fragment_shader, which will confirm the existance of the functions we wish to obtain. Source:

#ifdef WIN32_OR_X11
PFNGLCREATEPROGRAMOBJECTARBPROC     glCreateProgramObjectARB = NULL;
PFNGLDELETEOBJECTARBPROC            glDeleteObjectARB = NULL;
PFNGLCREATESHADEROBJECTARBPROC      glCreateShaderObjectARB = NULL;
PFNGLSHADERSOURCEARBPROC            glShaderSourceARB = NULL;
PFNGLCOMPILESHADERARBPROC           glCompileShaderARB = NULL;
PFNGLGETOBJECTPARAMETERIVARBPROC    glGetObjectParameterivARB = NULL;
PFNGLATTACHOBJECTARBPROC            glAttachObjectARB = NULL;
PFNGLGETINFOLOGARBPROC              glGetInfoLogARB = NULL;
PFNGLLINKPROGRAMARBPROC             glLinkProgramARB = NULL;
PFNGLUSEPROGRAMOBJECTARBPROC        glUseProgramObjectARB = NULL;
PFNGLGETUNIFORMLOCATIONARBPROC      glGetUniformLocationARB = NULL;
PFNGLUNIFORM1FARBPROC               glUniform1fARB = NULL;
PFNGLUNIFORM1IARBPROC               glUniform1iARB = NULL;

#include <cstring>

static bool findString(char* in, char* list)
{
  int thisLength = strlen(in);
  while (*list != 0)
  {
    int length = strcspn(list," ");
    
    if( thisLength == length )
      if (!strncmp(in,list,length))
        return true;
        
    list += length + 1;
  }
  return false;
}

void setupExtensions()
{
  char* extensionList = (char*)glGetString(GL_EXTENSIONS);

  if( findString("GL_ARB_shader_objects",extensionList) &&
      findString("GL_ARB_shading_language_100",extensionList) &&
      findString("GL_ARB_vertex_shader",extensionList) &&
      findString("GL_ARB_fragment_shader",extensionList) )
  {
    glCreateProgramObjectARB = (PFNGLCREATEPROGRAMOBJECTARBPROC)
      uglGetProcAddress("glCreateProgramObjectARB");
    glDeleteObjectARB = (PFNGLDELETEOBJECTARBPROC)
      uglGetProcAddress("glDeleteObjectARB");
    glCreateShaderObjectARB = (PFNGLCREATESHADEROBJECTARBPROC)
     uglGetProcAddress("glCreateShaderObjectARB");
    glShaderSourceARB = (PFNGLSHADERSOURCEARBPROC)
      uglGetProcAddress("glShaderSourceARB");
    glCompileShaderARB = (PFNGLCOMPILESHADERARBPROC)
      uglGetProcAddress("glCompileShaderARB");
    glGetObjectParameterivARB = (PFNGLGETOBJECTPARAMETERIVARBPROC)
      uglGetProcAddress("glGetObjectParameterivARB");
    glAttachObjectARB = (PFNGLATTACHOBJECTARBPROC)
      uglGetProcAddress("glAttachObjectARB");
    glGetInfoLogARB = (PFNGLGETINFOLOGARBPROC)
      uglGetProcAddress("glGetInfoLogARB");
    glLinkProgramARB = (PFNGLLINKPROGRAMARBPROC)
      uglGetProcAddress("glLinkProgramARB");
    glUseProgramObjectARB = (PFNGLUSEPROGRAMOBJECTARBPROC)
      uglGetProcAddress("glUseProgramObjectARB");
    glGetUniformLocationARB = (PFNGLGETUNIFORMLOCATIONARBPROC)
      uglGetProcAddress("glGetUniformLocationARB");
    glUniform1fARB = (PFNGLUNIFORM1FARBPROC)
      uglGetProcAddress("glUniform1fARB");
    glUniform1iARB = (PFNGLUNIFORM1IARBPROC)
      uglGetProcAddress("glUniform1iARB");

    if( false
     || glActiveTextureARB == NULL
     || glMultiTexCoord2fARB == NULL
     || glCreateProgramObjectARB == NULL
     || glDeleteObjectARB == NULL
     || glCreateShaderObjectARB == NULL
     || glShaderSourceARB == NULL
     || glCompileShaderARB == NULL
     || glGetObjectParameterivARB == NULL
     || glAttachObjectARB == NULL
     || glGetInfoLogARB == NULL
     || glLinkProgramARB == NULL
     || glUseProgramObjectARB == NULL
     || glGetUniformLocationARB == NULL
     || glUniform1fARB == NULL
     || glUniform1iARB == NULL
      )
      shading_enabled = false;
    else shading_enabled = true;
  } else
    shading_enabled = false;
}
#endif

A Header File

I find it useful to keep the extension code above in a separate file, and provide access through a header file which gives access to a boolean value shading_enabled which keeps track of whether or not the OpenGL shading language extensions could be acquired, and a function setupExtensions which will attempt to set them up and update that boolean accordingly. Please note that setupExtensions should be called after creating an OpenGL context, which in SDL would be after any call to SDL_SetVideoMode. (This includes resizing the window.) If shading_enabled is ever set to false, you should not attempt to use the extension functions. Source:

#ifndef EXTENSIONS_H
#define EXTENSIONS_H

#define NO_SDL_GLEXT
#include <SDL\SDL_opengl.h>

extern bool shading_enabled;

extern void setupExtensions();

#ifdef __APPLE__
  #include <OpenGL/glu.h>
  #include <OpenGL/glext.h>
#else
  #include "glext.h"

  /* Shading language prototypes. */
  extern PFNGLCREATEPROGRAMOBJECTARBPROC     glCreateProgramObjectARB;
  extern PFNGLDELETEOBJECTARBPROC            glDeleteObjectARB;
  extern PFNGLCREATESHADEROBJECTARBPROC      glCreateShaderObjectARB;
  extern PFNGLSHADERSOURCEARBPROC            glShaderSourceARB;
  extern PFNGLCOMPILESHADERARBPROC           glCompileShaderARB;
  extern PFNGLGETOBJECTPARAMETERIVARBPROC    glGetObjectParameterivARB;
  extern PFNGLATTACHOBJECTARBPROC            glAttachObjectARB;
  extern PFNGLGETINFOLOGARBPROC              glGetInfoLogARB;
  extern PFNGLLINKPROGRAMARBPROC             glLinkProgramARB;
  extern PFNGLUSEPROGRAMOBJECTARBPROC        glUseProgramObjectARB;
  extern PFNGLGETUNIFORMLOCATIONARBPROC      glGetUniformLocationARB;
  extern PFNGLUNIFORM1FARBPROC               glUniform1fARB;
  extern PFNGLUNIFORM1IARBPROC               glUniform1iARB;
#endif

#endif

Using the GLSL Extensions

Before using the GLSL extensions, you must first have some sort of shader written in GLSL to pass to it. This is accomplished by writing the program in a character string, or loading it in from a text file, then passing it to the program. There is a good tutorial on the basics of GLSL at Lighthouse 3D.

The first step is to get some handles to objects in the OpenGL context that you will put your programs into. This is accomplished with glCreateProgramObjectARB and glCreateShaderObjectARB. You will need a shader object for both a vertex shader and a fragment shader. The vertex shader deals with the vertices sent between glBegin and glEnd and their transformation into the drawing space, whereas the fragment shader handles the pixel-by-pixel colouring. Finally you will need a program object that will combine these two into one functional unit.

GLenum shader_vert = 0;
GLenum shader_frag = 0;
GLenum shader_prog = 0;

shader_prog = glCreateProgramObjectARB();
shader_vert = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);
shader_frag = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);

Now that the shader objects have been created, you may pass your GLSL source code to them. glShaderSourceARB takes four parameters: the handle to your object, the number of strings you are passing (usually 1), a pointer to the array of these strings, and a pointer to an array of integers that is their length. (You may specify a 0 length and omit the terminating null character on the string.) In the following example, shader_vert_source and shader_frag_source are of type char* which point to their respective GLSL source code.

int len = 0;
len = strlen(shader_vert_source);
glShaderSourceARB(shader_vert, 1, (const GLcharARB**)&shader_vert_source, &len);
len = strlen(shader_frag_source);
glShaderSourceARB(shader_frag, 1, (const GLcharARB**)&shader_frag_source, &len);

Now that OpenGL knows where to find your source code, you can compile both shaders, attach them to the program, then link them all together. All of these steps must be accomplished before the shader becomes functional.

glCompileShaderARB(shader_vert);
glCompileShaderARB(shader_frag);
glAttachObjectARB(shader_prog, shader_vert);
glAttachObjectARB(shader_prog, shader_frag);
glLinkProgramARB(shader_prog);

There is, however, a possibility that either compiling step, or the linking stage will fail because of bad code. OpenGL provides a way to get any error messages generated by the compiler with the glGetInfoLogARB function. It takes four parameters: the object handle, the maximum buffer length to use, a pointer to an integer which it will update with the output string length, and a character buffer which it will fill with the error message. There will be no terminating 0 on the string it outputs (which is why it gives you the length). The length of this string will be 0 if there were no errors. The following is dirty little example of one call to this function that would take place after compiling the shader_vert object.

char infobuffer[1000];
int infobufferlen = 0;
glGetInfoLogARB(shader_vert, 999, &infobufferlen, infobuffer);
infobuffer[infobufferlen] = 0;

If the shaders compiled and linked without error, the program object will be ready to use. At some point in your program you may want to discard old shaders that you no longer need, which can be done easily with the glDeleteObjectARB function.

glDeleteObjectARB(shader_vert);
glDeleteObjectARB(shader_frag);
glDeleteObjectARB(shader_prog);
shader_bump_vert = 0;
shader_bump_frag = 0;
shader_bump_prog = 0;

Using the Shading Program

Once compiled and linked, the program is ready to use, and it is very easy to tell OpenGL to start using your program instead of its default shaders:

glUseProgramObjectARB(shader_prog);

This may be called at any time except between a glBegin and glEnd. The default shader has a handle of 0 and may be restored with the call:

glUseProgramObjectARB(0);

A Simple Multitexturing Shader

This example will show how to write a simple shader, but also how to use multitexturing and uniform variables. For this example, you must also enable the extension GL_ARB_multitexture which will have the functions glActiveTextureARB and glMultiTexCoord2fARB, among others. (I will leave it as an exercise for you to enable these extensions.) The following shader programs will apply the sum of two textures to an object. First, the vertex shader:

varying vec2 Tex_coord;

void main()
{
  gl_Position = ftransform();
  Tex_coord = vec2(gl_MultiTexCoord0);
}

This simple program updates gl_Position with the provided function ftransform which performs the standard OpenGL coordinate transformations. Also, it updates a custom varying piece of data called Tex_coord. Values assigned to a varying type will be linearly interpolated and passed later to the fragment shader. Here we pick up gl_MultiTexCoord0 which contains the texture coordinate given at that vertex, probably passed by glTexCoord2f or possibly glMultiTexCoord2fARB. For this example I am assuming both textures will use the same coordinates, but several sets of coordinates may be found by using the built in variables gl_MultiTexCoord1, gl_MultiTexCoord2, etc., and specifying them with glMultiTexCoord2fARB. After the vertex shader does its work, the value it assigned to gl_Position determines which pixel on the screen the fragment shader is going to work on. It will pick up the interpolated texture coordinate from Tex_coord and use it to look up the appropriate colours from the textures:

uniform sampler2D mytex1, mytex2;
varying vec2 Tex_coord;

void main()
{
  gl_FragColor = texture2D(mytex1, Tex_coord) + texture2D(mytex2, Tex_coord);
}

This shader uses the built in texture2D function to look up the texels required, but the uniform variables mytex1 and mytex2 must be set up before it will know which texture is to be used for each. A variable that is uniform is guaranteed not to change between calls to glBegin and glEnd, thus it is uniform over the object being rendered. Because a sampler2D variable may not be initialized by the shader itself, we must pass it a handle to the desired texture at runtime. This is accomplished by the two functions glGetUniformLocationARB and glUniform1iARB. (There are many different glUniform type functions, each of which uses a specified number of int or float inputs. The 1i version is suitable for the sampler2D type.)

After glUseProgramObjectARB has been called with your program handle (and not before), you may set its uniform variables in the following manner:

GLint uniform_location;
uniform_location = glGetUniformLocationARB(shader_prog, "mytex1");
glUniform1iARB(uniform_location, 0);

The preceding code tells OpenGL to set the mytex1 uniform to use texture unit 0. A similar process should be made to have mytex2 use texture unit 1. Note that there are two steps involved, first acquiring the memory location at which your data is stored with glGetUniformLocationARB, then setting it with glUniform1iARB which takes that location as a parameter. Again, I must remind you that glUseProgramObjectARB must have been called before you use these functions, as the same variable names may be reused by different program objects for different purposes; a call to glGetUniformLocationARB will find that variable only in the currently active program.

Finally, before drawing your object with the usual OpenGL code, you will need to set up the appropriate texture units with the multitexture functions. (I will assume that you already know how to load textures, and could produce the appropriate texture_handle_1 and texture_handle_2 variables for the following example.) To use two textures at once, you must select the texture units individually with glActiveTextureARB, bind the appropriate textures, and enable them:

glActiveTextureARB(GL_TEXTURE0_ARB);
glBindTexture(GL_TEXTURE_2D,texture_handle_1);
glEnable(GL_TEXTURE_2D);

glActiveTextureARB(GL_TEXTURE1_ARB);
glBindTexture(GL_TEXTURE_2D,texture_handle_2);
glEnable(GL_TEXTURE_2D);

Once this is done, you are free to draw your object. That's it for this tutorial! If you'd like a second take on it, you could try the NeHe tutorial Introduction to GLSL. E-mail me if you have any comments or questions.


Return to my Dragon Demo, or main page.
brad @ rainwarrior.ca
2007-11-25