USMGLSL Shaders
About GLSL

What is GLSL?
by David Cornette
Computer Science Department
University of Southern Maine
Faculty Advisor: Dr. Bruce MacLeod


The OpenGL Shading Language, or GLSL, is a computer programming language used to instruct the graphics card precisely how to draw objects on the computer screen.  It allows the programmer far greater control over the way three dimensional objects are displayed than was previously possible using OpenGL, one of the leading 3D programming APIs.

There are two types of GLSL programs, and they work together.  The first is the vertex shader.  An object in a computer graphics scene is usually a mesh made up of polygons.  The corner of each of those polygons is called a vertex.  The vertex shader runs for every vertex in the mesh.  It receives input in the form of per-vertex variables called attribute variables, and per-polygon variables called uniform variables.  It must specify the coordinates of the vertex in question.  Thus, the geometry of the object can be altered.  Here is a simple vertex shader.

varying vec3 normal;
varying vec4 pos;
varying vec4 rawpos;

void main() {
  normal = gl_NormalMatrix * gl_Normal;
  gl_Position = ftransform();
  pos = gl_ModelViewMatrix * gl_Vertex;
  rawpos = gl_Vertex;
  gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
}

The vertex shader can also set other variables which are called varying variables.  These values are passed on to the second kind of shader, the fragment shader.  The fragment shader is run for every pixel on the screen where the polygons of the mesh appear.  The fragment shader is responsible for setting the final color of that little piece of the mesh.

Here is an example of a fragment shader.  This one uses the Lambertian lighting model for an object which is a single solid color.  A lighting model deals with how light reflects off or transmits through a material, and in what direction it does so.  The Lambertian lighting model is one of the oldest and simplest lighting models.

varying vec3 normal;
varying vec4 pos;

void main() {
  vec4 color = gl_FrontMaterial.diffuse;
  vec4 matspec = gl_FrontMaterial.specular;
  float shininess = gl_FrontMaterial.shininess;
  vec4 lightspec = gl_LightSource[0].specular;
  vec4 lpos = gl_LightSource[0].position;
  vec4 s = -normalize(pos-lpos);

  vec3 light = s.xyz;
  vec3 n = normalize(normal);
  vec3 r = -reflect(light, n);
  r = normalize(r);
  vec3 v = -pos.xyz;
  v = normalize(v);
   
  vec4 diffuse  = color * max(0.0, dot(n, s.xyz)) *             gl_LightSource[0].diffuse;
  vec4 specular;
  if (shininess != 0.0) {
    specular = lightspec * matspec * pow(max(0.0,                 dot(r, v)), shininess);
  } else {
    specular = vec4(0.0, 0.0, 0.0, 0.0);
  }

  gl_FragColor = diffuse + specular;
}

Here is what the resulting image looks like.

A teapot drawn with the shader given here.
There are several pitfalls that one may encounter when programming GLSL.  Some of these issues are due to limitations inherent in programming on a GPU, and others are due to the newness of the technology.

The first issue is that there is no debugger for programs that run on a GPU.  Furthermore, it is not possible to print out error messages.  This makes it very difficult to debug shaders.  One workaround to deal with this is to test for a condition you suspect to be true with an if statement, and if the condition is true, set the output color to something recognizable, like solid red.

A second pitfall to be aware of is that sometimes a variable may have an "undefined" value.  Here is an example:

float x = pow(y, 2.0);
if (x >= 0.0 || x < 0.0) {
  // Set the fragment to be pure blue
  gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
} else {
  // Set the fragment to be pure red
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

You would excpect the condition in the if statement to be true, and thus you would expect the fragment to be blue.  However, if y is negative, the result of the call to pow() is undefined, so x is neither greater than, equal to, nor less than 0.0, so the fragment will actally be colored red.

Another issue a GLSL programmer needs to be aware of is that, since GLSL is so new, and it has not yet been widely used, there may be some bugs in the compiler.  In general, the GLSL compiler will be provided by the graphics card vendor.  I found two issues with the NVIDIA implementation.  The first of these is related to arrays.  According to the GLSL specification, the compiler must be able to determine the size of an array at compile time.  That is, when the array is declared, the length must be specified as a literal or as a number calculated entirely from literals.  The specification makes no mention of any such limitation on array subscripts when the array is accessed.  However, I found that I was unable to access an array if the array subscript was dependent on values that are only known at runtime.  I was able to work around this limitation, but, if you look at the spline function that I wrote, you can see that the code is awkward and much harder to read than it would be otherwise.  Rather than writing code like this:

varying int x
float y[2];
// ...
float z = y[x];

the code must be written like this:

varying int x
float y[2];
// ...
float z;
if (x == 0) {
  z = y[0];
} else if (x == 1) {
  z = y[1];
}


Another compiler bug I found was that it is not possible to return from a function in an if clause and put code after that if statement.  The rest of the code must be placed in an else clause.  That is, code such as this:

float f(int x) {
  if (x == 0) {
    return 0.0;
  }
  float y = 2.0 * x;
  return y;
}

must be written like this:

float f(int x) {
  if (x == 0) {
    return 0.0;
  } else {
    float y = 2.0 * x;
    return y;
  }
}