OpenGL: Cross Platform 3D Graphics


Highlights of this lab:

This lab is an introduction to OpenGL programming. You will see:

Exercise:

After the lab lecture, you have one week to: Online OpenGL Manual

Seminar Notes

A. General OpenGL Architecture

General OpenGL Architecture

Block Diagram of Typical Application / OpenGL / OS interactions.

OpenGL is the definitive cross-platform 3D library. Its programming style is the same on all platforms and if you modularise properly porting between them should not be too difficult. Getting to the point where you can begin writing OpenGL code is a bit trickier as the process differs from operating system to operating system. Fortunately there are cross platform libraries that can make your code extremely simple to port. Regardless of how you choose to set up : We will not go into detail on exactly how everything works in this week's lab. Instead we will focus on getting to the point where you can begin drawing.

B. Overview of an Interactive Program.

Model-View-Controller Architecture

Interactive program design entails breaking your program into three parts:

Object-oriented programming was developed, in part, to aid with modularising the components of Model-View-Architecture programs. Most user interface APIs are written in an Object Oriented language.

You will be structuring your programs to handle these three aspects of an interactive program. A 3D graphics: In your simplest programs your Document and View will be tightly coupled. You might have one or two variables set up that control a few things in your scene, and the scene itself may be described by hard coded calls made directly to the OpenGL API with only a few dynamic elements. Eventually you will learn to store the scene in separate data structures.

The Main Events

All OpenGL programs should respond to at least three events:
There are other events you will frequently handle as well. If you place your program in a resizeable window it is very important to respond to size change events. If you don't, your rendered result will be distorted when the window is resized. You may also wish to respond to mouse motions and clicks, and handle key strokes from the keyboard.

C. Building a Simple GLUT OpenGL Program with XCode

GLUT is the cross platform programming library of choice for this course. Follow these instructions to learn how to set up a GLUT OpenGL program on the Macs in the lab. You will be given the opportunity to learn how to move the same code to Windows or Linux as part of your take home exercise.

Before starting these instructions make sure you have a Mac running Mountain Lion, and that you have installed XCode 4.6.2 and the 3rd party GLEW framework. You can find links to instructions for installing the GLEW framework on a Mac, or for configuring and using Windows and Linux PCs below the lab schedule. Windows and PC users should

1. Start Xcode

You will see a Welcome screen similar to this one:

This screen is useful, so please leave the "Show this window when Xcode launches" box checked.

2. Create a New Project

The following will show you how to create a framework for the OpenGL viewing class. You will need to perform the following steps.

At this point, you have created a project to manage the OpenGL program workspace window with multiple areas, panes and bars. The following map is from the Xcode User Guide, which is available through the help menu.

The Xcode Development
      Window

Click here for a brief tour of the workspace.

Click here for Apple's Xcode Basics Help to learn even more about Xcode

You should read through the tour and try the things suggested there. At a minimum find main.cpp, read it and run it. The program doesn't do much - it's just a Hello World app.

You may not see all the panes or areas yet. They can be shown or hidden. Some may reveal themselves when you perform certain actions.

3. Link to OpenGL and OpenGL Support Libraries

In MacOS X, libraries are stored as packages called Frameworks. A framework encapsulates related headers, objects and documentation.

And that's it. By adding frameworks you have added include and link paths for the desired libraries with ease.

4. The Code

The XCode Project Wizard creates the minimum code necessary to run a program. For our project we are creating a GLUT application and GLUT will handle setting up windows. Later you will make a native Mac or Windows application. The basic app will have enough code to show you an empty window.

You have already set up your project with the necessary libraries, so replace the code in your main.cpp with the following:

Basic Glut Program: main.cpp
// CS315 GLUT Template

#include "Angel.h"

//--- Headers, Prototypes and Global Variables -------------------------------

void init ( void );
void display ( void );
void reshape (int w, int h);
void keyboard ( unsigned char key, int x, int y );

//--- Main -------------------------------------------------------------------

int main( int argc, char **argv )
{
	glutInit( &argc, argv );
	glutInitWindowSize( 512, 512 );

#ifdef __APPLE__
	glutInitDisplayMode( GLUT_3_2_CORE_PROFILE | GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH );
#else
	// If you are using freeglut, the next two lines will check if 
	// the code is truly 3.2. Otherwise, comment them out
	glutInitContextVersion( 3, 2 );
	glutInitContextProfile( GLUT_CORE_PROFILE );
	glutInitDisplayMode( GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH );

#endif

   glutCreateWindow( "OpenGL Template" );
   glewExperimental = GL_TRUE;
	glewInit();

	init();

	//Register callback functions
	glutDisplayFunc( display );
	glutKeyboardFunc( keyboard );
	glutReshapeFunc(reshape);


	glutMainLoop();
	return 0;
}

