9 Sept 2010

Create fullscreen OpenGL application using Cocoa

When starting to develop OpenGL applications on Mac I was looking for tutorial of creating OpenGL app, that can be resized to fullscreen. I have found three or more but no one was woking for me.

So in this post I will show how to create OpenGL application using Cocoa and how to draw into my own NSOpenGLView .

Creating project

  1. In Xcode create new Cocoa appliction project

    Create project
  2. Right click on project - Add - Existing framework... and select OpenGL.framework

    Insert OpenGL framework

  3. Open Interface Builder and in the classes find NSOpenGLView, right-click and select New Subclass... Then name it for example MainOpenGLView
  4. Drag created MainOpenGLView to the MainWindow

    Drag OpenGLView
  5. Create new menu item called Fullscreen

    Create menu item Fullscreen

  6. When you create new application, application delegate is created. It is named appnameAppDelegate. Create new action for this delegate called setFullscreen.
  7. Create outlets for this delegate. One called StartingWindow (type NSWindow), FullScreenWindow (NSWindow too) and glView (type )
  8. Link Sent Actions selector in Fullscreen menu item with setFullscreen action in application delegate

    Link actions
  9. Create new link of Window to application delegate outlet StartingWindow we have created.


    Link outlet

  10. Create new link of MainOpenGLView to application delegate outlet glView we have created.Link open gl view
  11. Right-click the MainOpenGLView class and select Write Updated Class Files
  12. Save and close.

Coding

First of all I have to add generated .m and .h file to your project. It is class MainOpenGLView.

Next I import and include header files and specify some constants:

#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

Interface of the MainOpenGLView contains:
@interface MainOpenGLView : NSOpenGLView {
int colorBits, depthBits; ///< color a depth bits
}
/**
* initialization of OpenGL view
*/
- (id) initWithFrame:(NSRect)frame colorBits:(int)numColorBits
depthBits:(int)numDepthBits;
/**
* method when resized window
*/
- (void) reshape;
/**
* draw scene
*/
- (void) drawRect:(NSRect)rect;
/**
* dealloc
*/
- (void) dealloc;
@end

@interface MainOpenGLView (InternalMethods)
- (BOOL) initGL;
@end
I will start with initWithFrame method which creates CocoaGL instance and sets OpenGL context:
- (id) initWithFrame:(NSRect)frame colorBits:(int)numColorBits
depthBits:(int)numDepthBits;
{

colorBits = numColorBits;
depthBits = numDepthBits;

///< First, create an NSOpenGLPixelFormatAttribute
NSOpenGLPixelFormat *nsglFormat;

NSOpenGLPixelFormatAttribute attr[] =
{
NSOpenGLPFADoubleBuffer,
NSOpenGLPFAAccelerated,
NSOpenGLPFAColorSize, colorBits,
NSOpenGLPFADepthSize, depthBits,
0
};

[self setPostsFrameChangedNotifications: YES];

///< Next, initialize the NSOpenGLPixelFormat itself
nsglFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];

///< Check for errors in the creation of the NSOpenGLPixelFormat
if(!nsglFormat) {
NSLog(@"Invalid format... terminating.");
return nil;
}

///< Now create the the CocoaGL instance, using initial frame and the NSOpenGLPixelFormat
self = [super initWithFrame:frame 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  set this context to the current context
[[self openGLContext] makeCurrentContext];

///< Finally, call the initGL method
[self initGL];

return self;
}
The initGL method is simple. I just set some variables to default values.

- (BOOL) initGL
{
glShadeModel( GL_SMOOTH );                ///< Enable smooth shading
glClearColor( 0.0f, 0.0f, 0.0f, 0.5f );   ///< Black background
glClearDepth( 1.0f );                     ///< Depth buffer setup
glEnable( GL_DEPTH_TEST );                ///< Enable depth testing
glDepthFunc( GL_LEQUAL );                 ///< Type of depth test to do

glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST ); ///< Really nice perspective calculations

return TRUE;
}
Method to reshape when window is resized
- (void) reshape
{
NSRect sceneBounds;

[ [ self openGLContext ] update ];
sceneBounds = [ self bounds ];
///< Reset current viewport
glViewport( 0, 0, sceneBounds.size.width, sceneBounds.size.height );
glMatrixMode( GL_PROJECTION );   ///< Select the projection matrix
glLoadIdentity();                ///< and reset it
///< Calculate the aspect ratio of the view
gluPerspective( 45.0f, sceneBounds.size.width / sceneBounds.size.height,
0.1f, 100.0f );
glMatrixMode( GL_MODELVIEW );    ///< Select the modelview matrix
glLoadIdentity();                ///< and reset it
}
Method where scene is drawed:
- (void) drawRect:(NSRect)rect
{

///< Clear the screen and depth buffer
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glLoadIdentity();   ///< Reset the current modelview matrix

//you can draw here

[ [ self openGLContext ] flushBuffer ];
}
It is necessary to create these two methods to avoid taking key events.
- (BOOL)acceptsFirstResponder
{
return NO;
}
- (BOOL)becomeFirstResponder
{
return YES;
}
And dealloc:
- (void) dealloc
{
[super dealloc];
}
Second class is appnameAppDelegate.
It's interface is:
@interface appnameAppDelegate : NSResponder {
IBOutlet NSWindow *StartingWindow; ///< starting window
IBOutlet NSWindow *FullScreenWindow; ///< fullscreen window
IBOutlet MainOpenGLView *glView; ///< OpenGL view
NSTimer *renderTimer; ///< timer to refresh opengl view
BOOL FullScreenOn; ///< if fullscreen is on
}

