Lab 4: Introduction to OpenGL

CSC385 - Computer Graphics

Goal: Learn the basics of OpenGL by trying it out!

Action:

NOTE: As you edit code you will be adding, removing and/or changing the code. Do not delete anything: comment out changes so I can see your edits.

This lab works on the current version of Visual C++ Express (VC++) installed on the Olin 102 machines. Other versions may require modification.

  1. Create a project:
    In VC++, create a new Visual C++ Project. Select "Win32 Console Project" as the template. Create the project on your desktop with the name "MyOpenGL." You'll be presented with a second window. Click on "Applications Settings" on the left and check"Empty Project."

    Download openGL.cpp from Blackboard to the project folder. Open the file from the VC++ interface. Right-click on MyOpenGL (under Soluton 'MyOpenGL' in the solution explorer on the left), select "AddExistingItem..." and navigate to select the OpenGL.cpp file. Build the project and run it. The application opens two windows; one is a console window and the other is an OpenGL window. The latter shows a red triangle.

  2. glut:
    OpenGL is a graphics library but it does not provide an interface to your windows system. This allows OpenGL to remain device independent. Interacting with window devices is left to a separate library call glut. The glut manual is available on Blackboard. The first few lines in main() initialize glut and various glut parameters. The line
    	glutInitWindowSize(400, 600);
    
    sets the window width to 400 and height to 600. Change this line to
    	glutInitWindowSize(400, 400);
    
    The line
    	glutCreateWindow("My First OpenGL program");
    
    creates the OpenGL window with the specified title. Change the string to "My Second OpenGL program." If you are still running the original application ("My First OpenGL program") exit by closing both windows. Then recompile and run the new version. (If try to recompile while the application is running you'll get a link error.) The OpenGL window should be square and have the new title.

  3. Display:
    The OpenGL window is drawn by the display() function. Add the line
    	cout << "In display." << endl;
    
    at the beginning of the display() function. Recompile and run the program. You should see the message written to the console window. This is because glut automatically calls display() when the program starts. We tell glut the name of our function that draws the OpenGL window by registering it for callback in main() with the command
    	glutDisplayFunc(display);
    

    Comment out this line of code, recompile, and run the program. What happens? Be sure to uncomment the line before going on to the next step.

  4. Colors:
    The line
    	glClear(GL_COLOR_BUFFER_BIT);
    
    in display() draws the background of the OpenGL window. Comment out this line of code, recompile, and run the program. What happens? (If nothing happens try resizing the window to make it larger.) Uncomment the line before continuing.

    The background color is preset in the init() routine by the command
      	glClearColor(0,0,0,0);
    
    Replace this line with
      	glClearColor(0,0,1,0);
    
    then recompile the program and run it. The background of the OpenGL window should be blue.

    Colors in OpenGL are typically represented in RGB or RGBA mode. In RGB, a color is represented by a 3-tuple of real numbers in the range [0,1]. These numbers represent the intensities of the red, green, and blue components of the color. The color (0,0,1) is blue; or more precisely it it is no red (0), no green (0), and maximum blue (1). Sometimes colors have a fourth component, called the alpha channel. This is used to specify transparency. You will probably never use transparency in your projects for this class but the glClearColor() command requires that you specify the alpha channel, even if it is not used. You may always set the alpha channel to 0.

  5. Drawing triangles:
    The code
    	// draw a red triangle
    	glColor3f(1,0,0);
    	glBegin(GL_TRIANGLES);
    		glVertex3f(-3,-1,-8);
    		glVertex3f(3,-1,-10);
    		glVertex3f(0,3,-9);
    	glEnd();
    
    in display() draws a red triangle with vertices at (-3,-1,-8),(3,-1,-10), and (0,3,-9). OpenGL uses a right handed coordinate system; from the viewer's perspective positive x is to the right, positive y is up, and positive z is out of the screen toward the viewer.

    Let's modify display() to draw another triangle. Immediately after the red triangle code shown above, insert the following.
    	// draw a green triangle
    	glColor3f(0,1,0);
    	glBegin(GL_TRIANGLES);
    		glVertex3f(-1,-1,-5);
    		glVertex3f(1,1,-5);
    		glVertex3f(0,1,-5);
    	glEnd();
    
    Recompile and run the program. You should now see a green triangle in front of the red one.

    Next move the red triangle code so it immediately follows the green triangle code. Recompile and run the program and you'll see that the green triangle is now behind the red one. From our viewpoint in 3D space, the green triangle should be in front of the red one. OpenGL doesn't perform hidden surface removal unless we specifically tell it to. We'll do that next.

  6. Hidden surface removal:
    To have OpenGL perform hidden surface removal we need to enable depth buffering, provide a depth buffer, and specify when the depth buffer should be cleared. Enable depth buffering by adding the code
     	// enable depth buffering
     	glEnable(GL_DEPTH_TEST);
    
    in the init() function. The depth buffer is established by glut. To tell glut we need one, change the line
      	glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
    
    in main() to
      	glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH);
    
    Finally, change the code
    	// clear buffers
    	glClear(GL_COLOR_BUFFER_BIT);
    
    in display() to
    	// clear buffers
    	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    to clear the depth buffer before the window is drawn. Recompile the code and run the program. The green triangle should appear in front of the red one.

  7. Drawing spheres:

    Glut provides support for drawing a variety of shapes (including a teapot). To draw a sphere add the code
    	glColor3f(1,0,1);
    	glTranslatef(0,0,-5);
    	glutSolidSphere(1,10,10);
    
    in display() after the triangle code. Recompile the code and run. You should see a purple sphere-like object.

    Change the glutSolidSphere(1,10,10) call to glutSolidSphere(0.75f,20,20). Recompile and run the program. The sphere-like object should be smaller but more sphere-like. The first parameter to glutSolidSphere() is the radius of the sphere; the second two parameters control the resolution of the sphere.

    Remove the sphere code before proceeding.

  8. Transformations:
    Suppose we want to rotate the rotate the red triangle by 90 degrees about the z axis. We could recompute the triangle vertices but OpenGL will do this for us. The command glRotatef(angle,x,y,z) tells OpenGL to perform a rotation by angle, which is in degrees, about the vector <x,y,z>. Modify the red triangle drawing code in display() as follows:
    	// draw a red triangle
    	glColor3f(1,0,0);
    	glRotatef(90, 0,0,1);
    	glBegin(GL_TRIANGLES);
    		glVertex3f(-3,-1,-8);
    		glVertex3f(3,-1,-10);
    		glVertex3f(0,3,-9);
    	glEnd();
    
    Recompile and run the program. The red triangle should now be rotated. OpenGL supports three types of transformations; rotations, translations, and scaling. We'll continue to explore transformations in the next few step.

  9. Animation:
    Let's animate the green triangle to move through the world. Create a global variable (don't tell anyone I said that ;0 ) as follows:
    	float move=0;
    
    and an idle() function that updates move.
    	void idle()
    	{
    		static float increment=-.0001;
    		move+=increment;
    		if (move>2 || move <-20)
    			increment *= -1;
    		glutPostRedisplay();
    	}
    
    Register this function for callback with the following line
    	glutIdleFunc(idle);
    
    in main() where the other callback functions are registered. Also include the function prototype at the top of the file with the other function declarations.

    The idle function will be called whenever the program is idle. The glutPostRedisplay() command tells OpenGL to call the display() function at the next opportune moment. We could have called display() directly but that is almost never a good idea. Use glutPostRedisplay() when you want the window redrawn. Modify the green triangle code as follows.
    	// draw a green triangle
    	glColor3f(0,1,0);
    	glTranslatef(0,0,move);
    	glBegin(GL_TRIANGLES);
    		glVertex3f(-1,-1,-5);
    		glVertex3f(1,1,-5);
    		glVertex3f(0,1,-5);
    	glEnd();
    
    This causes the triangle to be translated from its specified position by 0 in the x direction, 0 in the y direction, and move in the z direction. As move is updated the triangle will move backward and forward through our 3D world. Comment out the red-triangle drawing code so that we can watch the green triangle as it moves backwards. Compile and run the program. You should see the triangle moving back and (if you wait a really, really long time) forward but it should be flickering. To fix this, we need to use double buffering . Change the line
      	glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH);
    
    in main() to
      	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
    
    and replace the line
    	glFlush();
    
    in display() with
    	glutSwapBuffers();
    
    Recompile the program and run it. The flickering should be gone.

    OpenGL also let's us compose transforms. Replace the line
    	glTranslatef(0,0,move);
    
    with
    	glTranslatef(0,0,move);
    	glTranslatef(0,move,0);
    
    Recompile and run the program. The triangle should move down as well. The same effect can be achieved by a single call
    	glTranslatef(0,move,move);
    
    Verify this by modifying the code. We'll see more about composing transformations in the next step.

  10. Transformation matrices
    OpenGL translates, rotates, or scales a triangle by multiplying its vertices by an appropriate transformation matrix. It maintains a current modelview matrix that is updated with each transformation command. When the display() function is called the first time in our example, the modelview matrix is set to the identity, move= 0, and no translation occurs. The second time display() is called, the modelview matrix is still set to the identity matrix but move=-.01. The modelview matrix is updated to reflect a translation in y and z by -.01. The third time display() is called, the modelview matrix holds the translation by -.01 and move is -.02. The modelview matrix is updated to reflect an additional translation of -.02 for a total of -.03. The process continues.

    To further clarify the issue, uncomment the red triangle code, recompile the program, and run it. Now a 90 degree rotation is added in each step.

    To prevent this accumulation of transforms add the line
    	glLoadIdentity();
    
    at the start of display() . This command re-initalized the modelview matrix to the identity each time display is called. Now the translation in the i-th call of display() is -.01*(i-1). You may notice that the movement is much slower. To speed it up, change the size of increment in the idle() function. The animation speed depends on the the system you are using to run the program.To achieve a constant frame rate (that is machine independent) we should build a timer function. But that is beyond the scope of this tutorial.

    There is still a problem with our program. The translation command is applied to the red triangle as well as the green one. To correct this, change the green triangle code as follows.
    	// draw green triangle
    	glColor3f(0,1,0);
    	glPushMatrix();
    	glTranslatef(0,move,move);
    	glBegin(GL_TRIANGLES);
    		glVertex3f(-1,-1,-5);
    		glVertex3f(1,1,-5);
    		glVertex3f(0,1,-5);
    	glEnd();
    	glPopMatrix();
    
    The command glPushMatrix() instructs OpenGL to push a copy of the current modelview matrix onto a matrix stack. At this point in our code the modelview matrix is set to the identity. The glPopMatrix() command pops the matrix off the top of the stack and sets it as the current modelview matrix. Now the translate command affects the green triangle but not the red one. It is very important that every glPushMatrix() has a corresponding glPopMatrix() and vice versa!

  11. View volume:
    The portion of the 3D world that is visible to the view is called the view volume. Its shape is a truncated pyramid, or frustum . The frustum is specified by the line
    	gluPerspective(45,1,1,100)
    
    in init(). The first parameter is the half-height angle. The next parameter is the aspect ratio of the front face. The next two parameters specify the distance from the viewpoint to the near and far planes. The view volume shape and its specification are described in the following figure.



    Change the frustum specification to
    	gluPerspective(45.0, 1.0, 1.0, 6.0);
    
    You should also disable the animation. Then recompile and run the program. The red triangle should not appear. Why?

    Now change the frustum specification to
    	gluPerspective(45.0, 1.0, 6.0, 100.0)
    
    and recompile the program. Now the green triangle should disappear. Why? Try changing the other parameters and notice the effect they have on the OpenGL window. Restore the original settings before proceeding to the next step.

  12. Reshape:
    The objects in the view volume are projected onto the front face of the view volume creating the image that is drawn to the OpenGL window. Since the OpenGL window can be resized by the viewer, and thus have a different aspect ratio that the view volume, we must decide how the mapping is performed. This mapping is specified by the glViewport() command in the reshape() function. This function is called by glut whenever the user resizes the OpenGL window. The arguments passed to the function are the width and height of the OpenGL window in pixels. Currently, the mapping is specified as
    	glViewport(0,0,width,height);
    
    This command tells OpenGL to map the lower left corner of the projected image to the coordinate (0,0) of the OpenGL window; (0,0) is the lower left corner of the window. It also specifies that the upper right corner of the projected image is mapped to the coordinate(width,height) in window space; this is the upper right corner of the OpenGL window.

    Change the code to
    	glViewport(0,0,width/2,height/2);
    
    then recompile and run the program. Our scene will be drawn in the lower left quarter of the OpenGL window. Change the code back to its original setting and recompile.

    Since the projected image is square (the gluPerspective() specifies an aspect ratio of 1), the image in the OpenGL window will be distorted if the window is resized to be non-square. Try it. To fix this problem modify the reshape() function as follows:
    	void reshape(int width, int height)
    	{
    		if (width<height)
    			glViewport(0,0,width,width);
    		else
    			glViewport(0,0,height,height);
    	}
    

    Recompile and run the program. Now he projected image is always mapped to a square in the OpenGL window so its aspect ratio is preserved.

  13. Viewpoint:
    By default, the viewer is positioned at the origin. It is often convenient to change the viewpoint using the gluLookAt() command. Add the following after the glLoadIdentity() and gluPerspective() commands in display()
    	gluLookAt(0,0,-20,0,0,0,0,1,0);
    
    This command moves the viewpoint to the point (0,0,-20). The viewer is looking toward the point (0,0,0). The up direction is <0,1,0>. Recompile and run the program. From the new viewpoint, the red triangle occludes the green one.

    (Note: The frustum is always positioned with its apex at the viewpoint. It is always centered about toward vector; the toward vector is directed from the viewpoint to the point the viewer is looking at. The top of the frustum is always oriented by the up vector.)

  14. Keyboard input:
    OpenGL applications are typically event-driven. This means that the program waits in an infinite loop for user input. At the moment, your program does not accept user input but you are about to change that. First create a keyboard routine:
    	void keyboard(unsigned char key, int x, int y) 
    	{
    		if (key == ' ') 
    			cout << "You hit the space bar." << endl; 
    	}
    
    Register the keyboard callback with
    	glutKeyboardFunc(keyboard);
    
    and specify the function prototype at the top of the file. Recompile and run the program. Make sure the OpenGL window is active (click on it) then press the space bar. Your program should write "You hit the space bar." to the console window. Note that the keyboard function is only called when the OpenGL window is active. If you click on the console window and then press the space bar your program won't respond.

  15. Console input:
    In addition to writing to the console window, you can also read from it. Modify the keyboard() function as follows:
    	void keyboard(unsigned char key, int x, int y) 
    	{
    		int val;
    		cout << "You entered " << key << "." << endl; 
    		cout << "Now enter an integer." << endl; 
    		cin >> val;
    		cout << "You entered " << val << "." << endl;
    		}
    
    Compile and run the program. Click on the OpenGL window to activate it then press any key. Your program should output the first two messages to the console window. Click on the console window. Type a number and press return. The program should print the final message and the number you input. Try repeating the process without activating the console window. What happens and why? That will be useful to remember!

  16. Mouse Input:

    We can also take input from the mouse. Create the mouse() function shown below.
    	void mouse(int button, int state, int x, int y) 
    	{
    		if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
    			cout << "You pressed the left mouse button down with the cursor at"
    			     << x << " " << y << endl;
    	}
    
    Register the mouse function with
    	glutMouseFunc(mouse);
    
    and specify the function prototype.

    Compile and run the program. Activate the OpenGL window then left click on the mouse. Your program should output the mouse message. Other values the button parameter may hold are GLUT_MIDDLE_BUTTON and GLUT_RIGHT_BUTTON. The state parameter may also be set to GLUT_UP.

    To detect mouse movement you need to use glutMotionFunc or glutPassiveMotionFunc. Details are provided in the glut manual.

Deliverables: Email a your OpenGL.cpp to me and hand in a hard-copy in class next Tuesday.