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 must respond to at least three events:
There is a fourth event 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.

C. Building a Simple OpenGL Window with Carbon

Mac OS X offers three ways to create a native OpenGL window. For now we will use a C based API called Carbon to set up a Window, and Apple Graphics Library or AGL to configure it for OpenGL. The other options are to use the Objective-C based Cocoa and NSOpenGL, or the very low level C based Core Graphics Library or CGL. CGL is the way to get high performance full screen applications.

1. Start XCode

You will see a Welcome screen similar to this one:

Feel free to uncheck the "Show at launch" option.

2. Create a New Project

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

At this point, we have created a project for the OpenGL program we are to develop. Your project's components are organized into a single window that has five main regions.

The Xcode Development Window

  1. Toolbar: collects important functions. By far the most important one to you right now is Build and Go. The toolbar is customizable.
  2. Groups and Files List: allows you to explore the various components of your project. Of most importance to you at the moment is the first group, the Project Group, and the Errors and Warnings group which you can click to see a complete list of errors from your last build. If there are errors simply single click on the error to jump to it.
  3. Detail View: enables you to see the class hierarchies and file folders of the project or solution that you are working on
  4. Editor: where you write and edit your programs.
  5. Status Bar: watch here for messages as you perform tasks such as editing and compiling.
At first the Detail View shows all the files that make up your project. You can narrow this down to code files by clicking on the Source folder in the Groups and Files panel. 

At this point, you might want to compile the application just to make sure there are no errors in your building process. Simply click the Build and Go button. If it works you will get a simple program with one window that does nothing. 

You can learn more about XCode and the XCode workflow from Apple by visiting this page.

3. Hook Into the Main Events

The XCode Project Wizard creates the minimum code necessary to put a window and its UI components up on the screen. It performs only the default behaviours. If we wish to do anything special in response to an event we have to tell the application what events we are interested in, figure out what events we have received, and perform an appropriate action for the event.

Take a look in main.c. From the first few lines you can deduce that there are four functions:

  1. AppEventHandler: when the main program receives an event of a type it is actively listening for, that event is sent here. Currently the only behavior it has that is not default is the response to the "File | New" command from the menu - it will create a new instance of the main window. The function will return an Status code indicating one of the following:
  2. HandleNew: this function reads a design file, called a nib or .xib file, for the main application window and displays it. We will add code here to listen for the four main events identified earlier. We will also use this function to configure OpenGL.
  3. WindowEventHandler: HandleNew uses this function to process its events. You will use switch-case statements to identify and handle the four main events.
  4. main: here we set up the application menu, hook into application-wide events, create the program's one and only window, and start listening for events.
Edit function: main
...
    // Create a new window. A full-fledged application would do this from an AppleEvent
    // handler for kAEOpenApplication. 
    HandleNew();

    //Code to get Resources directory as working directory
    //(default for glut apps in OS X which is the app type you will use
    // for most labs)
    char *parentdir, *c;
    parentdir = strdup(argv[0]);
    c = parentdir;

    while (*c != '\0') c++; //goto end
    while (*c !='/') c--; //goto last subdirectory
    *c = '\0'; //chop off binary name
    //make actual switch to Resources directory
    chdir(parentdir);
    chdir("../Resources");
    free(parentdir);
