CS 381 Fall 2012  >  Lecture Notes for Tuesday, October 9, 2012

CS 381 Fall 2012
Lecture Notes for Tuesday, October 9, 2012

Communicating with Shaders

Introduction

Recall that GLSL has three qualifiers that deal with communicating data to shaders:

Image not displayed: illustration of GLSL qualifiers attribute, uniform, varying

We now look at how each of these is handled in a program.

Using varying Variables

The varying qualifier is the easy one. Simply declare a global variable using the varying qualifier, in both the vertex and fragment shaders, having the same type and name in each.

[GLSL vertex shader]

varying vec3 objpos;

[GLSL fragment shader]

varying vec3 objpos;

Set the variable in the vertex shader, and read it in the fragment shader.

Note that a position in space, communicated as a varying, should be a vec3, not a vec4. Convert to and from homogeneous form as previously discussed.

Sending Values to uniform Variables

In the vertex or fragment shader (or both), declare a global uniform variable.

[GLSL]

uniform float abc;

In the application, we write a value to a uniform variable in two steps.

We can get the variable’s location after successfully compiling and linking the shaders (i.e., after the call to makeProgramObjectFromFiles or makeProgramObject). Pass the program object and a string holding the variable’s name, to glGetUniformLocationARB. The string must be a (char *); if s is a C++ std::string object, then use s.c_str(). The return value of glGetUniformLocationARB is the location, or –1 if there is no uniform variable with that name. You may not pass 0 for the program object; this results in an OpenGL error.

[C++]

GLint loc = glGetUniformLocationARB(prog, "abc");
if (loc == -1)
{
    // uniform abc does not exist
}

Note: The above call should only be made when prog is the currently active program object, that is, the one that glUseProgramObjectARB was most recently called with. It is not clear to me whether the GLSL standard requires this (or if it allows glUseProgramObjectARB to be called on any valid, linked program object); however, certainly some implementations require it.

Having obtained the location of a variable, we write data to it using glUniform*ARB. This takes the location and then as many parameters as are required to make up the value. Some examples:

[C++]

int n;
glUniform1iARB(loca, n);           // Write to (uniform) int
// The 2nd parameter is really GLint, not int, but type
// conversions take care of that.

float f;
glUniform1fARB(locb, f);           // Write to float

float f1, f2, f3;
glUniform3fARB(locc, f1, f2, f3);  // Write to vec3

GLfloat ff[3];
glUniform3fvARB(locc, ff);         // Alternate write to vec3
// When we use arrays & pointers, we must get the types right.
// Do not use an array of float above!

bool b;
glUniform1iARB(locd, b);           // Use "1i" for bool

Note again that variable ff above must be a (GLfloat *). There are implicit type conversion for numeric types, but not for pointers.

Sending Values to attribute Variables

Handling attribute values is much like handling uniform values. The differences are as follows.

Application useshaders.cpp has been modified to send uniform values to shaders. Furthermore, the shaders in dabble_shaders.zip, intended for use with this application, now make use of both varying and uniform values.

Basic Lighting

Preliminaries

Introduction

An illumination model is a mathematical model of the way light affects the appearance of a surface. (Terminology in the field is not entirely standardized; one also sees “lighting model”, “reflection model”, and “shading model”.)

To allow for fast animation of realistic-looking scenes, we need an illumination model that mimics the real world at least somewhat, and that is also capable of being computed quickly.

Normals

A normal vector for a surface (often simply a normal) is a vector that is perpendicular to the surface. We want our normals to have length 1. We also generally make them point toward the outside of an object.
Image not displayed: vertex on a surface, with normal vector

In OpenGL we specify the normal vector at a vertex, using the glNormal* command. Be sure to do this before the associated glVertex*. In a GLSL vertex shader, the normal is available in the predefined variable gl_Normal, which is an attribute vec3.

We need to know how to compute normal vectors. We will cover this later. For now, we will mostly use GLUT routines that draw objects with normals included (glutSolidTorus, glutSolidTeapot, etc.). We may also draw objects with obvious normals, like a square in the \(x\),\(y\)-plane, whose normal vector is \(\langle 0,0,1 \rangle\).

