tutorial 0

I assume in this tutorial that you have a passing familiarity with the Apple IDE of your choice and know how to create your NIB and project files (and indeed, that you know what all those acronyms are for). If you don't try working with one or two of the Apple project tutorials, such as the currency converter.

For those of you who are left, create a new project with the type "Cocoa Application." You need to add a framework into your project: "OpenGL.framework".

You can find this in '/System/Library/Frameworks/OpenGL.framework'

Now we're going to move on to the NIB file. Open up your NIB.

First you need to subclass NSOpenGLView. I've called my subclass kGLView, so if you want to be consistant with my project, call it that.

subclassing OpenGLView:
1. In the window "MainMenu.nib", click on the classes tab
2. Find your way to NSView->NSOpenGLView (click on it)
3. In the menu bar, click Classes->Subclass NSOpenGLView

Now, drag an NSView (conveniently called Custom View on your panel) to the window and fill the window with your new NSView.

Let's have some practice in making things via the NIB file. Create a new menu called "OpenGL options" and add the menu item "Full Screen" with the key shortcut "F". Add an action to kGLView called "fullScreen" and attatch the menu item to the action.

Create the files and add them to your project. Then close the NIB. We are done with it for now: it's time to start coding!

First we need to import and include our headers, as well as define a few constants:

// Libraries: to see what is in these, you can look // in Frameworks/OpenGL/Headers in the Groups and Files view in Project Builder
#import <OpenGL/OpenGL.h> #include <OpenGL/gl.h> #include <OpenGL/glu.h> #include <OpenGL/glext.h>
#define BITS_PER_PIXEL 32.0 #define DEPTH_SIZE 32.0 #define DEFAULT_TIME_INTERVAL 0.001

Next, we fill in the rest of the information for our class:


@interface kGLView : NSOpenGLView
{
bool FullScreenOn;
bool first;

NSWindow *FullScreenWindow;
NSWindow *StartingWindow;
NSTimer *time;
}
// Actions
- (IBAction)toggleFullScreen:(id)sender;
// Initialization
- (id)initWithFrame:(NSRect) frameRect;
// We can set this up as public or private
- (void)initGL;
@end

The first methods we must complete are initWithFrame and initGL. Let's look at initWithFrame first. In this method, we create an NSOpenGLPixelFormat which is used by the superclass, NSOpenGLView, to create the NSOpenGLContext to which we will be drawing. We return nil if anything goes wrong, which means that the system will display only an empty window. Once the view has been set up properly, we make the NSOpenGLContext in our view into the current context so that all drawing commands will go there. Finally, we call initGL which initializes all the OpenGL parameters we need to initialize.


// First, we must create an NSOpenGLPixelFormatAttribute
NSOpenGLPixelFormat *nsglFormat;
// We need to specify what kind of OpenGL context we want // For my example, I'm asking for a double buffered, accelerated if possible // context, with a depth and color buffer (so, no stencil buffer here) // The 0 at the end is an end of list marker
NSOpenGLPixelFormatAttribute attr[] = { NSOpenGLPFADoubleBuffer, NSOpenGLPFAAccelerated, NSOpenGLPFAColorSize, BITS_PER_PIXEL, NSOpenGLPFADepthSize, DEPTH_SIZE, 0 };
// This is just some code to tell Cocoa that this class wants to know // if the size of the window is changed
[self setPostsFrameChangedNotifications: YES];
// Initialize the NSOpenGLPixelFormat with some attributes described earlier
nsglFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
// Check for errors in the creation of the NSOpenGLPixelFormat // If we could not create one, return nil (the OpenGL is not initialized, and // we should send an error message to the user at this point)
if(!nsglFormat) { NSLog(@"Invalid format... terminating."); return nil; }
// Now we create the the CocoaGL instance, using our initial frame and the NSOpenGLPixelFormat
self = [super initWithFrame:frameRect pixelFormat:nsglFormat]; [nsglFormat release];
// If there was an error, we again should probably send an error message to the user
if(!self) { NSLog(@"Self not created... terminating."); return nil; }
// Now we set this context to the current context (means that its now drawable)
[[self openGLContext] makeCurrentContext];
// Finally, we call the initGL method (no need to make this method too long or complex)
[self initGL];
return self;

Next let's look at initGL. The comments make this method fairly self explanatory, and really it's just the same code as you might find in the nehe tutorials.


// Set the clear color to black. This is the color // that your background will be if you don't draw to it // If setting to black, we needn't specifically say this, // but it's good style to mention explicitly // You can set the clear color to something else later, // but it should not be between a glBegin and glEnd
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
// Set the clear depth to 1. This is the initial (default) value. // Set the depth function. This specifies when something will be drawn. // GL_LESS passes if the incoming // depth value is LESS than the current value // Turn the depth test on
glClearDepth(1.0f); glDepthFunc(GL_LESS); glEnable(GL_DEPTH_TEST);
// Set up a hint telling the computer to create the nicest (aka "costliest" or "most correct") // image it can // This hint is for the quality of color and texture mapping
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
// This hint is for antialiasing
glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);