// Run the event loop RunApplicationEventLoop(); ...
Edit function: HandleNew
static OSStatus
HandleNew()
{
    OSStatus err;
    WindowRef window;
    static const EventTypeSpec kWindowEvents[] =
    {
        { kEventClassWindow, kEventWindowDrawContent }, // draw events
        { kEventClassWindow, kEventWindowClose }, // window closed
        { kEventClassWindow, kEventWindowBoundsChanged },// window resized
{ kEventClassCommand, kEventCommandProcess } }; // Create a window. "MainWindow" is the name of the window object. This name is set in // InterfaceBuilder when the nib is created. //err = CreateWindowFromNib( sNibRef, CFSTR("MainWindow"), &window ); require_noerr( err, CantCreateWindow ); // Install a command handler on the window. We don't use this handler yet, but nearly all // Carbon apps will need to handle commands, so this saves everyone a little typing. InstallWindowEventHandler( window, GetWindowEventHandlerUPP(), GetEventTypeCount( kWindowEvents ), kWindowEvents, window, NULL ); // Position new windows in a staggered arrangement on the main screen RepositionWindow( window, NULL, kWindowCascadeOnMainScreen ); // The window was created hidden, so show it ShowWindow( window ); CantCreateWindow: return err; }
Edit function: WindowEventHandler
static OSStatus
WindowEventHandler( EventHandlerCallRef inCaller, EventRef inEvent, void* inRefcon )
{
    OSStatus err = eventNotHandledErr;
    static WindowRef window;
    static Rect windowRect;
switch ( GetEventClass( inEvent ) ) { case kEventClassCommand: { HICommandExtended cmd; verify_noerr( GetEventParameter( inEvent, kEventParamDirectObject, typeHICommand, NULL, sizeof( cmd ), NULL, &cmd ) ); switch ( GetEventKind( inEvent ) ) { case kEventCommandProcess: switch ( cmd.commandID ) { // Add your own command-handling cases here default: break; } break; } break; }
 
        case kEventClassWindow:
        {
            GetEventParameter(inEvent, kEventParamDirectObject, typeWindowRef, 
                              NULL, sizeof(WindowRef), NULL, &window);
            switch (GetEventKind(inEvent))
            {
                case kEventWindowClose:
                    printf("closing\n");
                    break;
                case kEventWindowBoundsChanged:
                    printf("resizing\n");
                    InvalWindowRect(window, &windowRect);
                    break;
                case kEventWindowDrawContent:
                    printf("drawing\n");
                    break;
            }
        } 
default: break; } return err; }

4. Compile Your Application

You may be excited to try your program. It may take a little while to compile and link the program. Watch the status bar to see how things are going.

If this is your first Windowed program feel free to do a happy dance if it works.

For now watch your code print to the console as it catches events:

5. Take Over Drawing From the Compositor.

Notice there were no drawing messages. This is because some of the window's instructions are stored in a nib (or xib) file. The nib file describes the menus and windows in a Mac application. It includes instructions on where a window's messages are routed by default. By default draw messages and drawing updates are sent to a default routine that does the sensible thing to draw buttons, lists and other UI components. We are going to bypass that mechanism and take full control. This means that if you use Interface Builder to add things to the main window program, you will not see them without writing calls to the widget display system (not covered here).

Click on the Resources folder and double click main.xib. This will start Interface Builder.

Once Interface Builder has loaded, click on the MainWindow icon.

Press Shift + Command + i to open the Inspector. Find window template attributes, check Updates, and uncheck Compositing.

Save your changes and exit Interface Builder.

6. Adding OpenGL Libraries

So far what we have created is all standard Carbon stuff. It has nothing to do with OpenGL yet. To connect to OpenGL, we have do a few things first. Then we may use functions from the OpenGL library.

First, we need to insert the following lines into the headers section:

    #include <OpenGL/gl.h>
    #include <OpenGL/glu.h>
    #include <Agl/agl.h>
    #include <stdbool.h>

The next step is to add the OpenGL 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 all in one step.

D. Setting Up the OpenGL Window and Drawing to It

Now that the OpenGL libraries have been added to our project, we are ready add some real OpenGL code. For this part of the lab you will be working entirely in main.c. The code you will add will handle events that:

  1. Set up OpenGL by:
    1. Creating and configuring a Device Context (Apple calls them Pixel Formats - but they also include device information)
    2. Establishing a Rendering Context
    3. Configuring 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

You will learn more about all this in Lab 2. For now here are the changes you need to make to the two files. The parts you will work with are highlighted in red.

New Code: Prototypes and Global Variables Section
    #include <Carbon/Carbon.h>
    #include <OpenGL/gl.h>
    #include <OpenGL/glu.h>
    #include <Agl/agl.h>
    #include <stdbool.h>


    // OpenGL Setup Functions and Variables
    AGLContext openGLContext = NULL;
    bool InitializeOpenGL(WindowRef windowRef);
    bool SetupHardwareContext(WindowRef windowRef);

    // Scene Related Functions and Variables
    bool PreRenderScene( void );
    void RenderStockScene( void );
    bool RenderScene( void );

    // Window Event helpers
    void Draw( void );
    bool ChangeSize(int w, int h);
    bool SetupViewport( int w, int h );
    bool SetupViewingFrustum( GLdouble aspect_ratio );
    bool SetupViewingTransform( void );
    void ShutdownGL();

    //Model Control Variables
    GLfloat sceneRotate; //rotate camera up or down
    GLfloat rotY = 0;    //rotate model around y axis
    GLfloat rotX = 0;    //rotate model around x axis
static OSStatus AppEventHandler( EventHandlerCallRef inCaller, EventRef inEvent, void* inRefcon ); static OSStatus HandleNew(); static OSStatus WindowEventHandler( EventHandlerCallRef inCaller, EventRef inEvent, void* inRefcon );
Edit function: HandleNew
...

    InstallWindowEventHandler( window, GetWindowEventHandlerUPP(),
                               GetEventTypeCount( kWindowEvents ), kWindowEvents,
                               window, NULL );

    //Configure the Carbon window up for OpenGL rendering and set an 
    //initial OpenGL state.
    InitializeOpenGL(window);

// Position new windows in a staggered arrangement on the main screen RepositionWindow( window, NULL, kWindowCascadeOnMainScreen ); ...
Edit function: WindowEventHandler
...

    case kEventClassWindow:
    {
        GetEventParameter(inEvent, kEventParamDirectObject, typeWindowRef, 
                          NULL, sizeof(WindowRef), NULL, &window);
        switch (GetEventKind(inEvent))
        {
            case kEventWindowClose:
                printf("closing\n");
                ShutdownGL();
                QuitApplicationEventLoop();
break; case kEventWindowBoundsChanged: printf("resizing\n");

                //Get the new window dimensions from Carbon
                GetWindowPortBounds(window, &windowRect);
                int width = windowRect.right-windowRect.left;
                int height = windowRect.bottom-windowRect.top;

                //Tell OpenGL the new window dimensions.
                ChangeSize(width, height);

                //Tell Carbon that the OpenGL drawable area has
                //changed size and should be redrawn
                aglUpdateContext(openGLContext);
                InvalWindowRect(window, &windowRect);
break; case kEventWindowDrawContent: printf("drawing\n");

                //Do some drawing with OpenGL
                Draw();

                //Tell Carbon to swap draw and display buffers
                aglSwapBuffers(openGLContext);

                //Tell Carbon the whole window should be redrawn
                GetWindowPortBounds(window, &windowRect);
                InvalWindowRect(window, &windowRect);
break; } } ...
New Code: Mixed Carbon and OpenGL Setup Functions for the End of main.c
//Function: InitializeOpenGL
//Purpose:
//    Set a Carbon window up for OpenGL rendering and set Initial OpenGL
//    state.
//    Returns false if OpenGL could not be set up, true otherwise.
//Parameters:
//    windowRef - reference to a Carbon window

bool InitializeOpenGL(WindowRef windowRef)
{
    if (!SetupHardwareContext(windowRef))
    {
        fprintf(stderr,"Could not initial OpenGL");
        return false;
    }
	
    // specify black as clear color
    glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
    // specify the back of the buffer as clear depth
    glClearDepth( 1.0f );
    // enable depth testing
    glEnable( GL_DEPTH_TEST );
	
    sceneRotate = 20.0;
	
    return true;
}

//Function: SetupHardwareContext
//Purpose: 
//    Use Carbon calls to set a Carbon window as an OpenGL
//    rendering context and make it current.
//Parameters:
//    windowRef - reference to a Carbon window.
bool SetupHardwareContext(WindowRef windowRef)
{
    // Get display ID to use for a mask
    // In this case the main display as configured via System Preferences
    CGDirectDisplayID displayID = CGMainDisplayID(); 
    CGOpenGLDisplayMask openGLDisplayMask = CGDisplayIDToOpenGLDisplayMask(displayID);

    //Select a pixel format with desired attributes
    static GLint glAttributes[] = {
        AGL_DOUBLEBUFFER,   //Double Buffered
        AGL_RGBA,           //Four-component color
        AGL_DEPTH_SIZE, 24, //24-bit pixel depth is the minimum
        AGL_DISPLAY_MASK, 1,//On the primary screen
        AGL_NONE };         //End of pixel format requirements

    glAttributes[5] = openGLDisplayMask;
    AGLPixelFormat openGLFormat = aglCreatePixelFormat(glAttributes);
    if(openGLFormat == NULL)
    {
        fprintf(stderr, "No compatible Pixel Contexts\n");
        return false;
    }

    //Could be code in here to get and describe full actual pixel format,
    //But you should be asking for *everything* you need. Make *no* assumptions.
    
    // Create the context
    openGLContext = aglCreateContext(openGLFormat, NULL);
    if(openGLContext == NULL)
    {
        fprintf(stderr, "Could not create a pixel context.\n");
        return false; // Handle failure gracefully please!
    }
    
    // No longer needed
    aglDestroyPixelFormat(openGLFormat);
    
    // Point to window and make it the current OpenGL target
    aglSetWindowRef(openGLContext, windowRef);
    aglSetCurrentContext(openGLContext);
    
    //OpenGL is up ... go load up geometry and stuff
    return true;
}

New Code: Platform Agnostic OpenGL Drawing and Resizing Functions for the End of main.c

// select the full client area
bool SetupViewport( int w, int h )
{
    glViewport(0, 0, w, h);
    	
    return true;
}

bool SetupViewingFrustum( GLdouble aspect_ratio )
{
    // select a default viewing volume
    gluPerspective( 40.0f, aspect_ratio, .1f, 20.0f );
    return true;
}


bool SetupViewingTransform()
{
    // select a default viewing transformation
    // of a 20 degree rotation about the X axis
    // then a -5 unit transformation along Z
    glTranslatef( 0.0f, 0.0f, -5.0f );
    glRotatef( sceneRotate, 1.0f, 0.0f, 0.0f );
    return true;
}

//Use this to perform additional view transforms, or other
//tasks that will affect both stock scene and detail scene
bool PreRenderScene()
{
    glRotatef(rotX, 1, 0, 0);
    glRotatef(rotY, 0, 1, 0);
    
    return true;
}

//Code additional scene details here
bool RenderScene()
{
    // draw a red wire sphere inside a
    // light blue cone (cylinder with one radius set to 0)
    
    //Quadrics are part of the glu library
    //they make parameterized shapes
    //They are generated by a combination of a style defined in a
    //Quadric object (here qobj) and a quadric generating function
    GLUquadricObj *qobj;
    qobj = gluNewQuadric();
    gluQuadricDrawStyle(qobj, GLU_LINE); /* wireframe */
    
    // Move "drawing space" up by the wire sphere's radius
    glTranslatef(0, 0.5, 0);
    
    // Rotate drawing space so the sphere's poles are veritcal
    glRotatef( 90.0f, 1.0f, 0.0f, 0.0f ); 
    // set the sphere's color to red
    glColor3f( 1.0f, 0.0f, 0.0f ); 
    // call the quadric sphere generator
    gluSphere( qobj, 0.5, 20, 10);
    
    // set the cone's color to light blue
    glColor3f( 0.5f, 0.5f, 1.0f );
    
    // when we rotated the sphere earlier, we rotated drawing space
    // and created a new "frame"
    // to move the cylinder up or down we now have to refer to the z-axis
    glTranslatef(0,0,-1.0);
    gluCylinder(qobj, 0.0, 1.0f, 1.5f, 20, 5);
    	
    return true;
}

// Draws a stock scene consisting of a square surface that looks like a
// black and white checkerboard
void RenderStockScene()
{
    // define all vertices X Y Z
    GLfloat v0[3], v1[3], v2[3], v3[3], delta;
    int color = 0;
    	
    delta = 0.5f;
    	
    // define the two colors
    GLfloat color1[3] = { 0.9f, 0.9f, 0.9f };
    GLfloat color2[3] = { 0.05f, 0.05f, 0.05f };
    	
    v0[1] = v1[1] = v2[1] = v3[1] = 0.0f;
    	
    glBegin( GL_QUADS );
    	
    for ( int x = -5 ; x <= 5 ; x++ )
    {
        for ( int z = -5 ; z <= 5 ; z++ )
        {
            glColor3fv( (color++)%2 ? color1 : color2 );
            	
            v0[0] = 0.0f+delta*z;
            v0[2] = 0.0f+delta*x;
            
            v1[0] = v0[0]+delta;
            v1[2] = v0[2];
            	
            v2[0] = v0[0]+delta;
            v2[2] = v0[2]+delta;
            	
            v3[0] = v0[0];
            v3[2] = v0[2]+delta;
            	
            glVertex3fv( v0 );
            glVertex3fv( v1 );
            glVertex3fv( v2 );
            glVertex3fv( v3 );
        }
    }
    glEnd();
}


// Draw window event helper
void Draw()
{
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    	
    glPushMatrix();
    PreRenderScene();
    
    glPushMatrix();
    RenderStockScene();
    glPopMatrix();
    	
    glPushMatrix();
    RenderScene();
    glPopMatrix();
    
    glPopMatrix();
    glFinish();
} 

//Resize window event helper
bool ChangeSize(int w, int h)
{
    GLdouble aspect_ratio; // width/height ratio
    	
    if ( 0 >= w || 0 >= h )
    {
        return false;
    }
    	
    
    SetupViewport( w, h );
    	
    // select the projection matrix and clear it
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    	
    // compute the aspect ratio
    // this will keep all dimension scales equal
    aspect_ratio = (GLdouble)w/(GLdouble)h;
    	
    // select the viewing volume
    SetupViewingFrustum( aspect_ratio );
    	
    // switch back to the modelview matrix
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    	
    // now perform any viewing transformations
    SetupViewingTransform();
    return true;
}
New Code: Carbon Specific OpenGL Shutdown Functions for the End of main.c
//Quit application event helper
void ShutdownGL()
{

    //Unbind GL Context
    aglSetCurrentContext(NULL);
    	
    //Remove Drawable
    aglSetWindowRef(openGLContext, NULL);
    aglDestroyContext(openGLContext);
}

Once you have made all these changes to your program, you can compile and run it again. 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.

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 the Desktop or other folder in your home directory as these are located on the network and are backed up nightly, or to a USB drive. 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 OpenGL folder to a floppy disk. This folder can be a bit large. To save space you might want to delete the Build folder where your code is compiled as it can be rather large. Besides, 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: Opening Existing projects
To open an existing project, on the hard drive, you change to that directory  and double click on the project file which ends with the extension .xcodeproj
The project should be up and ready to go, just as you left it.

You can access your Mac Home folder on the Mac Server via ssh or scp. You can use this to save work you do in other labs. The address for the mac server is: mcserver.cs.uregina.ca. Log in with your UDML username and password.

*Warning: if you save to a folder in your Home directory, make a copy to the XDATA folder before working with it. If you don't, you may experience serious performance degradation while you work.

*You have 4GB of networked storage space on the Mac server. The extra space will disappear at the end of this semester.


EXERCISE

To be completed and sent to your lab instructor within one week of the start of the lab.

Play with OpenGL

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

Think about Window Programming

Learn About GLUT

In the next section you will built a GLUT based program. Before you begin:

Experiment with GLUT

Now that you have created a Carbon based OpenGL program, create one using GLUT.

Deliverables

Zip your project folders and written answers into one zip file. Provide a README text file with a summary of what you handed in an how to make it run. Include the following:
  1. Working Carbon based XCode project with OpenGL. It should spin when you resize the window, not continuously.
  2. Written answer to: What is an event?
  3. Written answer to: What is GLUT?
  4. GLUT based XCode and VS2008 projects that show the same initial picture as the Carbon example.
  5. BONUS: complete one or both Extras.

Extras

Experiment with SDL (For Multimedia Enthusiats/Gamers)

SDL is a cross platform API focused on gaming. It includes basic hooks for 2D and 3D video, audio, input, networking and event handling. If you want to write a crossplatform multimedia app or video game this a good place to start.

Come Full Circle

In this lab you learned how to build a Mac OS X native OpenGL app. You also learned how to create cross platform OpenGL apps. It is also helpful to know how to write a Windows native app.

References