Goal: In this lab, you will get acquainted with how OpenGL is designed. At the end of the lab, you should be able to display a textured and lit 3D object.
If you run into problems, you can either look in the textbook, or visit http://www.opengl.org. There you will, among many other things, find the entire OpenGL Programming Guide in on-line version (old version) and, most importantly, the OpenGL 3.2 Quick Reference Card.
Note that you should write down answers to all questions before you get examined!
Note: This lab is all new as of 2012! There is bound to be unclear
instructions, vague questions and even incorrect statements. Please let
us know if you find such problems.
We will use the C language for this lab, plus GLSL for shader programs.
Some notes about the C language
& takes the address to a variable. When passing variables by reference, C always passes pointers.
An array and a pointer to the first element is the same thing.
int *a; declares the variable a as a pointer to an int.
#define and #include are preprocessor directives. Be very careful
with them. A #define to make a constant is safe, but if you make a
#define with variables in it, it quickly goes out of hand.
Point3D p; is a point struct, as defined in the VectorUtils library.
GLSL
GLSL code is similar to C code, but with a strong emphasis on computation.
Most GLSL code performs floating-point calculations. Common
datatypes used are float, vec2, vec3 and
vec4. These datatypes represent 1D, 2D, 3D and 4D vectors.
Arithmetic operations can be performed directly on these
datatypes.
For integer calculations (such as counting loop iterations),
int is available. The bool datatype is also
available.
A small GLSL function can look like:
vec4 applyDirectionalLight(vec3 normal, vec4 originalColor)
{
vec3 lightDirection = normalize(vec3(0.5, 0.8, 0.7));
float strength = dot(lightDirection, normal);
if (strength < 0.0)
strength = 0.0;
vec4 color = originalColor.xyxx * strength;
return color;
}
vec3(0.5, 0.8, 0.7) constructs a new vec3 from three
floating-point values.
dot() calls a predefined math function.
originalColor.xyxx performs "swizzling" on the original
vector: the result is a vec4 whose XYZW elements are taken
from the X, Y, X and X elements of originalColor,
respectively.
You can find a complete list of built-in mathematical functions in the GLSL Language Specification.
Vertex shaders perform per-vertex calculations. That's where
vertices are transformed, per-vertex lighting calculations are done,
and where skeletal animation systems do most of their work.
Fragment shaders perform per-pixel calculations. That's where texture
and lighting colours are combined into one final pixel colour value.
The code for a shader program is enclosed inside the main() function. It takes no arguments, and returns nothing. Communications between OpenGL, the vertex shader and the fragment shader is done by reading/writing global variables.
Variables can have a few different qualifiers:
uniform - the value is constant over an entire polygon; it is
read/write for OpenGL, and read-only for fragment and vertex
shaders.
in/out - input and output. In vertex shaders, these are
"attributes", can be unique for every vertex (by passing arrays). The
resulting color froma fragment shader is an out variable.
in/out between shaders - out from vertex, in to fragment: the value will be interpolated over the surface of a
polygon; write in vertex shader, read in
fragment shader.
All variables whose names begin with "gl_" are predefined by OpenGL. These are always present, and they can be used without declaring them first. For now, you only need to care about gl_Position, which is a vec4 with the resulting vertex from your vertex shader after transformation and projection. Writing this in your vertex shader is mandatory.
OpenGL will take the output from the vertex shader, interpolate the resulting values over the surface of any neighboring polygons, and then run the fragment shader once for every pixel which the polygon is supposed to render to. Any extra out variables in the vertex shader will also be interpolated over the polygon, and the result is available to the fragment shader in in variables.
You can find a full list of pre-defined variables in the GLSL Language Specification.
Debugging a shader is a story of its own. On the positive side, shaders are often very simple (especially in this lab). However, debugging takes some special tricks. You can't printf() from a shader.
Compilation errors are reported to stdout. This is a main source for information.
You can also play some tricks in the shaders. If your shader is running, but produces the wrong data, you can use its output for extra information. For example, you can output bright red to signal the result of some test comparison.
|
|
Download the lab package below and unpack it at a suitable location. You may note that the lab package consists of the first example from Chapter 3 although updated for using FreeGLUT instead of the preliminary replacement I had to use for the book.
lab2012-1.zipNOTE: This package was updated 120202 with a new VectorUtils2 unit, which I hope will be more "by the book" to avoid confusion.
There are several files included in the lab environment:
makefile - contains rules for how the executable should
be built; read by make.
lab1-1.c - the actual lab code; this is where the main
program resides.
GL_utilities.c - utilities for loading shaders and more.
VectorUtils2.c - Simple vector/matrix package.
loadobj.c - Loader for "OBJ" models.
lab1-1.vert - Minimal vertex shader.
lab1-1.frag - Minimal fragment shader.
VectorUtils2.c - Simple vector/matrix package.
You will be using makefile, lab1-1.c, shaders and respective .h files directly.
Compile the test program by entering the lab1 directory and performing make on the command line. This should produce a new executable file called lab1-1.
Run lab1-1 by typing ./lab1-1 on the command-line. It should show a white triangle against a dark background.
Open lab1-1.c and have a look inside it. There are two functions of interest to you, init() and display(). init() is called once during program startup and display() is called every time it is time to render a new frame of graphics.
Currently, display() does three things:
* Clearing of screen and Z-buffer
* Rendering of a triangle using OpenGL rendering
commands
* Swapping front- and backbuffer
The init() function do work critical for rendering:
* Sets the background color and activates the Z-buffer
* Uploading the vertex list to the GPU
* Loading the vertex and fragment shaders
Try changing the triangle data, by moving the vertices.
Change the color of the triangle and the background.
For those of you on other systems, you may need other makefiles or project files. Let us know if you need them.
Questions:
|
|
Goal: To transform your polygon with 2D transforms defined by matrices.
Copy lab1-1.c to
lab1-2.c and add a new entry to the makefile. Make this
section's changes to lab1-2.c. Also copy the shaders similarly.
Define transformation matrices, somewhat like this:
GLfloat myMatrix[] = { 1.0f, 0.0f, 0.0f, 0.5f,What does this matrix do? Define other 2D transformations.
Use the following call to send your matrix to your shaders.
The "program" variable is a reference to your shaders, returned when you first loaded them.
In your vertex shader, declare your matrices and apply them to your vertices as you see fit. For the example above, there should be a matrix declared like this:
uniform mat4 myMatrix;
Questions:
|
|
Goal: To add time-based rotation/translation of the object.
Copy lab1-2.c to lab1-3.c and add a new entry to the makefile. Make this section's changes to lab1-3.c. Also copy the shaders similarly.
You can get the current time using
GLfloat t = (GLfloat)glutGet(GLUT_ELAPSED_TIME);
The function returns an integer, a milliseconds value. We cast it to float to avoid truncation when scaling it.
In order to render new images repeatedly, you should use glutTimerFunc(). It may look like this:
glutTimerFunc(20, &OnTimer, 0);
which refers to a call in your code that can look like this:
void OnTimer(int value)
{
glutPostRedisplay();
glutTimerFunc(20, &OnTimer, value);
}
Modify matrices using a time-varying variable to produce an animation. Note that you will now need to upload your matrices in the display() callback, not in init().
Does your animation flicker? Try this:
1) In display(), replace glFlush() with glutSwapBuffers();
2) After glutInit(), add this:
glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
Any improvement? What happened?
Questions:
|
|
Goal: To interpolate data between vertices.
Copy lab1-3.c to lab1-4.c and add a new entry to the makefile. Make this section's changes to lab1-4.c. Also copy the shaders similarly.
Add a new array, similar to the vertex array, but this time for colors. Each vertex should have its own color.
Upload this array to the shaders just like you did with the vertices.
Pass the colors to "out" variables in the vertex shader, and as "in" variables in the fragment shader.
Use the interpolated color for the fragment color.
Questions:
|
|
Goal: To build a cube, using more 3D data.
Copy lab1-4.c to lab1-5.c and add a new entry to the makefile. Make this section's changes to lab1-5.c. Also copy the shaders similarly.
Build a cube by creating twelve triangles. Make the cube coordinates within +/- 0.5 units from origin.
As of part 4, set each vertex to a unique color and interpolate between these colors.
Use a transformation as of part 3 to rotate the model. Does something look strange?
It is likely that it looks strange in some orientations. We need some kind of visible surface detection (VSD). We will try one of the most widely used VSD
methods: Z buffering. To use that, you need to do three things:
Enable/disable Z-buffering. Compare the difference.
Questions:
|
|
Goal: To render a complex 3D model read from disc.
Copy lab1-5.c to lab1-6.c and add a new entry to the makefile. Make this section's changes to lab1-6.c. Also copy the shaders similarly.
The file loadobj.c will load a Wavefront OBJ file to disc.
Model *m;
m = LoadModel("bunny.obj");
However, from there we need to upload it to the GPU ourselves, using
a Vertex Array Object as main reference. We also need a few Vertex
Buffer Objects temporarily.
unsigned int bunnyVertexArrayObjID;
unsigned int bunnyVertexBufferObjID;
unsigned int bunnyIndexBufferObjID;
unsigned int bunnyNormalBufferObjID;
Uploading it is similar to what we did with simpler models before:
glGenVertexArrays(1, &bunnyVertexArrayObjID);
glGenBuffers(1, &bunnyVertexBufferObjID);
glGenBuffers(1, &bunnyIndexBufferObjID);
glGenBuffers(1, &bunnyNormalBufferObjID);
glBindVertexArray(bunnyVertexArrayObjID);
// VBO for vertex data
glBindBuffer(GL_ARRAY_BUFFER, bunnyVertexBufferObjID);
glBufferData(GL_ARRAY_BUFFER, m->numVertices*3*sizeof(GLfloat), m->vertexArray, GL_STATIC_DRAW);
glVertexAttribPointer(glGetAttribLocation(program, "inPosition"), 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(glGetAttribLocation(program, "inPosition"));
// VBO for normal data
glBindBuffer(GL_ARRAY_BUFFER, bunnyNormalBufferObjID);
glBufferData(GL_ARRAY_BUFFER, m->numVertices*3*sizeof(GLfloat), m->normalArray, GL_STATIC_DRAW);
glVertexAttribPointer(glGetAttribLocation(program, "inNormal"), 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(glGetAttribLocation(program, "inNormal"));
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bunnyIndexBufferObjID);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, m->numIndices*sizeof(GLuint), m->indexArray, GL_STATIC_DRAW);
Don't forget to make error checks.
glBindVertexArray(bunnyVertexArrayObjID); // Select VAO
glDrawElements(GL_TRIANGLES, m->numIndices, GL_UNSIGNED_INT, 0L);
There are no colors, so you need to edit your shaders. You can use the normal vector in any way you like (be creative!) to select colors by vertex. Then these colors should be interpolated over the triangles.
Questions:
Goal: To render a model with diffuse shading.
Copy lab1-6.c to lab1-7.c and add a new entry to the makefile. Make this section's changes to lab1-7.c. Also copy the shaders similarly.
Finally, we want the bunny to look a bit better, with somewhat realistic light. A good start is diffuse shading.
You need to transform the normal vector in order to make the normal vectors follow the rotation of the model. You do that by removing the translation, which is equivalent to casting the 4x4 matrix to a 3x3 one.
If you have a transformation for your models, it should then be applied to normals as well. Example:uniform mat4 myMatrix;
mat3 normalMatrix1 = mat3(myMatrix);
transformedNormal = normalMatrix1 * inNormal;That normal vector is now ready to use for light calculations. Use a hard-coded light source in your shader, like this:
const vec3 light = vec3(0.58, 0.58, 0.58);You will need to use the following built-in functions in the shaders:
dot() takes the dot product.
max() and clamp()... OK, you get the picture?
With 3D models like this, it is more important than ever to use visible surface detection. Try turning Z buffering off. What happens?
Note: Usually, we divide the transformation in three parts: Model to world, world to view, and projection. Normal vectors should only be affected by the first two.
Questions:
Goal: To evaluate the difference between Gouraud and Phong
shading. (Note: This will be more meaningful when we get to specular
shading, and more of a curiosity at this point.)
Copy lab1-7.c to lab1-8.c and add a new entry to the makefile. Make this section's changes to lab1-8.c. Also copy the shaders similarly.
In the previous task, you implemented one shader of the two main types. Now, implement the other one and compare the difference.Questions:
That concludes lab 1. Good work! In the next lab, you will experiment with texture mapping, scenes containing multiple objects, and camera placement.