/**
* After initialization and creation method
*/
- (void) awakeFromNib;
/**
* Method to toggle fullscreen
*/
- (IBAction) setFullScreen:(id)sender;
/**
* Deallocation method
*/
- (void) dealloc;
/**
* Key event handler
*/
- (void) keyDown:(NSEvent *)theEvent;

@end

@interface appnameAppDelegate (InternalMethods)
- (void) setupRenderTimer;
- (void) updateGLView:(NSTimer *)timer;
- (void) createFailed;
@end
In method awakeFromNib I do my initialization and set default values:
- (void) awakeFromNib
{
[NSApp setDelegate:self ];
renderTimer = nil;
[ StartingWindow makeFirstResponder:self ]; ///< to delegate events to this class
glView = [ [ MainOpenGLView alloc ] initWithFrame:[ StartingWindow frame ] ///< initialization of OpenGL view
colorBits: 32 depthBits: 32];
if( glView != nil )
{
[ StartingWindow setContentView:glView ];
[ StartingWindow makeKeyAndOrderFront:self ];
[ self setupRenderTimer ]; ///< setup timer
}
else
[ self createFailed ];
}
It is necessary to setup timer to time redrawing of the scene:
- (void) setupRenderTimer
{
NSTimeInterval timeInterval = 0.005;

renderTimer = [ [ NSTimer scheduledTimerWithTimeInterval:timeInterval
target:self
selector:@selector( updateGLView: )
userInfo:nil repeats:YES ] retain ];
[ [ NSRunLoop currentRunLoop ] addTimer:renderTimer
forMode:NSEventTrackingRunLoopMode ];
[ [ NSRunLoop currentRunLoop ] addTimer:renderTimer
forMode:NSModalPanelRunLoopMode ];
}
The method called in timer is opdateGLView:
- (void) updateGLView:(NSTimer *)timer
{
if( glView != nil )
[ glView drawRect:[ glView frame ] ]; ///< update view
}
Method to toggle fullscreen just change window style:
- (IBAction)setFullScreen:(id)sender
{

if( FullScreenOn == true ) ///< we need to go back to non-full screen
{
[FullScreenWindow close];
[StartingWindow setAcceptsMouseMovedEvents:YES];
[StartingWindow setContentView: glView];
[StartingWindow makeKeyAndOrderFront: self];
[StartingWindow makeFirstResponder: self];
FullScreenOn = false;
}
else ///< go to fullscreen
{
unsigned int windowStyle;
NSRect       contentRect;

StartingWindow = [NSApp keyWindow];
windowStyle    = NSBorderlessWindowMask;
contentRect    = [[NSScreen mainScreen] frame];
FullScreenWindow = [[NSWindow alloc] initWithContentRect:contentRect
styleMask: windowStyle backing:NSBackingStoreBuffered defer: NO];
[StartingWindow setAcceptsMouseMovedEvents:NO];
if(FullScreenWindow != nil)
{
NSLog(@"Window was created");
[FullScreenWindow setTitle: @"myWindow"];
[FullScreenWindow setReleasedWhenClosed: YES];
[FullScreenWindow setAcceptsMouseMovedEvents:YES];
[FullScreenWindow setContentView: glView];
[FullScreenWindow makeKeyAndOrderFront:self ];
[FullScreenWindow setLevel: NSScreenSaverWindowLevel - 1];
[FullScreenWindow makeFirstResponder:self];
FullScreenOn = true;
}
}
}
Just to handle OpenGL init error:
- (void) createFailed
{
NSWindow *infoWindow;

infoWindow = NSGetCriticalAlertPanel( @"Initialization failed",
@"Failed to initialize OpenGL",
@"OK", nil, nil );
[ NSApp runModalForWindow:infoWindow ];
[ infoWindow close ];
[ NSApp terminate:self ];
}
Handle key down events:
- (void) keyDown:(NSEvent *)theEvent
{
unichar unicodeKey;

unicodeKey = [ [ theEvent characters ] characterAtIndex:0 ];
switch( unicodeKey )
{
case 'q':
case 'Q':
[ NSApp terminate:self ];
break;
case 'f':
case 'F':
[self setFullScreen: self];
break;

}
}
dealloc method:
- (void) dealloc
{
[ StartingWindow release ];
[ glView release ];
if( renderTimer != nil && [ renderTimer isValid ] )
[ renderTimer invalidate ];
[super dealloc];
}

And it is done.
I hope you will find it helpful.

No comments:

Post a Comment