Now we will set ourselves up to take advantage of the operating system's event loop. This is the awakeFromNIB method, which is called when your program starts. We create a timer that will call the selector drawFrame. Selector is just a way to specify that this is a pointer to a method. You could add other timers in here to time things like your frame rate, but we'll just stick with the basics for now.


time = [ [NSTimer scheduledTimerWithTimeInterval: DEFAULT_TIME_INTERVAL target:self
//go to this method whenever the time comes
selector:@selector(drawFrame) userInfo:nil repeats:YES] retain ];
// Add our timers to the EventTracking loop
[[NSRunLoop currentRunLoop] addTimer: time forMode: NSEventTrackingRunLoopMode];
// Add our timers to the ModelPanel loop
[[NSRunLoop currentRunLoop] addTimer: time forMode: NSModalPanelRunLoopMode];

Let's look at the reshape method now. We didn't have to do some of the things nehe did in his tutorial in the initGL because when we start the program we will be reshaping the view as it wakes from the NIB.


float aspect; NSSize bound = [self frame].size; aspect = bound.width / bound.height;
// change the size of the viewport to the new width and height // this controls the affine transformation of x and y from normalized device // coordinates to window coordinates (from the OpenGl 1.1 reference book, 2nd ed)
glViewport(0, 0, bound.width, bound.height); glMatrixMode(GL_PROJECTION);
// you must reload the identity before this or you'll lose your picture
glLoadIdentity(); gluPerspective(45.0f, (GLfloat)aspect, 0.1f,100.0f); glMatrixMode(GL_MODELVIEW); glLoadIdentity();

Here's our draw method. It's really only a stub here, except for the fact that we will be ensuring that this is the first responder. Being a first responder means that mouse and keyboard events will automatically come to this class in order for us to process them.


// make this control the First Responder if this is the first time to draw the frame // ACTUALLY, this little workaround is no longer needed // as it was fixed in (or possibly before) Jaguar // If you've still got an older OS, keep this here, otherwise, get rid of this if-statement
if( first ) { first = NO; if ([[NSApp keyWindow] makeFirstResponder:self] ) NSLog( @"self made first responder" ); else NSLog( @"self is not first responder"); }
// Clear the buffers!
glLoadIdentity(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// this is where you would want to draw


// flush the buffer! (send drawing to the screen)
[[self openGLContext] flushBuffer];

To actually become a first responder and get all those operating system events sent to us, we need to define two other methods. All these methods need to do is return true.


// if you want to get events sent to you, you must make yourself a first responder
- (BOOL)acceptsFirstResponder { return YES; } - (BOOL)becomeFirstResponder { return YES; }

But we don't need those events just yet: all we want to do is be able to quit and switch to a full screen mode. Unfortunately, all our work is for naught if we use apple's GLContext fullScreen method. If we use this, or the code from OmniGroups OpenGL Example code, we will not be first responders, and we will have to implement our own event queue. Not fun. However, there is a solution. Looking at the code for GLUT by Dietmar Planitzer on the Apple site, we see the solution! Don't get rid of the window, just make a new window for full screen! This way, we don't lose our event queue (or our ability to quit, which made me crash while trying to use the standard fullScreen without implementing an event queue of my own).

There are two things to consider with our full screen method. First, we may be in full screen mode already, second, we may not be. We'll look at what to do if we are not in full screen mode first.


// we need to remember where we were if we ever want to go back
StartingWindow = [NSApp keyWindow];
// NSBorderlessWindowMask means we don't see any of the things we normally see // on a window, such as scroll bars, close/resize/minimize buttons and title bars
windowStyle = NSBorderlessWindowMask; contentRect = [[NSScreen mainScreen] frame];
// Now we allocate the window
FullScreenWindow = [[NSWindow alloc] initWithContentRect:contentRect styleMask: windowStyle backing:NSBackingStoreBuffered defer: NO]; if(FullScreenWindow != nil) { [FullScreenWindow setTitle: @"myWindow"]; [FullScreenWindow setReleasedWhenClosed: YES]; [FullScreenWindow setContentView: self];
// show the window and make it the front most window of the application
[FullScreenWindow makeKeyAndOrderFront:self ];
// set the window to the uppermost level, ie, in front of the screensaver // Note that the user can no longer access windows beneath this one
[FullScreenWindow setLevel: NSScreenSaverWindowLevel - 1];
// since we created the window, we don't need to wait to make ourselves the first responder
[FullScreenWindow makeFirstResponder:self]; FullScreenOn = true; }

We're almost done: all we need to do now is consider what the program should do if we ARE already in full screen mode. This is actually quite simple: just close the full screen window, make our original window the key and frontmost window in the application, and make this the first responder again.


[FullScreenWindow close]; [StartingWindow setContentView: self]; [StartingWindow makeKeyAndOrderFront: self]; [StartingWindow makeFirstResponder: self]; FullScreenOn = false;

And that's it. The code is a bit long considering we're just drawing a black rectangle to the screen, but using this code allows us to use the nifty OSX Cocoa features that we'd like to use. It means that we don't have to set up our own event loop, and we don't have to restrict ourselves to the features that GLUT allows. All in all, a good deal if we're going to program native OSX and not worry about windows or OS9 features.


     Download the project builder files

 

return to deep cocoa / cocoagl tutorials