We also need to know how to transform a normal vector, based on a given model/view matrix. We store a normal as a 3-vector (vec3 in GLSL). To find the proper transformation matrix, we begin with the \(3\times 3\) matrix consisting of all but the right-hand column and bottom row of the model/view matrix. Then we find the transpose of the inverse of this matrix. The result is the normal matrix.

In a GLSL shader, we do not need to compute the normal matrix ourselves. It is in the predefined variable gl_NormalMatrix, which is a uniform mat3.

Application useshaders.cpp has been modified so that normal vectors are specified for the square, using glNormal*.

Drawing a Smooth Surface

When we draw a lit surface, the obvious thing to do is to compute a normal for each facet (i.e., polygon making up a surface) and use these for lighting computations. Using facet normals in this way is known as flat shading. This per-facet lighting results in an angular, gem-like appearance, unless we use a very large number of very small polygons.
Image not displayed: facets making up a surface

A cheaper way to render a smooth-looking surface is to compute a normal at each vertex, then do lighting computations at each vertex (per-vertex lighting), to find the vertex colors. Lastly, interpolate colors between vertices and along scan lines, to find fragment colors. Using vertex normals in this way is smooth shading. (A special case of this, in which vertex normals are computed by averaging the facet normals of facets incident with a vertex, is called Gouraud shading, after Henri Gouraud [1971].)

Another method is to compute vertex normals, but instead of interpolating colors, we interpolate normals, and then do the lighting computations at the fragment level (per-fragment lighting). Such interpolation is called Phong interpolation, after Bui Tuong Phong [1973]. (Using this interpolation method, together with an illumination model that Phong developed, is called Phong Shading.)

Storing Vector Values

Normals—In GLSL, we store a normal vector in a vec3, and we generally make sure it has length 1 (use the built-in function normalize).

Points/vertices—Store a position in space as a vec3 unless you are

In these cases, store as a vec4 in homogeneous form. We have discussed how to convert to and from homogeneous form.

Colors—Store a color as a vec4. The fourth component is alpha, which we have not used. Set this to 1.0, if you have no reason to make it any other value.

Lambertian Illumination Model

Suppose that, for some point on a surface, we know a normal vector and a vector pointing at the light source. If the light source is above the horizon, then the intensity of light falling on the surface is proportional to the cosine of the angle between the two vectors. This cosine is the Lambert cosine, after Johann Heinrich Lambert [1760].
Image not displayed: illustration of the Lambert cosine

Suppose that the apparent color & brightness of a surface do not change when we view it from different directions; such a surface is a Lambertian surface. The brightness of a Lambertian surface is proportional to the Lambert cosine, as long as the light source is above the horizon. The corresponding illumination model is the Lambertian Illumination Model.

If the two vectors have unit length, then the cosine of the angle between them is just their dot product. Thus, we generally normalize these vectors (i.e., make them have length 1) and avoid using trigonometric functions.

Again, if the light source is below the horizon, then it should not illuminate the surface. In this case, the above dot product will be negative. Thus, we do Lambertian illumination by multiplying the surface color by the maximum of the Lambert cosine and zero.

We will use this model to do per-vertex lighting. We get vertex normals from the application, in gl_Normal. At each vertex, we compute the light-direction vector and find the Lambert cosine. We compute the apparent color of the surface using the Lambertian Illumination Model.

In a vertex shader, we could do all this with code like the following.

[GLSL]

// Compute vertex position in camera coordinates
vec4 myvert4 = gl_ModelViewMatrix * gl_Vertex;
vec3 myvert = myvert4.xyz / myvert4.w;

// Transform normal vector and ensure it has length 1
vec3 mynorm = normalize(gl_NormalMatrix * gl_Normal);

// Compute light-source direction (unit vector)
vec3 lightpos = vec3(-1., 2., 3.);  // Light position, cam coords
vec3 lightdir = normalize(lightpos - myvert);

// Compute Lambert cosine (or 0 if this is negative)
float lambcos = max(0., dot(lightdir, mynorm));

// Apply Lambertian illumination model
gl_FrontColor = lambcos * gl_Color;

See lambertvert_shaders.zip for shaders that compute per-vertex lighting using the Lambertian Illumination Model. These shaders are intended to be used with useshaders.cpp.


CS 381 Fall 2012: Lecture Notes for Tuesday, October 9, 2012 / Updated: 9 Oct 2012 / Glenn G. Chappell / ggchappell@alaska.edu