Wiring It All Together
Back in the Xcode project window, you see the MyOpenGLView header and implementation files. These contain the stubbed definition of the MyOpenGLView class, derived from NSOpenGLView.
Interface Builder has already wired this class to our OpenGL view in
the main window, and we now only need to add the class framework and
OpenGL rendering code.
The edited header file for the new class is trivial and simply contains a member pointer to an NSTimer that will be used for animation:
#import <Cocoa/Cocoa.h>
@interface MyOpenGLView : NSOpenGLView {
NSTimer *pTimer;
}
@end
In the implementation file, we add an idle function and implement four essential top-level OpenGL tasks that every OpenGL program needs: prepareGL for OpenGL initialization, clearGLContext for OpenGL cleanup, reshape for calculating the viewport and window bounds, and finally drawRect where we perform our rendering tasks. The entire source for our minimal OpenGL framework is given in Listing 1.
Listing 1. A Skeleton OpenGL View Class
#import "MyOpenGLView.h"
@implementation MyOpenGLView
- (void)idle:(NSTimer *)pTimer
{
[self drawRect:[self bounds]];
}
- (void) prepareOpenGL
{
pTimer = [NSTimer timerWithTimeInterval:(1.0/60.0) target:self
selector:@selector(idle:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:pTimer forMode:
NSDefaultRunLoopMode];
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
}
- (void) clearGLContext
{
// Do any OpenGL Cleanup
}
- (void) reshape
{
NSRect rect = [self bounds];
glViewport(0, 0, rect.size.width, rect.size.height);
}
- (void) drawRect:(NSRect)dirtyRect
{
glClear(GL_COLOR_BUFFER_BIT);
glFlush();
}
@end
|
The output for CocoaGL is shown in Figure 11. As you can see it’s nothing but an empty blue window, but now we have a complete framework to build on for our next example.
Double or Single Buffered?
At this point, the astute reader may be imagining the sound of screeching tires on pavement. Was that a glFlush you saw in Listing 14.1
instead of some sort of buffer swap call? Indeed it was, and this
brings us to an interesting subtlety of OpenGL on Mac OS X (as well as a
nice segue into the next section).
On Mac OS X, the entire desktop is actually OpenGL
accelerated. Anytime you are rendering with OpenGL, you are always
rendering to an off-screen buffer. A buffer swap does nothing but signal
the OS that your rendering is ready to be composited with the rest of
the desktop. You can think of the desktop compositing engine as your
front buffer. Thus, in windowed OpenGL applications (this applies to
both Cocoa and the now deprecated Carbon), all OpenGL windows are really
single buffered. Depending on how you look at it, it would also be okay
to say that all OpenGL windows are really double buffered, with the
desktop composite being the front buffer. Pick whichever one helps you
sleep best at night! In fact, if you were to execute a glDrawBuffer(GL_FRONT),
the drivers on the Mac actually would fall into a triple-buffered mode!
In reality, all OpenGL windows on the Mac should be treated as single
buffered. The buffer swap calls are really just doing a glFlush,
unless you are working with a full-screen context. For this reason (and
many others—the least of which is that you are bypassing the driver’s
own good sense as to when to flush) you should avoid glFlush in Cocoa views until you have completed all of your OpenGL rendering.
SphereWorld
We begin by creating a new Xcode project and following
all of the same steps as we did for CocoaGL, with the exception that we
call the custom view class SphereWorldView. Next, we copy the
SphereWorld.cpp file and the three texture files to the folder with our
new project. Add SphereWorld.cpp to the project and add the three
texture files to the /Resources folder in Xcode.
GLTools and Objective-C++
For this example we use our C++ library GLTools
again. We can add GLTools the same way we did for our previous
GLUT-based examples, so we do not rehash that here. However, what we do need to do here that
is new is allow our C++ code to work with Objective-C. As it turns
out...you can’t. The solution is surprisingly simple: We change our
project to use Objective-C++ instead! All you have to do is rename the
.m files to .mm, and they become Objective-C++ files, and you can use
your C++ classes within them just as if they were C++ files themselves.
As a bonus, all the Cocoa Objective-C classes work just the same as
well. Figure 12
shows our Xcode project with these changes so far. Now it’s time to
clean up the SphereWorld.cpp file and place the appropriate functions
calls in the Cocoa class.
Pruning SphereWorld
If we remove the GLUT framework code from
SphereWorld.cpp, we are left with four functions that need to be called
from our Cocoa framework: SetupRC, ShutdownRC, ChangeSize, and RenderScene.
You can probably guess where they go. Removing GLUT turns out to be
trivial. First, remove the GLUT headers from the top of the source file.
#ifdef __APPLE__
#include <glut/glut.h>
#else
#define FREEGLUT_STATIC
#include <gl/glut.h>
#endif
Next, at the end of the RenderScene
function, we perform a buffer swap and trigger a refresh. We don’t need
the buffer swap any longer as the Cocoa framework takes care of that and
has a timer that takes care of periodic refreshes. Removing these two
lines nearly completes the job.
// Do the buffer Swap
glutSwapBuffers();
// Do it again
glutPostRedisplay();
The final bit of pruning is to remove the main function in its entirety and delete the SpecialKeys callback, as we no longer are receiving keyboard input through GLUT.
Wiring It In
Calling a function in a C++ module from an Objective-C++ module is no different than normal C++ cross module programming. Listing 2
shows our SphereWorldView implementation with the appropriate functions
from SphereWorld declared and called where needed in the framework. We
have also added the Cocoa message keyDown to catch keystrokes and the acceptFirstResponder
method so that keystrokes will be handled by the OpenGL window. This
allows us to move the camera in the same manner as we did in the
GLUT-based version.
Listing 2. The Cocoa-Based SphereWorld
#include <GLTools.h>
#include <GLFrame.h>
#import "SphereWorldView.h"
void ChangeSize(int nWidth, int nHeight);
void RenderScene(void);
void ShutdownRC(void);
void SetupRC(void);
extern GLFrame cameraFrame; // Camera frame
@implementation SphereWorldView
- (void)idle:(NSTimer *)pTimer
{
[self drawRect:[self bounds]];
}
- (BOOL)acceptsFirstResponder
{
[[self window] makeFirstResponder:self];
return YES;
}
- (void)keyDown:(NSEvent*)event
{
float linear = 0.1f;
float angular = float(m3dDegToRad(5.0f));
int key = (int)[[event characters] characterAtIndex:0];
switch(key)
{
case NSUpArrowFunctionKey:
cameraFrame.MoveForward(linear);
break;
case NSDownArrowFunctionKey:
cameraFrame.MoveForward(-linear);
break;
case NSLeftArrowFunctionKey:
cameraFrame.RotateWorld(angular, 0.0f, 1.0f, 0.0f);
break;
case NSRightArrowFunctionKey:
cameraFrame.RotateWorld(-angular, 0.0f, 1.0f, 0.0f);
break;
}
}
- (void) prepareOpenGL
{
pTimer = [NSTimer timerWithTimeInterval:(1.0/60.0) target:self
selector:@selector(idle:) userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:pTimer forMode: NSDefaultRunLoopMode];
SetupRC();
}
- (void) clearGLContext
{
ShutdownRC();
}
- (void) reshape
{
NSRect rect = [self bounds];
ChangeSize(rect.size.width, rect.size.height);
}
- (void) drawRect:(NSRect)dirtyRect
{
RenderScene();
glFlush();
}
@end
|
Finding the Texture Files
Like
in our GLUT-based samples, we put the texture files for SphereWorld in
the Resources group in Xcode. Again, we must reset the current working
directory so that our file functions can find them. In our GLUT samples,
we put this in the main function. For Cocoa, we also put this in the
main function. The entire main.mm file is shown here:
#include <GLTools.h>
#import <Cocoa/Cocoa.h>
int main(int argc, char *argv[])
{
gltSetWorkingDirectory(argv[0]);
return NSApplicationMain(argc, (const char **) argv);
}
We needed to add the GLTools.h header file at the top of the file, and then the call to the by now familiar gltSetWorkingDirectory function takes care of the rest.
GLEW Versus Cocoa
There is one last thing we need to take care of.
SphereWorld makes use of GLTools, but GLTools makes use of the GLEW
library, which brings in additional OpenGL functions and extensions.
There is a funny requirement about using GLEW, which is that the GLEW
header files must be included before the actual system OpenGL header,
gl.h. With our GLUT-based programs, this was never an issue, as GLTools
brought in all the headers as needed. When we added SphereWorld.cpp to
this project, again we had no problem. But as soon as we add GLTools to
an .mm file...we get compiler errors. If you trace the errors down into
the headers, you’ll find that they are caused because glew.h was
included after gl.h. Search as you may, you will not find out where this
is occurring by inspection of the source code of the project.
As it turns out, Cocoa itself is including the OpenGL
headers. Remember, OpenGL is used everywhere on the Mac. There is a
file in the Xcode project called SphereWorld_Prefix.pch that is the
“prefix header.” It’s a precompiled header that automatically gets added
to all the Objective-C/C++ modules. What we need to do is sneak our
glew.h header file into that file. Not hard to do—it’s only four lines
long, and that’s with our change!
#ifdef __OBJC__
#include <gl/glew.h>
#import <Cocoa/Cocoa.h>
#endif
Finally, we can get a clean build, and we have a real full- (well, mostly full) featured Cocoa-based OpenGL program. Figure 14.13 shows our final masterpiece.