Jan 02 Mac OS X Cocoa
Volume Number: 18 (2002)
Issue Number: 01
Column Tag: Mac OS X
Using OpenGL with Cocoa
by David A. Trevas
The easiest way to begin working with stunning 3D imagery.
If you want to get started with the advanced 3D graphics of OpenGL, Cocoa is the easiest way to get you going. As an application programming interface (API) for graphics, OpenGL is the same on all of the platforms on which it works, so the many fine references (books, web sites, etc.) for it apply to everyone. One significant barrier the new user faces is simply getting to the point where they can make the standard OpenGL code work on their machine. In other systems, one is usually compelled to write dozens of lines of cryptic code to get to the point of drawing a simple line. This article will show you that with Interface Builder, you can be diving into OpenGL with very few lines of code.
There are two parts in the body of this article, each of which introduces not only OpenGL concepts, but also reinforces some of the fundamentals of Cocoa programming. The first part will show you how to get OpenGL up and running and highlights the benefits of using subclasses. This opens world of OpenGL for you, so if you have a bee in your bonnet about using it right away, you may choose to read the other parts later.
The loyal readers who press on (and did I mention that they are also good-looking and highly intelligent?) to the second part are rewarded with an overview of drawing in three dimensions and a perspective on drawing in perspective. You will also learn about primitives like cubes, spheres and cylinders (but, unfortunately, not Neanderthal men as the name may have led you to believe.) While the geometric concepts presented in the overview may or may not be confusing to you, the effects that various values have on actual graphics almost certainly require some hands-on experimentation. Coincidentally, the programming segment of this part involves adding a bunch of sliders and a controller object to the view object used earlier (and modified here) to adjust geometric parameters and see the results immediately (aka, real-time). This part demonstrates the Cocoa outlet and target/action concepts.
I hope all of this ado has gotten you psyched up to dive into OpenGL! So, crack your knuckles, warm up your mouse and without further ado, let's go!
Part 1: Basic OpenGL Drawing in Cocoa
One big selling point about using Object-Oriented Programming in a framework like Cocoa is that you can build on to existing work, instead of having to start from scratch. If you find a class you like and just want to change or add a method (that's like a function for you C folks) , make a subclass of that object and go to town! In this case, we'll make a subclass of NSOpenGLView (in the Cocoa Bible, Genesis 18:20–23 states: NSObject begat NSResponder begat NSView begat NSOpenGLView) so you get all of the functionality of that entire lineage "for free" and you simply add your own drawRect: method. You end up writing only the code that sets your object from its predecessors without having to get up to your elbows in device contexts, memory requests and callbacks.
You are going to start a new project using Project Builder, but it is Interface Builder that really helps you get a feel for what is going on.
Open up Project Builder, go to the File menu and select New Project... , select Cocoa Application. Give it a name like OpenGLWithCocoa and, if you like, choose a folder in which your project folder will reside.
On the left-hand side of the window that opens up, you should see a group of tiny folder icons with the names Classes, Other Sources, Resources and so forth. Click once on the disclosure triangle just to the left of Resources and you will see an item called MainMenu.nib. Double-click on it.
Wait a moment as Interface Builder fires up and locate a small window called MainMenu.nib that has tabs called Instances, Classes, Images and Sounds.
Click on the Classes tab in the MainMenu.nib window. (Warning: Avert your eyes from the Classes menu at the top of the screen!) Scroll up and down the gray list of classes until you find NSOpenGLView and click on it once.
With this next step, you are going to create your own version of NSOpenGLView with which you can add data and add or change its methods as you see fit. At long last, the time has come to go to the forbidden Classes menu and choose Subclass. It proposes the acceptable, but somewhat corny name of MyOpenGLView (the use of the prefix "my" in programming exercises is quite a cliché). You can change the name if you like, but I'll keep it.
Since you won't be adding any actions or outlets (these terms aren't important yet) to this class, you are ready to generate the code for it. Again, return to the Classes menu and choose Create Files... and select Choose in the ensuing dialog. If you look back into Project Builder, you will see two new files there, MyOpenGLView.h and MyOpenGLView.m. One annoying problem is that it's hard to predict where those files are going to be placed in that left-hand column of Project Builder. Where do you think they belong? Hint: you used the Classes tab and the Classes menu. It makes no difference where in the project folder they are, it's just a matter of style.
Now it is time to set up the window for this program. Select the window called Window and then go to the Tool menu and choose Show Info (the keyboard shortcut for this is Command (Apple)-Shift-I and is one that is definitely worth learning). We call this the design window. At the top of this info box (also called an Inspector) is a pull-down menu showing the various categories of information you can inspect. From that pull-down, choose Size and set the width (w) of the window to 640 and the height (h) to 542. (You may need to choose the Width/Height option in the second pull-down in the Content Rect box.)
You only need to add one item to this window: an OpenGL View. One of the windows in Interface Builder is called Cocoa - (something) Views Palette (if you don't see it, choose Tools>Palettes>Palettes to make it visible.) Use the horizontal scroll bar to get to the Graphics Views Palette. The symbol looks like graph paper with a learning curve on it — steeply sloping upwards at first and flattening out later. Mercifully, they cut it off before you see that as time goes on, you start forgetting things. It's pretty unmistakable once you find it since it has enormous QuickTime and OpenGL logos in it.
Drag the OpenGL logo into your 640x542 window and move it toward the upper left hand corner. When blue dotted lines appear, your object is positioned according to the Aqua interface guidelines. Drop it when you have two dotted lines showing — one above and one to the left of your OpenGL view. Go to the Inspector (Cmd-Shift-I) and choose the Size from the top pull-down. Make this view 600 wide and 300 high. Now you are going to do something amazing. Go back to that pull-down and choose Custom Class this time and simply click on MyOpenGLView.
This connects the code you generated earlier with what is going to be drawn in that box. Isn't that easy? What is amazing about that. Well, this is far simpler than what your brethren on other platforms or environments have to go through to get started with OpenGL.
Save your work in Interface Builder and go back to Project Builder.
Now that I've shown you how to get to OpenGL programming, I'm sure you're going to want to do some. Come back next month for another thrilling episode of..., just kidding, we do it right now.
OpenGL is an API that contains hundreds of functions. The original and most fundamental of these are in the Graphics Library and can be found in the header file, gl.h. Over the years, certain utilities were developed to simplify operations or consolidate frequently used sequences of commands. These were called GL Utilities and they are in the header file called glu.h (pronounced "glue"). For instance, since OpenGL was designed for 3D graphics, it took a little extra work to create 2D views. There is a simple function gluOrtho2D() that makes a 2D view, whereas the slightly more complex GL function glOrtho() is made to handle three-dimensional spaces. And where the GL Utilities leave off, there's a GL Utility Toolkit to do even higher-level work. You may feel that there is a glut of things to learn, but that should only serve to remind you that this last group of functions is in the header file, glut.h.
As far as putting these things in your program is concerned, keep in mind that glut.h calls glu.h and gl.h, so one call (#import or #include) to glut.h gets you the other two as well. It is easy to tell which header file an OpenGL function belongs to because the names begin with gl-, glu- and glut-.
Add the following code to the MyOpenGLView.m file (the code that was created automatically will be shown in italics.
Listing 1. Basic OpenGL Drawing
#import "MyOpenGLView.h"
#import <GLUT/glut.h>
@implementation MyOpenGLView
- (void)drawRect: (NSRect) theRect
{
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0., 2., 0., 1.);
glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(0.0, 0.0, 1.0);
glLineWidth(1.0);
glBegin(GL_LINE_LOOP);
glVertex2f(0.05, 0.05);
glVertex2f(1.95, 0.05);
glVertex2f(1.80, 0.85);
glVertex2f(0.20, 0.85);
glEnd();
glFlush();
}
@end
First, notice the command to import GLUT/glut.h. The first occurrence of the word GLUT (in ALL CAPS) is the name of the framework in which the header file, the second glut, resides. This is important because had you jumped the gun and tried to build and run this project, it probably would have failed. You need to link to the GLUT framework first. This is accomplished by going to the Project menu in Project Builder and choosing Add Frameworks..., from there navigate to /System/Library/Frameworks/GLUT.framework and choose it.
Next, you see that I start an instance method called drawRect: that has no return value (signified by the word void) and takes one argument, an NSRect structure called theRect. Where did I come up with this and why didn't I have to declare this method in MyOpenGLView.h? The method drawRect: is from NSView which is the "grandparent" of the current class. The methods of all of the ancestors of MyOpenGLView are implicitly included in the class and can be modified (or overridden in OOP jargon) and do not require a place in the MyOpenGLView.h file.
The code which you are putting into the drawRect: method is OpenGL code which is cryptic until you understand the logic behind it. The line-by-line descriptions that follow not only tell what each line does, but explains the OpenGL fundamentals associated with them.
The first three commands glMatrixMode(GL_PROJECTION), glLoadIdentity(), gluOrtho2D()are really at the heart of what OpenGL does: taking your abstract geometric figures and deciding what pixels on your monitor to color to faithfully represent them. To do this requires a lot of matrix operations and OpenGL does an excellent job of shielding mathophobes from the horrors of linear algebra. The first command states that the matrices that follow describe the projection, that is, what are the dimensions of the box that contain the things you want to see in your window. The second command loads an identity matrix which is a transformation that does nothing — what comes in goes out the same way — but it is needed as the starting point for matrices that really do things.
That third command, gluOrtho2D(), answers a question some of you may have been asking: What units does OpenGL use? Meters? Inches? Angstroms? Parsecs? It can be anything you like. The first two parameters represent the left and right sides or the horizontal, x-axis, and the last two represent the bottom and top or the vertical, y-axis.
Usually, you will want your view to have the same proportions as the window in which you draw it. Otherwise, squares and circles will appear as rectangles and ellipses and if you put photos of people in, you'll get some funhouse mirror effects. We will later see a way to use theRect to ensure that the ratio of width to height (called the aspect ratio) is always correct, but at this point it is sufficient to remember that we made the NSOpenGLView object 600 pixels wide by 300 pixels high to achieve an aspect ratio of 2. Now you can recognize that the values in gluOrtho2D() imply that the x-coordinates range from 0.0 to 2.0 and that the y-coordinates go from 0.0 to 1.0.
The next two lines, glClearColor() and glClear()tell you that you have to lay out a clean "sheet of paper" before you start drawing on it. The first three parameters in glClearColor() are the fractions of red, green and blue to be used as the background color. With all three maxed out at 1.0, the background will be white. If they were all set to the minimum value of 0.0, the background would be black. You can experiment with combinations of values that range between zero and one to get any other visible color. The fourth value is alpha and it tells how translucent the background color is. Zero would be completely transparent and one would be opaque. This value is only relevant if blending is enabled. The GL_COLOR_BUFFER_BIT argument in glClear() is needed because there are different buffers that are used for other graphics operations.
The statements glColor3f() and glLineWidth() are concrete examples of what it means when OpenGL is said to be a state machine and glColor3f() also illustrates a helpful naming scheme that appears frequently. In some drawing schemes, you'd draw a shape and then change its properties like color, line width, fill color. With a state machine, you set up the state (or mode) of these properties and then you create the geometry. Here you are going to be establishing the state of the color (no red, no green and maximum blue, so it's pure blue) and the state of the line width to one pixel.
Earlier we discussed how OpenGL uses prefixes to identify where a function is from, but it also has a system of suffixes to describe the number and type of arguments that functions take (provided that multiple versions are available). The first part of the suffix is a digit that is the number of parameters to be considered. Next is one or two lower-case letters signifying whether the input should be an integer (i), floating-point number (f), double precision number (d) or an unsigned long integer (ul), to name a few. Finally, there may be a "v" at the end to request that you put these parameters into an array (or vector). Thus, glColor3f() uses three floating-point numbers to specify the current color.
We now turn to the part of the program where we actually draw after expending all that effort to describe the desired state. This segment shows the use of the glBegin()/glEnd() method for drawing points, lines and polygons. Here we use the mode GL_LINE_LOOP to start at the first vertex we specify and continue in connect-the-dot fashion and then this mode automatically returns us to the first vertex at the end. GL_LINE_STRIP works exactly the same way only it doesn't connect the first and last points and GL_LINES makes disconnected lines between pairs of vertices. If you were to use glBegin() in a GL_LINES mode and use an odd number of vertices, the last one would simply be ignored. Other modes include GL_POINTS, GL_TRIANGLES, GL_QUADS (quadrangles) and GL_POLYGON. There are a few others dealing with alternative ways to create multiple triangles and quadrangles. You will learn as you progress into OpenGL that there are relatively few commands you can use inside a glBegin()/glEnd() construct. You'll see that glBegin()/glEnd() is so much like an if, switch or for control structure in C, that it is typical to see the intervening statements indented.
You then see the four vertices (two-dimensional, floating point numbers) describing a trapezoid starting from the lower left and proceeding counterclockwise until the upper left vertex and allowing GL_LINE_LOOP to close it up.
Finally, glFlush() flushes the drawing from the buffer (which is where you've actually been drawing) to the screen.
In Project Builder, you should select the hammer icon to build the application and after a bunch of messages fly by in the console pane, the words "Build succeeded" should appear in the status bar at the bottom. If they don't, try to fix the errors (I hope you remembered to use Tools>Add Frameworks... with GLUT.framework). Click the Run icon, the one that looks like a monochrome computer monitor (personally, I'd prefer the icon to be a Tangerine iMac, but who asked me?), and you should see your window showing the outline of a blue trapezoid on a white background (Figure 1). You could have combined the instructions described in this paragraph to a single keyboard shortcut, Cmd-R, which is Build and Run from the Build menu.
Figure 1. Output from Basic OpenGL Program
From here you are free to play with the program. Just remember to hit Cmd-R after each change. You could change the red, green and blue values in glClearColor() and glColor3f() to achieve different background and drawing colors. You could adjust the line width. Maybe you'd like to try different drawing modes in glBegin(), like GL_POINTS. Why not try playing with the number and value of vertices and the values in gluOrtho2D() to see how they interact? If you draw a square with four vertices and create a projection in gluOrtho2D() that does not have the same aspect ratio as the 2:1 window in which you are drawing, you will see how the shape can appear squashed.
By the way, if you've been paying too much attention, you may be asking, "Shouldn't gluOrtho2D() take two double-precision numbers as parameters because of the suffix system you mentioned earlier?" Good question and the answer is no. First, gluOrtho2D() is a GL Utilities (glu) routine, not a GL one, so it is not subject to the GL naming convention. Secondly, it is a capital D and the convention uses lower-case letters, so "2D" means "two-dimensional."
I hope you found this program interesting. While it was short and did little, you should have gotten a taste of the fundamentals of OpenGL on which we can build. Your are probably ready to move on into 3D and interacting with the graphics in a far more immediate way than repeatedly hitting Build and Run (Cmd-R) (Of course, computers are so fast now that the build and run sequence happens so quickly that it is "immediate" compared to the old days when a 10-line BASIC program could take two hours to run on a crowded mainframe. That two hours being the time from when you put the punch cards into the feeder until the "sysop" placed your green-and-white, continuous-feed output on the shelf. Frames per second ?! Harumph!)
Part 2: Interactive OpenGL Drawing
We are now going to extend the program to include a 3D view in perspective and give you the opportunity to have the observer move around, change his or her point of focus, twiddle perspective parameters and move an object around. These parameters will be adjusted with a matrix of sliders you will put underneath the OpenGLView window. To our project, we will add a controller object to take the data from the moving sliders and pass it to the picture.
First, let us discuss some geometry. Earlier on we saw that the x-coordinates went from left to right on the screen and the y-coordinates went from bottom to top. In 3D, you have a third coordinate, z, that is perpendicular to the xy-plane. Which way do positive values of z go: out from the screen or in toward the back (and if you have one of those nice flat-panel displays, you'll realize that there's not much room there!)? The answer is determined by the right-hand rule, which states that if you curl the fingers of your right hand and hold them so they point from the positive x-axis to the positive y-axis, then the positive z-axis will be in the direction of your outstretched thumb (if you are double-jointed, all bets are off). Thus, the positive z-coordinates come out of the screen.
This coordinate system is the one used by the modelview matrix. While the projection matrix tells you about what is visible onscreen, the modelview coordinate system is a reference frame for the objects and locations used to describe the scene you are drawing. Before you start drawing things, OpenGL wants to know where you are and what you're looking at. For this purpose, there is a nice function called gluLookAt() to describe all that in one shot. While the nine parameters seem intimidating at first, once you realize that it is just two points and a direction, it then feels quite manageable. The first three arguments represent your location in the scene and is called the eye point. The second three numbers is the point you are focusing on and is called the look point. The final trio tells OpenGL what "up" is (or as the youngsters are so fond of saying nowadays, "Whassssuupp!") Usually "up" is the direction of the positive y-axis (0, 1, 0) unless you are creating something like a flight simulator that can do barrel rolls (please let me off before you attempt that.)
Varying the eye point location is like flying around with your eyes fixed on some point. Varying the look point is like standing still and turning or nodding your head.
While it is more typical to introduce orthographic projection before perspective, I'm going to jump straight to perspective since it produces more natural-looking pictures. In orthographic projection, a cube that is two inches on each side appears the same whether its z-coordinate is right near the screen or a hundred feet away. In contrast, the distant cube in perspective would appear as a speck and the near one would be huge in comparison. Orthographic is usually only used in programs like computer-aided design (CAD) systems and can be quite disorienting. If you are going to create you own CAD package, I'm sure you have the ability to learn glOrtho() on your own time.
A perspective projection can be achieved using two different functions: the more flexible glFrustum() or the simpler gluPerspective(). You may be asking, "What the frust is a frustum?!" I could draw a picture, but most of you are probably already carrying at least one picture of a frustum. Look on the back of the U.S. $1 bill. See the pyramid with the top lopped off? That's a frustum! That eye on the top is placed perfectly for this discussion; maybe our founding fathers anticipated 3D computer graphics. We're not going to use glFrustum(), but the first four parameters describe the dimensions of the top, flat summit of pyramid, the fifth is the short distance between the eye and the top of the pyramid and the sixth is the distance from the eye to the pyramid's base. Most people find gluPerspective() easier to use. First, you choose the vertical field-of-view angle. That would be like the angle between opposite sides of the pyramid. Next, you choose the aspect ratio (width/height) of the near plane of the frustum since pyramids usually have square cross-sections, the aspect ratio would be one. Finally, you enter the near plane and far plane (pyramid base) distances and, if you are a Who fan and you make that last number very large, you'll be able to "see for miles and miles."
While you are digesting those abstract geometric concepts, let's outline our strategy for the programming task of this part. We are going to create a matrix of sliders so you can get the feel of the effect that many of the above-mentioned parameters have on the appearance of a drawing. You will also be able to manipulate the location of an object in the picture and spin it as well. We will create a controller object that takes the user input as an array and sends it to the MyOpenGLView object to create the picture.
Remembering that arrays in C and Objective-C start with zero, here is the table of parameters and their ranges that we are going to play and their starting values with:
Index | Property | Range | Starting Value |
0 | Eye x | -100. to 100. | 80. |
1 | Eye y | -100. to 100. | 80. |
2 | Eye z | -100. to 100. | 80. |
3 | Look x | -100. to 100. | 0. |
4 | Look y | -100. to 100. | 0. |
5 | Look z | -100. to 100. | 0. |
6 | View angle | 10¼ to 170¼ | 60. |
7 | Near plane | 5. to 50. | 10. |
8 | Far plane | 55. to 350. | 200. |
9 | Object x | -100. to 100. | 0. |
10 | Object z | -100. to 100. | 0. |
11 | Object angle | -720¼ to 720¼ | 0. |
Table 1. Adjustable parameters in Interactive Program.
We're keeping the object's y-coordinate constant at zero so it will appear to be sliding along on a table and we've chosen to let the object spin four revolutions so you can see how smoothly OpenGL handles the task.
Reopen your project, OpenGLWithCocoa, if necessary. Go to the Resources folder in Project Builder and double-click on the MainMenu.nib file to open up Interface Builder. Pick the Classes tab in the MainMenu.nib window and scroll to the top and click once on NSObject to select it. Now go to the Classes menu, choose Subclass and notice that the name MyNSObject is in reverse video (white letters on a black background) . Simply type the name OpenGLCocoaController and it will replace the default name.
Go to the Classes menu and pick Add Outlet and call it theView (an homage to Barbara Walters) and this will be the chosen destination of the data array we discussed above. Return to the Classes menu and this time choose Add Action and call it updateData which should be self-explanatory.
To reduce the number of times you have to go all the way up to the Classes menu, you can pick the little, gray electric socket to the right of the name, OpenGLCocoaController, to see the outlets for the class. While the word Outlets is selected (or any of the named outlets beneath it), hit Return once to generate a new one. Type in a new name and press Return again to enter that name into the list of outlets. Similarly, you can choose the gray crosshairs next to the outlet to access the Actions and the same procedures are in effect. Note that an action has to end with a colon, but if you omit it, Interface Builder will automatically supply it. This method is preferred to using the Classes menu when you are adding many actions and outlets.
You may be wondering how twelve pieces of data can be handled by a single action. If you are a C programmer, especially if you've done some Mac Toolbox work, you are probably anticipating a big ol' switch statement. Well, you are going to have a pleasant surprise.
Click once on OpenGLCocoaController and go again to the Classes menu and choose Create Files... and accept the proposed location. Return to the Classes menu and this time choose Instantiate to create an object of your new controller class.
Drag a slider from the Cocoa-More Views Palette and place it under the MyOpenGLView box close to the left side of the main window. Open the Inspector (Cmd-Shift-I) and note that the title bar reads NSSlider Info.
Set the Minimum to -100, leave the Maximum at 100 and set the Current value to zero. Choose the Small button if you like. Using the Size pulldown, set the slider width to 180.
One slider down, 11 to go! You may be saying, "Ugh! This is going to get awfully tedious." But, have no fear, I'm going to show you a real eye-popper. If your slider is selected, it is surrounded by little gray dots. Hold down the Option (Alt) key, click and hold on the lower right dot and drag down toward the right. Like a scene from The Sorcerer's Apprentice, sliders start to multiply out of thin air! Option-drag (as it's called) until you have a matrix of sliders 4 high and 3 wide. Return your gaze to the title bar of the Inspector and notice that it has changed to NSMatrix Info. Use the pulldown at the top of the Inspector to go back to Attributes. In the Spacing box, set x to 30 and y to 40. It may then become necessary to drag the whole matrix back into position.
Figure 2. Controls of Interactive Drawing Program.
Next you will connect this group of sliders to the code that gives it meaning. First, ensure that the Instances tab of the MainMenu.nib window is selected. Hold down the Control key, click in the matrix of sliders and drag the L-shaped line to the OpenGLCocoaController object in the MainMenu.nib window and release the mouse button. A Connections dialog will come up and you should pick updateData: and then hit the Connect button. While you're there, hold down Control again and this time drag from the OpenGLCocoaController object to the MyOpenGLView box in the design window. This time, you should connect with your only choice, theView.
Recall that the table of parameters that these sliders affect sometimes have ranges that are not from -100 to 100. Double-click on the View Angle slider(left column, third row). If the matrix containing the slider becomes highlighted, double-click again until the slider itself is covered by a small, gray, translucent rectangle about the size of the slider. Open up the Inspector and choose Attributes from the top pulldown. Set the Minimum to 10, the Maximum to 170 and the Current value to 60. Repeat this process for Near Plane (center column, third row), Far Plane (right column, third row) and Object Angle (bottom, right-hand corner). Adjust Current values of the Eye sliders (top row) to the values specified in Table 1.
Attaching the labels is easy, too. Simply go to the Cocoa Views Palette and drag the Informational Text label to the space just below the upper-left slider. Use Cmd-Shift-I to open the Inspector and click on the alignment icon for centering (a vertical bar with two arrows converging on it). Double-click the text of the actual label (not the text in the Inspector) and type Eye X and then Return.
Exploit the matrix feature again by Option-dragging the lower-right corner dot of the label until you get a matrix of labels that is also 4 high and 3 wide. Open the Inspector again and use 108 for the spacing in the x-direction and repeat 40 as the y-spacing.
Double-click the text of the label in the center of the top row and type it as it appears in Figure 2. Use the Tab key to move to the next field (it will automatically go from the last column of one row to the first column of the next row) and you will find labeling easy compared to other typical methods.
You've just put a matrix of labels that don't do anything over a matrix of sliders that does everything. Being on top, the label matrix will receive your mouse actions first and do nothing with them. You need to click once on one of the labels, like Eye X, so the box of gray dots that surrounds the labels (but not the larger slider matrix) appears. Go to the Layout menu and select Send to Back. Now the slider matrix will receive the mouse actions and things will happen as expected.
We've completed the interface, so now you can save it and quit Interface Builder. Returning to Project Builder, you might notice that the two files, OpenGLCocoaController.h and OpenGLCocoaController.m , are not in the Classes folder in the left-hand column. Please put them there. (For the sake of running the program, it doesn't matter where various files are, but it is a matter of good programming style. The last thing I want on my conscience are legions of programmers cranking out ugly programs!)
Before we enter "Code first, ask questions later" mode, I do want to tell you about several of the OpenGL commands you'll be using. The first is glTranslate*(x, y, z). The asterisk can either be an "f" or a "d" depending on whether you want floating-point precision or double precision. While using "d" makes the command look like a past-tense verb, double precision is overkill, since it will not make any perceptible difference in the drawings. With such a small program, the performance penalty you may realize by using double precision over floating-point will also be imperceptible, but it gets you started early in making these kind of design decisions when you move on to larger projects. In any case, glTranslate*(x, y, z) moves the origin of the current drawing coordinate system from its current location to the point (x, y, z). This point then becomes the origin of the new current coordinate system.
You can rotate the current coordinate system with glRotate*(angle, axis_x, axis_y, axis_z) by angle degrees (elegantly avoiding the tedious radians) counter-clockwise around a line going from the current coordinate system's origin to the point (axis_x, axis_y, axis_z).
The command, glScale*(xFactor, yFactor, zFactor), stretches the coordinate system by the three separate factors you specify. Typically, one thinks of scaling as being proportional and when xFactor, yFactor and zFactor are all equal, this is true, but as you will see, boxes of any shape can be made by using different scaling factors and creating a cube in the new coordinate system.
The way the translation, rotation and scaling functions work is by transforming the modelview matrix. Recalling that we are using a state machine, you know that we first set the transformation matrix, then draw the item. Since transformations accumulate, it might occur to you that you may want to store a matrix (particularly the original identity matrix). You manage various matrices in OpenGL by using a "stack." Imagine those spring-loaded cafeteria tray dispensers (my apologies if the food you carried on them brings back traumatic memories) where you can "push" a tray onto the stack to store it, and "pop" one off when you want to use it (ugh!) A historical note: the cafeteria tray stack is the inspiration for the computer science term.
When you want to depart from the identity matrix use glPushMatrix() to store it, do what you gotta do with transformations, draw your object and invoke glPopMatrix() to return to the neutral identity matrix. You can do successive translations without too much difficulty, but when you bring scaling and rotation in, things can get really confusing. Like the glBegin()/glEnd() construct, the lines between a glPushMatrix() and its complementary glPopMatrix() are sometimes indented.
First, look at OpenGLCocoaController.h and make the following modifications:
Listing 2. Interface for Controller Object
#import
#import "MyOpenGLView.h"
@interface OpenGLCocoaController : NSObject
{
IBOutlet MyOpenGLView *theView;
NSMutableArray *dataArray;
}
- (IBAction)updateData:(NSMatrix *)sender;
@end
The first line you have to add imports the MyOpenGLView object. If you didn't do this, when you referred to theView, the compiler will spit up when you try to build it.
The next change you are being asked to make is actually driven by a philosophical stance advocated by Apple in the book, Learning Cocoa. The original line reads IBOutlet id theView where IBOutlet means nothing (it's just a note to Interface Builder) and id means "the address of (or pointer to) anything." Using id to allow the maximum freedom to programmers is one of the key features of the Objective-C language. However, when you know exactly what class an outlet is (or what a method uses or returns), you code becomes easier to read and understand when you use the class name explicitly instead of using the generic id. Do this when you won't lose anything by limiting yourself to a particular class. Here take id out and put in MyOpenGLView * before theView, since you that is exactly what theView is going to be. Here, the asterisk means that theView is really the address of (or a pointer to) a MyOpenGLView object. While you are at it, since you know that the sender to updateData: is going to be an NSMatrix, put NSMatrix * where the id used to be.
The last change with for this interface file is to add a pointer to an NSMutableArray called dataArray. "Mutable" means "changeable" and an NSMutableArray is a subclass of the NSArray class, so it inherits all of NSArray's methods and variables. An NSArray is like a CD-R — you write to it once, but then the data is read-only. In other words, after one writing, the data on a CD-R is immutable. An NSMutableArray is like a CD-RW — you are free to add, remove and rearrange data at will even after you write to it. One note to C programmers: Just because an NSArray is said to be immutable does not mean that it is equivalent to the C language const type specifier. To alter an NSArray, you can wipe it out and put a new one in its place, much like you can swap the CD in the tray anytime you want. Since you are going to want to ability to change individual members of the array and any time while the program is running, you will use an NSMutableArray.
MyOpenGLView.h requires only a little attention.
Listing 3. Improved Drawing Interface
#import <Cocoa/Cocoa.h>
@interface MyOpenGLView : NSOpenGLView
{
NSArray *shugArray;
}
- (void)setShugArray:(NSArray *)inArray;
@end
Since you see that the OpenGLCocoaController is sending out an array of data to theView, then the MyOpenGLView needs to be able to receive it. I called it shugArray to test what generation you are from. Did the name conjure up images of the boxers Robinson or Leonard or of the rock group? It's rare to have fun with variable names, so enjoy it when you can!
In the MyOpenGLView object, we are only going to need to be able to read the members of the array, not change them or add members. Thus, an NSArray is more appropriate here (also, no one would get the wordplay of shugMutableArray!) You may be bothered that the controller object sends out an NSMutableArray, but the view object receives an NSArray. It looks like a type mismatch, but a rule of Objective-C is that a descendant of a class can normally be used wherever that class itself is specified. Since NSMutableArray is subclass of NSArray, this substitution is OK.
The setShugArray: method demonstrates the well-established Objective-C way of using accessor methods to look at or alter variables of an object, not by directly handling them from just anywhere in the code. The naming convention for the accessor method to get the variable's value is simply the variable's name itself. For the accessor method to adjust the value, place the prefix "set" before the variable's name and capitalize the name's first letter.
Now that you've dealt with the interfaces, you need to complete the implementations. Here's what OpenGLCocoaController.m ought to look like:
Listing 4. Initializing and maintaining drawing data.
#import "OpenGLCocoaController.h"
@implementation OpenGLCocoaController
- (void)awakeFromNib
{
dataArray = [[NSMutableArray alloc] init];
[dataArray addObject:[NSNumber numberWithFloat: 80.0]];
[dataArray addObject:[NSNumber numberWithFloat: 80.0]];
[dataArray addObject:[NSNumber numberWithFloat: 80.0]];
[dataArray addObject:[NSNumber numberWithFloat: 0.0]];
[dataArray addObject:[NSNumber numberWithFloat: 0.0]];
[dataArray addObject:[NSNumber numberWithFloat: 0.0]];
[dataArray addObject:[NSNumber numberWithFloat: 60.0]];
[dataArray addObject:[NSNumber numberWithFloat: 10.0]];
[dataArray addObject:[NSNumber numberWithFloat: 200.0]];
[dataArray addObject:[NSNumber numberWithFloat: 0.0]];
[dataArray addObject:[NSNumber numberWithFloat: 0.0]];
[dataArray addObject:[NSNumber numberWithFloat: 0.0]];
[theView setShugArray:dataArray];
[theView setNeedsDisplay:YES];
}
- (IBAction)updateData:(NSMatrix *)sender
{
int i, j, currItem;
int numCols = [sender numberOfColumns];
float x;
i = [sender selectedColumn]; //Get matrix position
j = [sender selectedRow];
currItem = j*numCols+i; //Convert to array position
x = [[sender cellAtRow:j column:i] floatValue];
[dataArray replaceObjectAtIndex:currItem
withObject:[NSNumber numberWithFloat:x]];
[theView setShugArray:dataArray];
[theView setNeedsDisplay:YES];
}
@end
The first line you enter, the awakeFromNib command, may seem like it fell from the clear blue sky or is yet another stupid mistake by the author. I assure you it is neither. Since the awakeFromNib method is associated with the parent of OpenGLCocoaController, there is no need to declare it in the interface (or header) file. The awakeFromNib method is executed automatically as soon as the nib file (things you create with Interface Builder) is activated. When you open a nib file, the awakeFromNib methods for all objects that define it are carried out. Maybe a clearer name would be doThisWhenNibAwakes.
The first thing that happens when the nib awakes is that a NSMutableArray is allocated and initialized. Forgetting statements like these and trying to use something without creating it is a common novice error (and I'm not naming names). What makes them so tricky is that the error messages that Project Builder generates when this problem occurs are not very helpful. You'll see things like SIGBUS, SIGSEGV and EXC_BAD_ACCESS, but nothing about what variable caused it.
The next 12 steps require first that you admit that you have a problem. Oops, wrong twelve steps! The first three steps set the original position of the camera (or eye) and the second three set the point at which the eye is looking. The seventh step is the view angle and 60¼ gives a reasonable picture. The eighth and ninth invocations of addObject: set the near and far planes so that you see things at least 10 units away from the eye and less than 200 units away. The tenth and eleventh parameters are the x and z-coordinates of the selected object in the picture and the last is the angle of rotation of this object around its central vertical axis.
All this data is sent to theView with the setShugArray: method. Finally, the last statement of the awakeFromNib method, is odd terminology. setNeedsDisplay: sounds like an order from the manager of an appliance store ("You guys, get off your duffs and unpack that new Magnabishi 8000. That set needs display!"), but when it is set with a value of YES, it means that the view should be redrawn, presumably with a call to the view's drawRect: method. The NSView class has a flag called needsDisplay which can be set to YES or NO. Now that you know the derivation of the name, I hope it is less mystifying. In any case, it is quite a bit clearer than the old terminology of "invalidating" areas to get a redraw.
The next method you filled in is the unsung hero of this project. You were faced with the prospect of writing 12 separate methods and making 12 connections to them from 12 sliders. This is a formula for probable errors and certain boredom. However, by using Option-dragging, you've created an array of sliders in a single matrix. The updateData: method is the guts that makes the beautiful NSMatrix a powerful programming technique.
The first command issued determines the number of columns in the matrix (which is called sender). Although we know that we created a three-column matrix, I just wanted you to see what you'd do in a more general case. The next commands determine the column and row of the particular slider that has been moved which called this method in the first place. The numbers follow the standard C practice of setting the first index of an array to zero, so i can be 0, 1 or 2 and j can be 0, 1, 2 or 3.
Using the formula j * (Number of columns) + i, we can find the corresponding array index called currItem. You go on to find the floating-point value of the selected slider. Then, you put this value you found into the correct slot in the data array. Since an NSArray can only be used to contain objects, it is necessary to take your simple floating-point number and "wrap" it in an NSNumber object. So, where you might expect to place a simple x, you are using the verbose message, [NSNumber numberWithFloat: x].
Just as you did with the awakeFromNib method, you send the array to theView and prompt a redraw with the setNeedsDisplay: method.
In MyOpenGLView.m, modify the drawRect: method and fill in the setShugArray: method like so:
Listing 5. Interactive OpenGL Drawing
#import "MyOpenGLView.h"
#import <GLUT/glut.h>
@implementation MyOpenGLView
- (void)drawRect:(NSRect)theRect
{
NSRect theBounds;
int i;
float w, h, a[12];
GLUquadricObj *myQuadric;
theBounds = [self bounds];
w = theBounds.size.width;
h = theBounds.size.height;
for(i=0; i<12; i++)
a[i] = [[shugArray objectAtIndex: i] floatValue];
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(a[6], w/h, a[7], a[8]);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt( a[0], a[1], a[2],
a[3], a[4], a[5],
0.0, 1.0, 0.0);
glClearColor(1.0, 1.0, 1.0, 1.);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
//Black floor
glColor3f(0., 0., 0.);
glBegin(GL_LINE_LOOP);
glVertex3f(-95., -97., -95.);
glVertex3f(95., -97., -95.);
glVertex3f(95., -97., 95.);
glVertex3f(-95., -97., 95.);
glEnd();
//Yellow left wall
glColor3f(1., 1., 0.);
glBegin(GL_LINE_LOOP);
glVertex3f(-97., -95., -95.);
glVertex3f(-97., 95., -95.);
glVertex3f(-97., 95., 95.);
glVertex3f(-97., -95., 95.);
glEnd();
//Magenta back wall
glColor3f(1., 0., 1.);
glBegin(GL_LINE_LOOP);
glVertex3f(-95., -95., -97.);
glVertex3f(95., -95., -97.);
glVertex3f(95., 95., -97.);
glVertex3f(-95., 95.,-97.);
glEnd();
//Red quadric objects
myQuadric = gluNewQuadric();
glColor3f(1.0, 0.0, 0.0);
glPushMatrix();
glTranslatef(30., 20., -30.);
gluSphere(myQuadric, 4., 12, 8);
glPopMatrix();
glPushMatrix();
glTranslatef(30., 20., 30.);
gluCylinder(myQuadric, 4., 2., 8., 16, 6);
glPopMatrix();
glPushMatrix();
glTranslatef(-30., 20., 30.);
glRotatef(-90., 1., 0., 0.);
gluCylinder(myQuadric, 4., 0., 8., 16, 6);
glPopMatrix();
glPushMatrix();
glTranslatef(-30., 20., -30.);
glRotatef(90., 0., 1., 0.);
gluDisk(myQuadric, 0.5, 4., 16, 5);
glPopMatrix();
gluDeleteQuadric(myQuadric);
//Green GLUT primitives
glColor3f(0.0, 1.0, 0.0);
glPushMatrix();
glTranslatef(50., -20.,-50.);
glScalef(4., 8., 20.);
glutSolidCube(1.0);
glPopMatrix();
glPushMatrix();
glTranslatef(50., -20., 50.);
glutWireTorus(3.0, 6., 20, 10);
glPopMatrix();
glPushMatrix();
glTranslatef(-50.,-20., 50.);
glScalef(4., 4., 4.);
glutWireDodecahedron();
glPopMatrix();
glPushMatrix();
glTranslatef(-50., -20., -50.);
glutWireTeapot(10.0);
glPopMatrix();
//Blue moving squashed sphere (ellipsoid to be precise)
glColor3f(0.0, 0.0, 1.0);
glPushMatrix();
glTranslatef(a[9], 0.0, a[10]);
glRotatef(a[11], 0., 1., 0.);
glScalef(4., 2., 8.);
glutWireSphere(1.0, 10, 16);
glPopMatrix();
glFlush();
}
- (void)setShugArray:(NSArray *)inArray
{
[shugArray autorelease];
shugArray = [inArray copy];
}
@end
The latter method, setShugArray:, shows a typical pattern of a set... accessor method when the variable is an object. The first line empties out what was in the array previously and the second makes a copy of the input array and assigns it to shugArray.
The first message issued is the MyOpenGLView object asking about its own bounds, that is, what are the dimensions of its drawing area. The result is an NSRect. You may search high and low in the Cocoa documentation, but you'll never find the NSRect class, because an NSRect is not a class; it is a data structure (a C language struct) which is why no asterisk was necessary when I declared theBounds a few lines earlier. It includes, among other things, an NSSize structure which contains width and height members. We assign these lengthy expressions to the letters w and h. To keep the code a little more compact, I extract the 12 numbers from the shugArray into an ordinary array of floating-point numbers, a[0] through a[11].
To show our scene in 3D perspective, we replace gluOrtho2D() with gluPerspective(). We discussed the arguments of gluPerspective() earlier and, in this case, the view angle is the number at index 6, while the near and far planes are at indices 7 and 8. The second parameter of the function is the aspect ratio of the view which is w/h.
Then, we switch gears and say we want to work with the modelview matrix and start it off as an identity matrix. Then, we use gluLookAt() where a[0], a[1] and a[2] represent the eye (or camera) position, the numbers at indices 3, 4 and 5 locate the point at which the eye is looking and the last three numbers, 0., 1., 0., point out that the positive y-axis is the direction of up.
We set the clear color to white and then use it to clear the drawing. We start with some line loops to create walls for our drawing area. The only thing that is different from the previous example is that we are using three coordinates, not two. First, we set the color to black and draw a floor of the drawing. Then, we use yellow for the left side wall and magenta for the back wall.
Without leaving the base OpenGL library, the limit of your drawing capability is points, lines and polygons. However, if you look at a soccer ball, you can see how crafty arrangements of polygons can be assembled to create a spherical object. Likewise, one can generate huge numbers of polygons and patch them together to create what appear to be surfaces or solids. Fortunately, the GL Utilities and GL Utility Toolkit have routines to draw common 3D objects by creating and assembling the polygons for you. Many of them allow you to adjust the subdivisions in different coordinates and you get to see first-hand another design decision. Higher numbers of subdivisions more closely approximate the abstract mathematical figures they represent, but they do require more computation, so aesthetics and performance are competing factors that require intelligent optimization.
The first type of 3D objects we will look at are quadric surfaces that are part of GL Utilities library. These are named for the mathematical equation that defines them and includes spheres, cylinders, cones and disks.
One concept particular to quadrics is that a quadric object has to be allocated with a call to gluNewQuadric(). As you will see, a quadric object is reusable and does not require a deallocate/reallocate cycle between uses. The first argument of all for the quadric functions is the address of that object.
To clearly distinguish the quadrics we'll be drawing, we set the color to red.
After the address of the quadric object, the remaining parameters of gluSphere() are: the radius, the number of slices (like lines of longitude) and the number of stacks (latitudes);
Many types of shapes are created using the gluCylinder() function. The parameters after the quadric object are the base radius, the top radius, the cylinder height, the number of slices (like pie slices) and the number of stacks (like a stack of coins.) If the base and top radii are equal, you get a cylinder. If they are unequal, but both positive, you have a cylindrical frustum, but if one of the radii are zero, a cone results.
Notice that the cylindrical frustum indicates that the default direction for the axis of rotation is the z-axis. For the cone (the second gluCylinder() with a top radius of zero), we rotated it -90 degrees (that is, clockwise) about the x-axis, so that the cone points up (or in the y-direction).
The last quadric we will look at is the gluDisk(). It is a flat circle where the second parameter is the inner radius, where zero makes an unbroken circular disk. The third argument is the outer radius followed by the number of slices (like pizza in this case) and number of concentric rings drawn.
Now you should release the quadric with glDeleteQuadric() function and get ready to examine the shapes you can draw using the GLUT. An interesting feature of the GLUT shapes is that every one has a wireframe and a solid version with identical argument lists. While there are GLUT spheres and cones (but not cylinders), we will not look at them since you've just used glu... functions to draw those shapes.
Let's make the GLUT objects green to set them apart.
While gluSolidCube() takes just one parameter which is the side of the cube, you can see how you can use scaling to create any rectangular prism you want.
A torus is the geometric shape that looks like a doughnut (mmm, doughnuts!), but please don't run down to Krispy Kreme and order a dozen tori. The first two parameters are the inner and outer radii and the second two are the number of concentric rings and number of slices used (note that this order is the opposite of that used in the gluDisk() function.)
Next, we see a more improbable shape, a dodecahedron (12-faced solid) which, from a distance, can approximate a sphere. The function glutWireDodecahedron() has no parameters, meaning that you must use glScale*() to control its size.
The last of the stationary objects we will place is a teapot. "A teapot!," you shriek, "Why not an espresso machine?! Or a waffle iron?!" It's kind of an inside joke. In C programming textbooks, it has become a cliché to make the first program write "Hello, world" to the screen. The similar cliché exercise in OpenGL modeling is creating a teapot. The glutWireTeapot() has a single parameter which is a size factor.
Now, we come to the object we are going to move around and spin. First, we turn the color to blue. First, use glTranslatef() to move the center of the object around on the y = 0 plane. The x-coordinate is number in the tenth slot (index number 9) and the z-coordinate follows. The spinning is achieved by using glRotatef() with the last element of the array as the angle using the y-axis as the axis of rotation. Use scaling to make an oblong object and finally place a sphere. Stretched as it is, the sphere will appear as an ellipsoid.
At last, you can build and run your project and start playing with the sliders and examining their effects. First, look at the effects of adjusting Far Plane. With the default value, some of the scene is not visible (Figure 3) and becomes so when you extend it close to the upper limit.
Figure 3. Interactive Drawing View with Original Parameters.
You can use the Eye and Look coordinates to move and look around. You should be able to zip the blue "football" around smoothly and make it spin fluidly with the Object sliders. The View Angle is really an intriguing parameter. What angle makes for the most realistic view? If you make it large, you get a distorted, fish-eye lens view. It is quite subjective and an exercise like this is designed to give you a feel for what view angle makes the best-looking picture. Figure 4 shows some of the things you can see by flying around, looking around and kicking around the football.
Figure 4. Other views in Interactive OpenGL Drawing.
Conclusion
If you have made it this far, congratulations! I hope you will go on to explore OpenGL in more depth now that you have a basic vocabulary of graphics terms and techniques. With further study and practice, you will be able to create the stunning graphics that exist in your mind and have been waiting to appear on a screen near you. For those who are new to Cocoa, you saw some examples of the flow of working with Project Builder, Interface Builder and the Cocoa frameworks. While the number of steps seems formidable to the novice, walking through introductory programs like these can give you insight into the logic of the procedures and recognize patterns that will help you gain confidence in creating your own Cocoa projects.
References
- Apple Computer, Inc., Learning Cocoa, Sebastopol, CA, O'Reilly & Associates, 2001.
- Hill, F. S., Jr., Computer Graphics Using Open GL, Upper Saddle River, NJ, Prentice Hall, 2001.
- Woo, Mason, Jackie Neider, Tom Davis and Dave Shreiner, OpenGL Programming Guide, The Official Guide to Learning OpenGL, Version 1.2, 3rd Ed., Boston, Addison-Wesley, 1999.
David Trevas lives in Houston, Texas with his wife, her iBook, his wonderful daughter, his dog and two Macs.