Lab 3: Virtual world and specular shading
Goal: In this lab, you will expand your view to a richer virtual world, including herarcical models and a skybox. You will also
implement the Phong lighting model.
Note: I expect this lab to be relatively demanding, parts 3 and 4 in
particular. Please let me know if anything is incomplete, misleading or
harder than it should. (See below for a draft for a revision of part 4 in order to reduce the risk of mistakes, as of 12-02-19.)
Lab files:
lab3.zip
Mac users will need the updated MicroGlut, see the main lab page.
If you run into problems, there are several resources. You have
your textbook. OpenGL
Reference Pages describe all the OpenGL API functions. OpenGL
Shading Language Specification (found via
http://www.opengl.org/documentation/specs/,
"OpenGL Shading Language Specification") describes GLSL.
1) Hierarcical modelling, the windmill
In the files from lab 2,
there is a windmill in two parts, the mill and the wings. Build a
working windmill from the parts. Four wings should be placed at the
appropriate place and rotate around it. Create appropriate rotation and
translation matrices to make suitable model-to-world transformations.
Questions:
- How can you get all four blades to rotate with just one
time-dependent rotation matrix?
- How do you make the wings follow the body's movements?
2) Manual viewing controls
The "look-at" function is useful for
more than placing the camera in some fixed place. You can use
glutPassiveMotionFunc to write a control based on mouse movements. The
function glutPassiveMotionFunc() takes a callback argument, a pointer
to a function that accepts the x and y coordinates of the mouse as
parameters. For keyboard controls, there are GLUT functions for
accepting keydown and keyup events, but for many purposes you are
better off with knowing what keys are presently down. That is provided
in the utility module. This overrides the key-up and key-down callbacks
so you can not use them. You initializes this with initKeymapManager(),
and after that you can check any key with keyIsDown(), e.g. if
(keyIsDown('a')) { (something happens) }.
Questions:
- What kind of control did you implement?
- Can you make this kind of control in some other way than manipulating a "look-at" matrix?
3) Virtual world and skybox
Using the manual controls above, you should expand your virtual
universe to a simple "virtual world" with a set of basic features.
- Add a "ground" as a fairly large textured polygon.
- The manual controls should allow you to move around in the "world"
- Several objects should be included, most of them stationary.
- Finally,
add a "skybox". For this purpose, a skybox model and texture are
provided. To implement a skybox, the skybox should follow the camera
and seem to be drawn at the back. To do this, you shoulddraw the skybox
first, with Z-buffer turned off (glDisable(GL_DEPTH_TEST))
. The skybox should be rotated as the camera, but not translated. You
can do this with a copy of the camera matrix where you zero out the
translation component. Culling needs to be off; you always want to draw
the skybox (and the model is not designed for culling). Finally, don't
forget to turn the Z-buffer on again after drawing the skybox.
Your program is growing now. You may want to look into ways to structure it a bit. There are many ways to do that.
Questions:
- How did you handle the camera matrix for the skybox?
- How did you represent the objects? Is this a good way to manage a scene or would you do it differently for a "real" application?
- Should lighting be used for the skybox?
4) Specular shading, external light sources
Now you have a nice
scene but you need better light. Implement specular Phong shading in
your shaders. Not only should it include a specular component, but it
should also do that using light sources that are specified by the CPU. (This is a challenging task, but quite rewarding.)
Here are specifications of four light sources:
OLD SPECIFICATION (WORKING BUT CAN CAUSE TROUBLE, SEE BELOW):
// Color r, g, b and specularity
GLfloat lightSourcesColors[] = { 1.0f, 0.0f, 0.0f, 10.0f, // Red light
0.0f, 1.0f, 0.0f, 20.0f, // Green light
0.0f, 0.0f, 1.0f, 60.0f, // Blue light
1.0f, 1.0f, 1.0f, 5.0f }; // White light
// Light source direction x, y, z and an unused component (used for indicating positional or directional light)
GLfloat lightSourcesDirections[] = { 5.0f, 0.0f, 0.0f, 0.0f, // Red light along X
0.0f, 0.0f, 5.0f, 0.0f, // Green light along Z
-1.0f, 0.0f, 0.0f, 1.0f, // Blue light along X
0.0f, 0.0f, -1.0f, 1.0f }; // White light along Z
The red and green light sources should be positional, the other
directional. All four should be implemented. This information needs to
be uploaded to the fragment shader as uniform data.
The fourth component of the color is the specularity, the exponent in the Phong model.
You may upload the entire matrices or as vector as you please. If you
upload as matrices, you may get vectors by columns. If your white light
source is extremely strong, that is an indication of that problem. Then
you have to read out your vectors differently. Try forming a vec4 by
reading separate components.
NEW SPECIFICATION:
The data above has one major problem: Since I defined the data as two
4x4 matrices, it is tempting to treat the data as matrices, but this
gave some problems. You can easily get into problems where the matrix
is transposed compared to what you think it should be. Also, it was a
mistake to put the positional light sources at height 0; If they are
some distance above the ground, they will be visible in a typical
ground plane which helps a lot.
Here is an alternate formulation, almost the same data (only adding
height of the positional lights) but now avoiding 4x4 matrices:
Point3D lightSourcesColorsArr[] = { {1.0f, 0.0f, 0.0f}, // Red light
{0.0f, 1.0f, 0.0f}, // Green light
{0.0f, 0.0f, 1.0f}, // Blue light
{1.0f, 1.0f, 1.0f} }; // White light
GLfloat specularExponent[] = {10.0, 20.0, 60.0, 5.0};
GLint isDirectional[] = {0,0,1,1};
Point3D lightSourcesDirectionsPositions[] = { {10.0f, 5.0f, 0.0f}, // Red light, positional
{0.0f, 5.0f, 10.0f}, // Green light, positional
{-1.0f, 0.0f, 0.0f}, // Blue light along X
{0.0f, 0.0f, -1.0f} }; // White light along Z
Upload to shader:
glUniform3fv(glGetUniformLocation(program, "lightSourcesDirPosArr"), 4, &lightSourcesDirectionsPositions[0].x);
glUniform3fv(glGetUniformLocation(program, "lightSourcesColorArr"), 4, &lightSourcesColorsArr[0].x);
glUniform1fv(glGetUniformLocation(program, "specularExponent"), 4, specularExponent);
glUniform1iv(glGetUniformLocation(program, "isDirectional"), 4, isDirectional);
Thus, I upload as arrays of three-component vectors and arrays of scalars. Declarations in shader:
uniform vec3 lightSourcesDirPosArr[4];
uniform vec3 lightSourcesColorArr[4];
uniform float specularExponent[4];
uniform bool isDirectional[4];
END OF REVISION
Debugging light like this requires some care. Take special care in
keeping track on what coordinate system you work in.
Model data starts in model coordinates, light sources are given in world coordinates. All lighting
calculations take place in any coordinate system, model, world or view (camera) coordinates
(not projected though) but you must decide which one.
Select one and stick to it. I recommend that you use view coordinates.
Make sure normal vectors, light direction and viewing direction are all
given in the same coordinate system.
The viewing direction requires that you create a vector from the
surface to the camera. For that, you need the position of the surface.
You get that by interpolating the vertex positions using varying ("in"
in vertex, "out" in fragment) variables.
Start out working on a single light source, diffuse component only.
When that works, switch to the specular component. Once that works for
one positional and one directional light source, chances are good that
everything works. Working on one at a time will help you not to get
distracted by too much information.
Questions:
- How do you generate a vector from the surface to the eye?
- Which vectors need renormalization in the fragment shader?
5) Multitexturing
Using multiple textures on a model can be
very useful for many purposes. The difference from the texture mapping
introduced in lab 2 is that you must bind textures to specific texture
units:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureId);
This enables the textures as texture unit 0.
If the texture is called "tex" in your shader, you can pass the texture unit to that variable like this:Next, go back to init() again. After the
shaderTimeLocation has been located, perform the following operation:
glUniform1i(getUniformLocation(shaderProgram, "tex"), 0);
Load two textures and apply them to an object. Build from your specular shader, and combine lighting and texture.
Questions:
- How did you choose to combine the texture colour and the lighting
colour?
- How did you choose to combine the two textures?
Extra) Managing transparency
In your virtual world, make at least two object semi-transparent. Move
around. You should be able to find locations where the transparency
(combined with Z buffering) causes problems. Solve these problems.
Questions:
- How did you remove the errors caused by transparency?
That concludes lab 3. Good work! In the next lab, we will make the ground more interesting, a 3D terrain.