//--- Support functions ------------------------------------------------------

// OpenGL initialization
void init()
{	
   //Put Call to OpenGL Initializing function here
   

}

//----------------------------------------------------------------------------

void display( void )
{
	// Put Call to main Drawing function here
    
	glutSwapBuffers();
}



void reshape (int w, int h)
{
	//Put Call to Size Changing function Here

}

//----------------------------------------------------------------------------

void keyboard( unsigned char key, int x, int y )
{
	switch( key ) {
		case 033:  // Escape key
		case 'q': case 'Q':
			exit( EXIT_SUCCESS );
			break;
	}
}
Take a look at this code. There is a main function and four support functions:
  1. main: Here we use
    1. GLUT functions to set up a request for an OpenGL Core Profile OpenGL rendering context with certain other attributes, then create a window with that rendering context. This must be done before any other GLUT, GLEW or OpenGL commands, or they will have no rendering context to act on and will crash.
    2. GLEW to access advanced OpenGL capabilities from that rendering context
    3. init to configure the context after it is set up.
    4. glut*Func functions to register callbacks for important events - display requests, keyboard presses, and window reshaping
    5. glutMainLoop to hand control over to GLUT.
  2. init: This function is intended to perform first time setup. It should be used to set an initial OpenGL state. It is a good idea to know what the OpenGL defaults are, and to never assume that they will be honoured by the driver provider. Always ask for what you intend to use. In a GLUT program there is no init event, you simply do your init work once you have a valid rendering context. Other windowing APIs may have a specific init event. Both MFC and Cocoa do.
  3. display: this function will be called whenever the window needs to be redrawn. It will be called at least once because a redraw event is sent when the window is first displayed.
  4. reshape: this function will be called whenever the window is resized. It will be called at least once because a resize event is sent when the window is first displayed.
  5. keyboard: this function handles input from keys that correspond to the ASCII table. If you want more advanced functions
This program is a GLUT + GLEW template. You will add code to the init, display and reshape functions later.

Unfortunately, this program is not complete. You can compile and run the program, and you could even do some simple drawing, but most of the the OpenGL code we use in this class also depends on some external files.

Tip: add an existing file by saving it into your project folder next to main.cpp, then in XCode select the group with main.cpp in it, right click, and choose Add Files to "project name".... Files already in your project are greyed out for your convenience.

You can now safely build and run your program. Note, however, that there are no OpenGL calls in the template. You will get a raw OpenGL window - it won't even be cleared for you so it may be filled with junk from unitialized memory or from the Mac OS X compositor which also uses OpenGL.

To run one of Dr. Angel's examples, go to his Windows code and replace your main.cpp with one of his .cpp files, then replace your shaders with the shaders that match it.

D. Setting Up the OpenGL Window and Drawing to It

OpenGL is set up and controlled by GLUT in this program. You will use GLUT throughout the semester for your assignments. If you need to know the details of a GLUT function mentioned in the notes, or if you want to browse the available functions to learn new features, you can look at the official online GLUT manual.

The GLUT functions in the template take care of the basics of setting up an OpenGL drawing environment by:

  1. Creating and configuring a Device Context - this is mostly done with glutInitDisplayMode(), but glutInit() may also be involved.
  2. Establishing a Rendering Context - this is done by getting a window and attaching the device context. In this program we set the window's size and position with glutInitWindowSize, glutInitWindowPosition, and glutCreateWindow.
  3. Connecting callback functions that will handle events. Note that there is little or no code in these functions.
  4. Starting the event loop.
In this section you will add code to appropriate callback functions to:
  1. Configure the Rendering Context for our window shape and anticipated scene.
  2. Draw a scene.
  3. Reconfigure the Rendering Context when the window size changes
  4. Clean up when the program is done. (This step is implementation dependent and may not be possible on all platforms.)

You will learn more about how to do these things throughout the semester. For now, copy the following code to appropriate places in main.cpp and refer to the in-line comments for details.

New Code: Headers, Prototypes and Global Variables Section
// uofrGraphics library for CS315 Labs
// defines urgl object which can draw some simple shapes
/////////
#include "uofrGraphics.h"

// Window Event helpers
/////////

//Init
void InitializeOpenGL();

//Resize
bool ChangeSize(int w, int h);

