Course list http://www.c-jump.com/bcc/
The GLUT library is responsible for
creating frame buffers
creating and refreshing windows
interacting with the user via the keyboard and mouse
(For an exhaustive list of GLUT functions, see FreeGLUT API on freeglut.sourceforge.net website.)
GLUT eliminates the need to interact with your current OS to control a window on the screen and deal with any platform-specific code for devices. GLUT isn't the only choice -- many OpenGL game developers prefer working with SDL. In our case, GLUT is the easiest library to interact with, and this lab is aiming to prove it.
Download c262_lab08.zip and unzip it under the labs subdirectory:
C:\bcc\GL262Labs\labs\c262_lab08
Open Visual Studio project
C:\bcc\GL262Labs\labs\c262_lab08\c262_lab08.sln
and compile the executable:
Build -> Build Solution
(or simply press F7.)
GLUT implements event-driven programming model for our window. For example, when the user clicks or moves the mouse, GLUT can call our event handler function and allow our program to keep up with the user's actions.
In this lab we ask GLUT to notify us about the following events (file c262_lab08.cpp):
glutReshapeFunc( change_viewport ); // when window is resized glutDisplayFunc( render ); // when window needs to be drawn glutIdleFunc( animate ); // when there is nothing else to do glutMouseFunc( callback_mouse_button ); // when mouse is clicked glutKeyboardFunc( keyboard_down ); // when a key is down glutKeyboardUpFunc( keyboard_up ); // when the key goes up glutPassiveMotionFunc( look ); // when mouse moves glutMotionFunc( look ); // when mouse drags around
Examine each of these event handling functions and make sure you understand their parameters.
Since we are about to start moving in 3D space, it is important to add a plane surface for visual guidance while moving in virtual world. Recall that in Lab 7, Diffuse and specular light , we used Wavefront OBJ loader to load and display various models.
Add Wavefront OBJ support to c262_lab08.cpp. (Hint: I would use WinMerge to do the initial editing.) First, comment out the existing #include directive:
//#include "../../../common/Objects/dragon.h"
To allow to testing of our program with a variety of objects, add code
//------------------------------------------------------------------ // HOW TO TEST DIFFERENT MODELS: //------------------------------------------------------------------ // Include one of the models defined in a header file, // -or- // load te model from the OBJ file by defining MODEL_OBJ_FILENAME //------------------------------------------------------------------ //#include "../../../common/Objects/dragon.h" //#include "../../../common/Objects/phlegm_small.h" // UPDATE! - look at this file if you haven't! // It has the vertices[], normals[] and indices[] in it //#include "../../../common/Objects/teapot.h" // use scale_amount = 10.0f; //#include "../../../common/Objects/cube.h" // scale_amount = 0.5f; //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/bunny-1500.obj" // scale_amount = 100.0f; //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/bunny-5000.obj" // scale_amount = 100.0f; //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/cow-1500.obj" //scale_amount = 40.0f; //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/cow-5000.obj" //scale_amount = 40.0f; //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/cup.obj" // scale_amount = 0.01f; //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/dragon-1500.obj" // scale_amount = 40.0f; //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/dragon-5000.obj" // scale_amount = 2.0f //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/lamp600.obj" // scale_amount = 0.007f //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/lego.obj" // scale_amount = 0.001f //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/shammy.obj" // scale_amount = 0.001f //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/snakie.obj" // scale_amount = 0.001f //#define MODEL_OBJ_FILENAME "../../Models/Spheretree/teapot.obj" // scale_amount = 0.001f //#define MODEL_OBJ_FILENAME "../../Models/Misc/tetrahedron.obj.txt" // crashes the driver //#define MODEL_OBJ_FILENAME "../../Models/Misc/cube5.obj" // a bigger cube (5 units) -- scale_amount = 0.15f; //#define MODEL_OBJ_FILENAME "../../Models/Misc/cube_blender.obj.txt" // works fine //#define MODEL_OBJ_FILENAME "../../Models/Misc/dragon.obj" // scale_amount = 40.0f; //#define MODEL_OBJ_FILENAME "../../Models/Misc/teapot.obj" // from http://people.sc.fsu.edu/~jburkardt/data/obj/obj.html #define MODEL_OBJ_FILENAME "../../Models/Misc/car.obj" // scale_amount = 3.0f; #define MODEL_PLANE_FILENAME "../../Models/Misc/plane.obj"
directly above the line of code that declares the object_model variable:
Model* object_model;
Uncomment the line
Model* plane_model;
At the end of the render() function body, immediately before the glutSwapBuffers() call, add code to draw a wireframe ground:
// Do everything related to the floor plane glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); // Note: you can probably *store* these matrices to improve efficiency - since it's not moving. // The plane is a square with side 0.5 units // sitting on the Z-X plane glm::mat4 mtx_trans = glm::translate( glm::mat4( 1.0f ), glm::vec3( 0.0f, -0.5f, 0.0f ) // push it down ); plane_model->mM = glm::scale( // Scale second mtx_trans, // Translate first glm::vec3( 100.0f, 100.0f, 100.0f ) ); camera->set_uniform_view_perspective( plane_model->shader->program_ID ); plane_model->render();
We also need a couple of changes in the main() function as follows:
First, replace the lines
object_model->set_geometry(vertices, num_vertices*3*sizeof(GLfloat)); object_model->set_normals(normals, num_normals*3*sizeof(GLfloat)); object_model->set_index_buffer(indices, num_indices*sizeof(GLuint));
with the lines
#ifdef MODEL_OBJ_FILENAME WavefrontObjLoader loader( MODEL_OBJ_FILENAME ); if ( !loader.load() ) { std::cout << "Failed to load OBJ file, exiting!" << std::endl; return 1; } assert( loader.num_vertices ); object_model->set_geometry( loader.vertices, loader.num_vertices * 3 * sizeof( GLfloat ) ); if ( loader.num_normals ) { object_model->set_normals( loader.normals, loader.num_normals * 3 * sizeof( GLfloat ) ); } if ( loader.num_uvs ) { object_model->set_texture_coordinates( loader.UVs, loader.num_uvs * 2 * sizeof( GLfloat ) ); } if ( loader.num_indices ) { object_model->set_index_buffer( loader.indices, loader.num_indices * sizeof( GLuint ) ); } #else object_model->set_geometry(vertices, num_vertices*3*sizeof(GLfloat)); object_model->set_normals(normals, num_normals*3*sizeof(GLfloat)); object_model->set_index_buffer(indices, num_indices*sizeof(GLuint)); #endif
The above code lets us switch between objects declared in the C++ header files and those loaded from the OBJ files.
Second, immediately after the line
object_model->upload_2_server();
add the following:
plane_model = new Model( solid_color_shader ); #ifdef MODEL_OBJ_FILENAME WavefrontObjLoader loader2( MODEL_PLANE_FILENAME ); if ( !loader2.load() ) { std::cout << "Failed to load plane.obj file, exiting!" << std::endl; return 1; } assert( loader2.num_vertices ); plane_model->set_geometry( loader2.vertices, loader2.num_vertices * 3 * sizeof( GLfloat ) ); if ( loader2.num_normals ) { plane_model->set_normals( loader2.normals, loader2.num_normals * 3 * sizeof( GLfloat ) ); } if ( loader2.num_uvs ) { plane_model->set_texture_coordinates( loader2.UVs, loader2.num_uvs * 2 * sizeof( GLfloat ) ); } if ( loader2.num_indices ) { plane_model->set_index_buffer( loader2.indices, loader2.num_indices * sizeof( GLuint ) ); } #else plane_model->set_geometry(vertices, num_vertices*3*sizeof(GLfloat)); plane_model->set_normals(normals, num_normals*3*sizeof(GLfloat)); plane_model->set_index_buffer(indices, num_indices*sizeof(GLuint)); #endif plane_model->set_light(light); plane_model->upload_2_server();
Compile and run the program. Make sure everything works as expected. Try different models by uncommenting the corresponding MODEL_OBJ_FILENAME definitions. Remember to change the scale_amount variable if the object appears to be too small or too big on the screen.
In main(), find glutInitDisplayMode() and add GLUT_MULTISAMPLE:
glutInitDisplayMode( GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH | GLUT_MULTISAMPLE );
Multisampling helps achieving full-screen antialiasing by averaging pixel colors on the edges of polygons.
You need to enable multisampling to activate this feature:
glEnable( GL_MULTISAMPLE );
Increase window size to 1024 x 1024 (if your screen resolution is permitting):
glutInitWindowSize( 1024, 1024 );
In a typical FPS (First-Person Shooter) game, the WASD keys allow the user to move around. Internally this is achieved by translating the coordinates of the scene along the X or Z axes.
Locate keyboard_down() in c262_lab08.cpp. This function takes in a character (the one that was pressed) as well as the x and y position of the mouse when the key was pressed. To implement some basic key controls, let's start with a switch like this:
switch (key) { case 033: // octal equivalent of the Escape key glutLeaveMainLoop(); break; case 'w': // increase the camera's Z velocity break; case 's': // decrease the camera's Z velocity break; case 'a': // decrease the camera's X velocity break; case 'd': // increase the camera's X velocity break; }
Since we want to control smooth and continuous camera movement, the kyes that we press on the keyboard should not update the camera position directly. So why not? It's because the keyboard sends the "key-press" events at a rate of 10 times per second. If we change the position at this rate, the motion on the screen will be noticeably jerky. Also, this kind of camera movement won't be consistent across multiple OS and hardware platforms.
Instead, we will have our camera set stationary to begin with, but when it starts moving, the motion is controlled by the velocity. The actual speed will be a combination of the motion velocities along the X and Z axes. For example, if the user pushes down the W key, it is a signal for the camera to start moving forward. This can be done by a single line of code like this:
case 'w': // increase the camera's Z velocity zVel = max_speed; break; case 's': // decrease the camera's Z velocity zVel = -max_speed; break; case 'a': // decrease the camera's X velocity xVel = max_speed; break; case 'd': // increase the camera's X velocity xVel = -max_speed; break;
Add code for the remaining keys according to the comments inside the switch statement listed above. Do it!
switch ( key ) { case 033: glutLeaveMainLoop(); break; case 'w': zVel=0; break; case 's': zVel=0; break; case 'a': xVel=0; break; case 'd': xVel=0; break; }
Next, when one of the WASD keys is released, the camera motion should stop. We capture this event in keyboard_up() function. Go ahead and set the X and Z velocities to zero. Do it!
As in a typical FPS game, we implement zooming by a mouse button click. When the mouse button is pressed, the view zooms in. As soon as the mouse button is released, the zoom level goes back to normal.
In OpenGL, the visual effect of zooming is set by the projection matrix transformation. Originally, projection is set in FPSCamera class constructor:
mP = glm::perspective( fov, aspect, nearPlane, farPlane );
To control the projection in support of different zooming levels, we add new member function to the FPSCamera class. Make sure to add function declaration in FPSCamera.h header file:
void set_perspective( GLfloat fov, GLfloat aspect, GLfloat nearPlane, GLfloat farPlane );
and then add the function implementation to the FPSCamera.cpp:
void FPSCamera::set_perspective( GLfloat fov, GLfloat aspect, GLfloat nearPlane, GLfloat farPlane ) { mP = glm::perspective( fov, aspect, nearPlane, farPlane ); }
Save all changes and open c262_lab08.cpp. Take a look at the callback_mouse_button() function. Here, we simply set the zooming flag to true or false. The flag is actually used by the render() function. If zooming mode is on, the FOV (field of view of the camera) is reduced. If zoom is off, the FOV is restored. We maintain the FOV range between 5.0f and 60.0f degrees.
The render() function invokes zoom_in() to reduce the FOV. Find zoom_in() and add this code:
// FOV == field of view of the camera // zooming is set by the callback_mouse_button() fov -= 10.0f; if ( fov < 5.0f ) fov = 5.0f; // don't allow fov less than 5 camera->set_perspective( fov, 1.0f, 1.0f, 1000.0f );
Similarly, locate zoom_out() function and add code to increase the FOV by 10.0f degrees at a time. Make sure to keep the max FOV at 60.0f degrees. Do it!
A common use of the mouse is to allow the viewer to look around, i.e. turn head left/right and also look up and down. In FPS game the behavior is the same in both zooming mode (when the mouse button is pressed down), and during a "passive" mouse movement within the window. Since GLUT generates two different events to distinguish mouse drags from the regular motion, we must specify the handler function twice to connect it with both mouse drag and mouse move events. Our handler function is named look():
glutPassiveMotionFunc( look ); // when mouse moves glutMotionFunc( look ); // when mouse drags around
To look around, the program must determine if the mouse is moving left/right, up/down, and by how much. To do all this, the program needs to
remember previous mouse position
compare previous position with the current one
use delta between old and new positions to rotate the camera accordingly
void look( int x, int y ) { if ( is_first_time ) { // Check for zooming mode, but do not allow the model // jump off the screen when the mouse enters the window from an outside // region of screen. Thus, to activate the mouse, you need to click the // mouse to zoom the view AND move it a little: if ( zooming ) is_first_time = false; prev_mouse_X = GLfloat( x ); prev_mouse_Y = GLfloat( y ); return; } //...
By subtracting the current mouse position from the old one, we determine how much of a difference there is in the mouse movement (do it!):
// 3) ... GLfloat deltaX = prev_mouse_X - x; GLfloat deltaY = prev_mouse_Y - y; if ( deltaX < -6.0f ) { deltaX = -1.0f; } if ( deltaX > 6.0f ) { deltaX = 1.0f; } if ( deltaY < -6.0f ) { deltaY = -1.0f; } if ( deltaY > 6.0f ) { deltaY = 1.0f; }
Follow the instructions provided inside the look() function comments and finish its implementation. Do it! Here's a sample:
void look (int x, int y) { if (is_first_time) { //is_first_time = false; // Check for zooming mode, but do not allow the model // jump off the screen when the mouse enters the window from an outside // region of screen. Thus, to activate the mouse, you need to click the // mouse to zoom the view AND move it a little: if ( zooming ) is_first_time = false; prev_mouse_X = x; prev_mouse_Y = y; return; } GLfloat deltaX = prev_mouse_X - x; GLfloat deltaY = prev_mouse_Y - y; if ( deltaX < -6.0f ) { deltaX = -1.0f; } if ( deltaX > 6.0f ) { deltaX = 1.0f; } if ( deltaY < -6.0f ) { deltaY = -1.0f; } if ( deltaY > 6.0f ) { deltaY = 1.0f; } if (zooming) { deltaX /= 10.0f; deltaY /= 10.0f; } if ( camera->is_flying ) { camera->rotateX((-deltaY)/( 50 * 5 )); camera->rotateY((-deltaX)/( 20 * 5 )); camera->rotateZ((-deltaX)/( 70 * 5 )); } else { camera->rotateX( deltaY / 150.0f ); camera->rotateY( deltaX / 150.0f ); } prev_mouse_X = x; prev_mouse_Y = y; }
Try resizing the window. Notice that if the OpenGL window becomes wider or taller, the aspect of the model remains unchanged, resulting in a skewed display. This is because the aspect, which accounts for the relationship between the window width and height is currently hardcoded as 1.0f:
camera->set_perspective( fov, 1.0f, 1.0f, 1000.0f );
This works only for square windows with same width and height. To eliminate such restriction, we need to add a true aspect ratio variable to the perspective matrix calculation:
camera->set_perspective( fov, aspect, 1.0f, 1000.0f );
To make this change, find declaration of the fov variable
GLfloat fov;
and add the aspect ratio variable next to it:
GLfloat fov; GLfloat aspect = 1.0f; // assume width and height of the viewport are the same
Further, locate change_viewport() function and add the aspect ratio calculation:
void change_viewport(int w, int h){ aspect = GLfloat( w ) / h; // update glViewport(0, 0, w, h); }
Finally, search for every call to camera->set_perspective() and add the aspect parameter:
camera->set_perspective( fov, aspect, 1.0f, 1000.0f );
Compile and run the program. Make sure everything works as expected. Resize or maximize the window and observe that the aspect ratio of the screen now works correctly. If it does, you can set the window size to glutGet(GLUT_SCREEN_WIDTH) and glutGet(GLUT_SCREEN_HEIGHT) to occupy the entire screen:
GLfloat winWidth = glutGet( GLUT_SCREEN_WIDTH ); GLfloat winHeight = glutGet( GLUT_SCREEN_HEIGHT ); glutInitWindowSize( winWidth, winHeight );
There are no files to submit.
Demonstrate a working c262_lab08 program to your instructor.