| CS 381 Fall 2012 > Lecture Notes for Tuesday, October 9, 2012 |
Recall that GLSL has three qualifiers that deal with communicating data to shaders:
attribute. Applied to globals in a vertex shader.
Means per-vertex input from application.uniform. Applied to globals in a vertex or fragment shader.
Means per-primitive input from application.varying. Applied to globals in
both vertex and fragment shaders.
Means output from vertex shader and input to fragment shader.
Value for each fragment is computed by interpolating
between values for vertices.
We now look at how each of these is handled in a program.
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.
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.
GLint—that
uniquely indentifies it).
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.
attribute Variables
Handling attribute values is much like
handling uniform values.
The differences are as follows.
attribute values are per-vertex
data, and are only used by vertex shaders.
Do not declare an attribute
variable in a fragment shader.attribute variable
with glGetAttributeLocationARB.attribute variable
with glVertexAttrib*ARB.
Note: The name of this function is not quite what you would expect.glVertexAttrib*ARB
either inside or outside a glBegin-glEnd
pair (just like other ways of setting attributes:
glColor*, etc.).
And there are facilities for sending data to attribute
variables using vertex arrays/vertex buffer objects.
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.
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.
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.
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*.
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.
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.)
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
gl_Vertex,gl_Position, orIn 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.
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].
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.
ggchappell@alaska.edu