//Draw
void Draw( void );
void PreRenderScene( void );
void RenderStockScene( void );
void RenderScene( void );
void drawSolidSphere(GLfloat radius, GLint slices, GLint stacks);
void drawSolidCube(GLfloat size);


//OpenGL State Management
////////
GLuint program1;  //Shader
GLint uColor;     //Shader color input
GLint uLightPosition;//Shader light position input
GLint mvIndex;    //Shader positioning input
GLint projIndex;     //Shader projection input
mat4 p, mv;       //Local projection and positioning variables


// Scene Related Functions and Variables
////////

//Model Control Variables
GLfloat rotY = 0;    //rotate model around y axis
GLfloat rotX = 0;    //rotate model around x axis
New Code: Initialize Rendering Context
//Function: InitializeOpenGL
//Purpose:
//    Put OpenGL into a useful state for the intended drawing.
//    In this one we:
//        - choose a background color
//        - set up depth testing (Requires GL_DEPTH in Pixel Format)
//        - turn on the lights
//        - set up simplified material lighting properties
//        - set an initial camera position
void InitializeOpenGL()
{  
   //Set up shader
   program1 = InitShader( "vshader.glsl", "fshader.glsl" );

   glUseProgram( program1 );

   //Get locations of transformation matrices from shader
   mvIndex = glGetUniformLocation(program1, "mv");
   projIndex = glGetUniformLocation(program1, "p");

   //Get locations of lighting uniforms from shader
   uLightPosition = glGetUniformLocation(program1, "lightPosition");
   uColor = glGetUniformLocation(program1, "uColor");

   //Set default lighting and material properties in shader.
   glUniform4f(uLightPosition, 0.0f, 0.0f, 10.0f, 0.0f);
   glUniform3f(uColor, 1.0f, 1.0f, 1.0f);

   //Configure urgl object in uofrGraphics library
   urgl.connectShader(program1, "vPosition", "vNormal", NULL);

   glEnable(GL_DEPTH_TEST);
}
New Code: Resize Window
// Function: ChangeSize
// Purpose:
//     Tell OpenGL how to deal with a new window size.
// Arguments:
//     int w, h: new width and height of the window, respectively.
bool ChangeSize(int w, int h)
{
   GLfloat aspect_ratio; // width/height ratio

   //Make sure the window size is valid
   if ( 0 >= w || 0 >= h )
   {
      return false;
   }

   // tell OpenGL to render to whole window area
   glViewport(0, 0, (GLsizei) w, (GLsizei) h);

   // compute the aspect ratio
   // this is used to prevent the picture from distorting when
   // the window is resized
   aspect_ratio = (GLdouble)w/(GLdouble)h;

   // calculate a new projection matrix
   p = Perspective(50.0f, aspect_ratio,  0.5f, 20.0f);

   // send the projection to the shader
   glUniformMatrix4fv(projIndex, 1, GL_TRUE, p);

   return true;
}
New Code: Draw Scene
// Function: Draw
// Purpose:
//     Control drawing of the scene. To be called whenever the window
//     needs redrawing.
void Draw()
{
    // Clear the screen and the depth buffer
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    
    PreRenderScene();
    RenderStockScene();
    RenderScene();
}


// Use this to perform view transforms or other tasks
// that will affect both stock scene and detail scene
void PreRenderScene()
{
    // select a default viewing transformation
    // of a 20 degree rotation about the X axis
    // then a -5 unit transformation along Z
    mv = mat4();
    mv *= Translate( 0.0f, 0.0f, -5.0f );
    mv *= RotateX ( 20.0f );
    
    //Allow variable controlled rotation around local x and y axes.
    mv *= RotateX(rotX);
    mv *= RotateY(rotY);
}


// Function: RenderStockScene
// Purpose:
//     Draw a stock scene that looks like a
//     black and white checkerboard
void RenderStockScene()
{
    const GLfloat delta = 0.5f;
    
    // define four vertices that make up a square.
    vec4 v1(  0.0f, 0.0f,  0.0f, 1.0f);
    vec4 v2(  0.0f, 0.0f, delta, 1.0f);
    vec4 v3( delta, 0.0f, delta, 1.0f);
    vec4 v4( delta, 0.0f,  0.0f, 1.0f);
    
    
    int color = 0;
    
    // define the two colors
    vec3 color1( 0.9f,  0.9f,  0.9f );
    vec3 color2( 0.05f, 0.05f, 0.05f );
    
    mat4 placementX = mv;
    mat4 placementZ;
    placementX *= Translate( -10.0f * delta, 0.0f, -10.0f * delta );
    for ( int x = -10 ; x <= 10 ; x++ )
    {
        placementZ = placementX;
        for ( int z = -10 ; z <= 10 ; z++ )
        {
            glUniform3fv(uColor, 1, (color++)%2 ? color1 : color2 );
            glUniformMatrix4fv( mvIndex, 1, GL_TRUE, placementZ );
            urgl.drawQuad(v1, v2, v3, v4);
            placementZ *= Translate( 0.0f, 0.0f, delta );
        }
        placementX *= Translate( delta, 0.0f, 0.0f );
        
    }
}

