written by Brian Christensen
In part I of this article we learned how to create a screen saver module, how to do some simple animations with NSBezierPath, and how to set up a configure sheet. Although interesting effects can certainly be achieved using NSBezierPath, more complex animations are usually done through OpenGL. An introduction to OpenGL itself is beyond the scope of this article; however, there are a number of basic steps that have to be taken to set up a useable OpenGL environment, which are outlined in this tutorial.
Furthermore, even if you aren't interested in OpenGL, you should skip down to the section titled Prevent Symbol Clashes for some critical information on how to avoid a potential problem. Additionally, we briefly discuss how to make a screen saver module universal to get it ready for the coming onslaught of Intel-based Macs.
The first order of business is to create a new screen saver project. Name the project CoolScreenSaver (or any other name you like). Once the project window opens, select Linked Frameworks under the Frameworks and Libraries group.
Proceed to choose Project > Add to Project... and navigate your way to /System/Library/Frameworks. Add the OpenGL.framework file.
Additionally, we need to import the appropriate OpenGL headers as well. Open CoolScreenSaverView.h and add the following lines:
CoolScreenSaverView.h
#import <OpenGL/gl.h>
#import <OpenGL/glu.h>
While we have the header file open, we might as well add the couple of code bits we're going to need. Add the appropriate statements so that your file looks like this:
@interface CoolScreenSaverView : ScreenSaverView
{
NSOpenGLView *glView;
GLfloat rotation;
}
- (void)setUpOpenGL;
@end
The way OpenGL is conventionally handled when used in screen saver modules is by adding an NSOpenGLView instance as a subview of the ScreenSaverView. We will do this inside our initWithFrame:isPreview: method. Let's begin by adding the first piece of code to the aforementioned method.
CoolScreenSaverView.m
- (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
{
self = [super initWithFrame:frame isPreview:isPreview];
if (self)
{
NSOpenGLPixelFormatAttribute attributes[] = {
NSOpenGLPFAAccelerated,
NSOpenGLPFADepthSize, 16,
NSOpenGLPFAMinimumPolicy,
NSOpenGLPFAClosestPolicy,
0 };
NSOpenGLPixelFormat *format;
format = [[[NSOpenGLPixelFormat alloc]
initWithAttributes:attributes] autorelease];
At this point it is worth mentioning that one could actually pass nil as the pixel format for the NSOpenGLView. This would cause some form of default pixel format to be established for us. However, this behavior does not appear to be officially documented anywhere and, more importantly, if we want such basic features as depth testing we will need to specifiy our own format anyway.
We continue by instantiating our OpenGL view with a NSZeroRect frame size and the pixel format we just created. Then we add that view as a subview of our regular ScreenSaverView, invoke our set up method, and set the animation time interval.
glView = [[NSOpenGLView alloc] initWithFrame:NSZeroRect
pixelFormat:format];
if (!glView)
{
NSLog( @"Couldn't initialize OpenGL view." );
[self autorelease];
return nil;
}
[self addSubview:glView];
[self setUpOpenGL];
[self setAnimationTimeInterval:1/30.0];
}
return self;
}
We might as well get the dealloc method out of the way:
CoolScreenSaverView.m
- (void)dealloc
{
[glView removeFromSuperview];
[glView release];
[super dealloc];
}
Recall that in our initWithFrame:isPreview: method we made an invocation to setUpOpenGL. This is where we will perform our initial OpenGL initialization.
The first thing we need to do is set the OpenGL context. You should invoke makeCurrentContext whenever you make OpenGL calls to ensure that the proper context is ready to receive commands.
CoolScreenSaverView.m
- (void)setUpOpenGL
{
[[glView openGLContext] makeCurrentContext];
Next, we do the usual OpenGL set up:
glShadeModel( GL_SMOOTH );
glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
glClearDepth( 1.0f );
glEnable( GL_DEPTH_TEST );
glDepthFunc( GL_LEQUAL );
glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );
Finally, initialize our rotation variable. We will be using this for our animation.
rotation = 0.0f;
}
What would typically be in a reshape() function in regular OpenGL programs will go in our setFrameSize: method. setFrameSize: is invoked at least once after the view is initialized, and may be invoked again at any time if the view resizes for some reason. This is perfect for our situation and is exactly when the reshape() function is normally called as well.
CoolScreenSaverView.m
- (void)setFrameSize:(NSSize)newSize
{
[super setFrameSize:newSize];
[glView setFrameSize:newSize];
[[glView openGLContext] makeCurrentContext];
// Reshape
glViewport( 0, 0, (GLsizei)newSize.width, (GLsizei)newSize.height );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
gluPerspective( 45.0f, (GLfloat)newSize.width / (GLfloat)newSize.height,
0.1f, 100.0f );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
[[glView openGLContext] update];
}
It is generally a good idea to keep the actual drawing of our animation separate from our internal state. What this means is that drawRect: should be used to execute the necessary OpenGL commands to draw our animation to screen, and that animateOneFrame should update the state of our animation.
In the first article we didn't bother with this distinction since it was such a trivial example. However, animateOneFrame really has more to do with the screen saver engine's timer firing than an actual view update being necessary. If our drawing is kept in drawRect: then it is guaranteed that we are always updating our view at the right time. Similarly, since animateOneFrame is tied to the timer, we will always be updating our internal state at the appropriate time interval.
The animateOneFrame method being, in our case, the simpler of the two methods, we will take a crack at it first. The following code simply updates our rotation variable and then invokes setNeedsDisplay. Whenever drawing is done in drawRect: instead of in animateOneFrame, this invocation is required.
CoolScreenSaverView.m
- (void)animateOneFrame
{
// Adjust our state
rotation += 0.2f;
// Redraw
[self setNeedsDisplay:YES];
}
Since interesting OpenGL animations tend to get quite complex and learning OpenGL is beyond the scope of this article, we will limit ourselves to drawing a trivial pyramid. (This was taken straight from one of the famed NeHe OpenGL Tutorials. If you want a more detailed explanation of what's going on in the code below, you are encouraged to check out that site.)
CoolScreenSaverView.m
- (void)drawRect:(NSRect)rect
{
[super drawRect:rect];
[[glView openGLContext] makeCurrentContext];
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glLoadIdentity();
glTranslatef( -1.5f, 0.0f, -6.0f );
glRotatef( rotation, 0.0f, 1.0f, 0.0f );
glBegin( GL_TRIANGLES );
{
glColor3f( 1.0f, 0.0f, 0.0f );
glVertex3f( 0.0f, 1.0f, 0.0f );
glColor3f( 0.0f, 1.0f, 0.0f );
glVertex3f( -1.0f, -1.0f, 1.0f );
glColor3f( 0.0f, 0.0f, 1.0f );
glVertex3f( 1.0f, -1.0f, 1.0f );
glColor3f( 1.0f, 0.0f, 0.0f );
glVertex3f( 0.0f, 1.0f, 0.0f );
glColor3f( 0.0f, 0.0f, 1.0f );
glVertex3f( 1.0f, -1.0f, 1.0f );
glColor3f( 0.0f, 1.0f, 0.0f );
glVertex3f( 1.0f, -1.0f, -1.0f );
glColor3f( 1.0f, 0.0f, 0.0f );
glVertex3f( 0.0f, 1.0f, 0.0f );
glColor3f( 0.0f, 1.0f, 0.0f );
glVertex3f( 1.0f, -1.0f, -1.0f );
glColor3f( 0.0f, 0.0f, 1.0f );
glVertex3f( -1.0f, -1.0f, -1.0f );
glColor3f( 1.0f, 0.0f, 0.0f );
glVertex3f( 0.0f, 1.0f, 0.0f );
glColor3f( 0.0f, 0.0f, 1.0f );
glVertex3f( -1.0f, -1.0f, -1.0f );
glColor3f( 0.0f, 1.0f, 0.0f );
glVertex3f( -1.0f, -1.0f, 1.0f );
}
glEnd();
glFlush();
}
At this point you could build and try testing your screen saver. However, if you do, you will find with great disappointment that all you get is a blank screen. There is an interesting story going here with the way AppKit's view drawing is implemented. The drawRect: method is invoked up the view hierarchy until an opaque view is found. Given that our NSOpenGLView is a subview of the CoolScreenSaverView, and that NSOpenGLView is an opaque view (at least as of recent Mac OS X releases), the drawRect: invocation chain gets broken right there and never reaches the drawRect: we just implemented above. Hence the reason you were staring at a blank screen if you tried running your screen saver just now.
Fortunately, there is a pretty simple fix to this problem. Select File > New File... and create a new Objective-C class. Name it MyOpenGLView.m (and, of course, have it create the corresponding .h file along with it). Open MyOpenGLView.h and change it to be a subclass of NSOpenGLView.
MyOpenGLView.h
@interface MyOpenGLView : NSOpenGLView
{
}
@end
Proceed to open the corresponding .m file and add the following method:
MyOpenGLView.m
- (BOOL)isOpaque
{
return NO;
}
Now go back to our CoolScreenSaverView.h file and add the following #import directive:
CoolScreenSaverView.h
#import "MyOpenGLView.h"
Furthermore, change the type of our glView instance variable from NSOpenGLView to MyOpenGLView:
MyOpenGLView *glView;
Correspondingly, CoolScreenSaverView.m's initWithFrame:isPreview: must be modified to reflect this change. Find the relevant line and change it the following code:
CoolScreenSaverView.m
glView = [[MyOpenGLView alloc] initWithFrame:NSZeroRect pixelFormat:format];
That's it. As you can see, we are merely using a subclass of NSOpenGLView that says it's non-opaque, therefore ensuring that our own drawRect: method gets a chance to perform its magic.
Unfortunately, that's not all there is to it. Before you release a screen saver out into the wild, you need to take into account the issue of symbol clashes. Screen saver modules are nothing more than regular NSBundle plug-ins. When you open the Desktop & Screen Saver preference pane in the System Preferences application, it loads up all the individual .saver files. In short, this means that all the screen saver modules (meaning all the classes contained therein) end up sharing the same namespace. So, if two readers of this tutorial release screen savers but neglect to use unique class names (ie. they leave the names unchanged and use CoolScreenSaverView and MyOpenGLView), and a user downloads and installs both of those modules, at least one of the modules is not going to work properly. Both of them will probably end up displaying the same animation.
A good strategy for preventing this is to add a unique prefix (preferably a reverse domain name identifier) to every class in your project. So, for our example, we would call our class ComCocoaDevCentral_CoolScreenSaverView. The custom NSOpenGLView subclass would be called ComCocoaDevCentral_CoolScreenSaver_MyOpenGLView. Let's modify our above example to reflect these changes. Open the relevant files and change the appropriate lines:
CoolScreenSaverView.h
@interface ComCocoaDevCentral_CoolScreenSaverView : ScreenSaverView
CoolScreenSaverView.m
@implementation ComCocoaDevCentral_CoolScreenSaverView
Note that for MyOpenGLView we use an additional project-specific prefix as well. Since you might use the same MyOpenGLView class in several projects, you need it to be unique in each case.
MyOpenGLView.h
@interface ComCocoaDevCentral_CoolScreenSaver_MyOpenGLView : NSOpenGLView
MyOpenGLView.m
@implementation ComCocoaDevCentral_CoolScreenSaver_MyOpenGLView
Granted, this does look quite ridiculous and it makes things a bit unwieldy. Luckily, there is a forgotten gem built-in to the Objective-C compiler that will help us out. It's called @compatibility_alias. Go back to MyOpenGLView.h and add the following line to the end of the file (after the @end).
MyOpenGLView.h
@compatibility_alias MyOpenGLView ComCocoaDevCentral_CoolScreenSaver_MyOpenGLView;
This tells the compiler to replace every instance of MyOpenGLView with our absurdly long actual class name. That way, instead of referencing our glView instance variable as:
ComCocoaDevCentral_CoolScreenSaver_MyOpenGLView *glView;
We can leave our code unchanged and keep using:
MyOpenGLView *glView;
Since we changed our principal class name, we need to make the project settings reflect that change as well. Open the Targets group in the left-hand pane of the project window and double-click the CoolScreenSaver target.
Click the Properties tab and change the Principal Class field to our new class name.
Furthermore, function symbols may also cause problems. The easy way to avoid clashes here is to either mark functions with the keywords static (if you define and use the function from within the same file) or __private_extern__ (if you call the function from multiple files).
You can always check what symbols your module is exporting by using the nm tool. For example, to check on CoolScreenSaver, cd into CoolScreenSaver.saver/Contents/MacOS and run nm -g CoolScreenSaver | grep -v " U ". This will give you the following output:
brian$ nm -g CoolScreenSaver | grep -v " U "
00000000 A .objc_class_name_ComCocoaDevCentral_CoolScreenSaverView
00000000 A .objc_class_name_ComCocoaDevCentral_CoolScreenSaver_MyOpenGLView
It is a good idea to run this check every time you are ready to publicly release a screen saver.
As every Mac developer well knows by now, the Intel-powered Macs are just around the corner. Given that screen savers are plug-ins and as such will not be eligible for emulation by the Rosetta environment, it makes sense to start compiling screen savers as universal binaries as soon as possible.
The first step is to double-click the CoolScreenSaver group in the left-hand pane of the project window.
The second step is to click the Build tab and then select the Deployment configuration.
Now change the Architectures property to ppc i386.
Finally, click the General tab and set the Cross-Develop Using Target SDK option to Mac OS X 10.4 (Universal).
Close the window, set your active build configuration to Deployment, and build the project. To verify that your module is in fact a universal binary, you can cd into build/Deployment/CoolScreenSaver.saver/Contents/MacOS and run file CoolScreenSaver. You should see the following result:
brian$ file CoolScreenSaver
CoolScreenSaver: Mach-O fat file with 2 architectures
CoolScreenSaver (for architecture ppc): Mach-O bundle ppc
CoolScreenSaver (for architecture i386): Mach-O bundle i386
That's all there is to it. We now have an OpenGL screen saver free from any potential symbol clash problems, and it is ready to go on PowerPC and Intel Macs to boot.
As always, let us know what you think about the tutorial.
Acknowledgements
Special thanks to Mike Trent for sharing his insight on how to get the OpenGL drawing to work properly.
Further Reading