// Function: RenderScene
// Purpose:
//     Your playground. Code additional scene details here.
void RenderScene()
{
    // draw a red sphere inside a light blue cube
    
    // Set the drawing color to red
    // Arguments are Red, Green, Blue
    glUniform3f(uColor, 1.0f, 0.0f, 0.0f);
    
    // Move the "drawing space" up by the sphere's radius
    // so the sphere is on top of the checkerboard
    // mv is a transformation matrix. It accumulates transformations through
    // right side matrix multiplication.
    mv *= Translate( 0.0f, 0.5f, 0.0f);
    
    // Rotate drawing space by 90 degrees around X so the sphere's poles
    // are vertical
    mv *= RotateX( 90.0f );

    //Send the transformation matrix to the shader
    glUniformMatrix4fv(mvIndex, 1, GL_TRUE, mv);
    
    // Draw a sphere.
    // Arguments are Radius, Slices, Stacks
    // Sphere is centered around current origin.
    urgl.drawSolidSphere(0.5f, 20, 20);
    
    
    // when we rotated the sphere earlier, we rotated drawing space
    // and created a new "frame"
    // to move the cube up or down we now have to refer to the z-axis
    mv *= Translate(0.0f, 0.0f, 0.5f);
    
    //Send the transformation matrix to the shader
    glUniformMatrix4fv(mvIndex, 1, GL_TRUE, mv);
    
    // set the drawing color to light blue
    glUniform3f(uColor, 0.5f, 0.5f, 1.0f);
    
    // Draw the cube.
    // Argument refers to length of side of cube.
    // Cube is centered around current origin.
    urgl.drawSolidCube(1.0f);
}

Once you have added all this, go back to main.cpp, add code to the GLUT event callback functions to call the appropriate helpers, then run your program to see what is displayed.

It should look like this:

Good luck!

If your program runs correctly, you might be wondering how exactly the picture is drawn. That is, you might want to understand how each function works. Well, you do not have to worry too much about this in the first lab. Over the rest of the semester we will go through these subjects one-by-one in detail. Of course, we will learn a lot of more advanced functions and features.

Tip: Only after you have called the initialization function will you know whether your shaders are being copied. If you get complaints about vshader.glsl or fshader.glsl in the output pane, go back and fix it.

E. Backup Your Project before You Leave

It's a good idea to backup your project before you leave. You can copy your work to a USB drive or your Hercules account. If you use an IDE such as XCode or Visual Studio you must exit it before doing a backup or you will be unable to copy some of your files. Remember to save all the documents before quitting.

Copy the entire project folder. This folder can be a bit large. To save space you might want to delete build products and support files as they can be rather large. Don't worry - you will get it back the next time you compile your program. All the source files should be small enough to put on a floppy disk.

Note: Cleaning project folders:
Note: Opening Existing projects
To open an existing project:

The project should be up and ready to go, just as you left it.


EXERCISE

To be completed and submitted to URCourses by the first midnight one week after the lab.

Play with OpenGL

Try to make some simple changes to the scene. For help with the function calls refer to the Official Online OpenGL Manual.

Your result should look like this:

You may add other objects to the scene if you wish, but please keep the blue cube and red sphere visible.

Think About Event Programming

Learn About the CS315 Libraries

You probably have a clear idea of what OpenGL is, but you may be confused about all the libraries used in this lab's program. Take a moment to learn about them.

Cross Platform OpenGL

Now that you have created a GLUT based OpenGL program on a Mac, demonstrate that OpenGL+GLUT+GLEW is a cross platform API by compiling and running your program with a different operating system. You will find Visual Studio specific set up information the CS 315 lab schedule page. The brave may attempt to do this on Linux or other UNIX-like OS.

Deliverables

Package your project folders and written answers into one zip file. Submit the .zip file to URCourses. Include the following:
  1. Working OpenGL XCode project with modified box/sphere position. It should spin when you press any mouse button move the mouse. It should not spin continuously.
  2. Working GLUT based project on a second platform.
  3. Document with written answers to the "Think About Event Programming" questions.
  4. Document with written answers to: "Learn About the CS315 Libraries"

References