时间:2021-07-01 10:21:17 帮助过:18人阅读
Note from Ray: This is the fourth iOS 5 tutorial in the iOS 5 Feast! This tutorial is a free preview chapter from our new book iOS 5 By Tutorials. Enjoy! iOS 5 comes with a new set of APIs that makes developing with OpenGL much easier than
Note from Ray: This is the fourth iOS 5 tutorial in the iOS 5 Feast! This tutorial is a free preview chapter from our new book iOS 5 By Tutorials. Enjoy!
iOS 5 comes with a new set of APIs that makes developing with OpenGL much easier than it used to be.
The new set of APIs is collectively known as GLKit. It contains four main divs:
The goal of this tutorial is to get you quickly up-to-speed with the basics of using OpenGL with GLKit, from the ground up, assuming you have no experience whatsoever. We will build a simple app from scratch that draws a simple cube to the screen and makes it rotate around.
In the process, you’ll learn the basics of using each of these new APIs. It should be a good introduction to GLKit, whether you’ve already used OpenGL in the past, or if you’re a complete beginner!
Note that this tutorial slightly overlaps some of the other OpenGL ES 2.0 tutorials on this site. This tutorial does not assume you have read them first, but if you have you might want to skip over the divs you already know.
Before we get started, I wanted to mention that this tutorial will be focusing on OpenGL ES 2.0 (not OpenGL ES 1.0).
If you are new to OpenGL ES programming, here is the difference between OpenGL ES 1.0 and OpenGL ES 2.0:
“OMG!” you may think, “well why would I ever want to use OpenGL ES 2.0 then, if it’s just extra work?!” Although it does add some extra work, with OpenGL ES 2.0 you make some really cool effects that wouldn’t be possible in OpenGL ES 1.0, such as this toon shader (via Imagination Technologies):
Or even these amazing lighting and shadow effects (via Fabien Sanglard):
Pretty cool eh?
OpenGL ES 2.0 is only available on the iPhone 3GS+, iPod Touch 3G+, and all iPads. But the percentage of people with these devices is in the majority now, so it’s well worth using!
OpenGL ES 2.0 does have a bit of a higher learning curve than OpenGL ES 1.0, but now with GLKit the learning curve is much easier, because the GLKEffects and GLKMath APIs lets you easily do a lot of the stuff that was built into OpenGL ES 1.0.
I’d say if you’re new to OpenGL programming, it’s probably best to jump straight into OpenGL ES 2.0 rather than trying to learn OpenGL ES 1.0 and then upgrading, especially now that GLKit is available. And this tutorial should help get you started with the basics! :]
OK, let’s get started!
Create a new project in Xcode and select the iOS\Application\Empty Application Template. We’re selecting the Empty Application template (not the OpenGL Game template!) so you can put everything together from scratch and get a better idea how everything fits together.
Set the Product Name to HelloGLKit, make sure the Device Family is set to iPhone, make sure “Use Automatic Reference Counting” is checked but the other options are not, and click Next. Choose a folder to save your project in, and click Create.
If you run your app, you should see a plain blank Window:
The project contains almost no code at this point, but let’s take a quick look to see how it all fits together. If you went through the Storyboard tutorial earlier in this book, this should be a good review.
Open main.m, and you’ll see the first function that is called when the app starts up, unsurprisingly called main:
int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } |
The last parameter to this method tells UIApplication the class to crate an instance of and use as it’s delegate – in this case, a class called AppDelegate.
So switch over to the only class in the template, AppDelegate.m, and take a look at the method that gets called when the app starts up:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; } |
This programatically creates the main window for the app and makes it visible. And that’s it! About as “from scratch” as you can get.
To get started with OpenGL ES 2.0, the first thing we need to do is add a subview to the window that does its drawing with OpenGL. If you’ve done OpenGL ES 2.0 programming before, you know that there was a ton of boilerplate code to get this working – things like creating a render buffer and frame buffer, etc.
But now it’s nice and easy with a new GLKit class called GLKView! Whenever you want to use OpenGL rendering inside a view, you simply add a GLKView (which is a normal subclass of UIView) and configure a few properties on it.
You can then set a class as the GLKView’s delegate, and it will call a method on that class when it needs to be drawn. In this method you can put in your OpenGL commands!
Let’s see how this works. First things first – you need to add a few frameworks to your project to use GLKit. Select your HelloGLKit project in the Project Navigator, select the HelloGLKit target, select Build Phases, Expand the Link Binary With Libraries div, and click the Plus button. From the drop-down list, select the following frameworks and click Add:
Switch to AppDelegate.h, and at the top of the file import the header file for GLKit as follows:
#import
|
Next switch to AppDelegate.m, and modify the application:didFinishLaunchingWithOptions to add a GLKView as a subview of the main window:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; EAGLContext * context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; // 1 GLKView *view = [[GLKView alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // 2 view.context = context; // 3 view.delegate = self; // 4 [self.window addSubview:view]; // 5 self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; } |
The lines marked with comments are the new lines – so let’s go over them one by one.
1. Create a OpenGL context. To do anything with OpenGL, you need to create a EAGLContext.
An EAGLContext manages all of the information iOS needs to draw with OpenGL. It’s similar to how you need a Core Graphics context to do anything with Core Graphics.
When you create a context, you specify what version of the API you want to use. Here, you specify that you want to use OpenGL ES 2.0. If it is not available (such as if the program were run on an iPhone 3G), the app would terminate.
2. Create a GLKView. This creates a new instance of a GLKView, and makes it as large as the entire window.
3. Set the GLKView’s context. When you create a GLKView, you need to tell it the OpenGL context to use, so we specify the one we already created.
4. Set the GLKView’s delegate. This sets the current class (AppDelegate) as the GLKView’s delegate. This means whenever the view needs to be redrawn, it will call a method named glkView:drawInRect on whatever class you specify here. We will implement this inside the App Delegate shortly to contain some basic OpenGL commands to paint the screen red.
5. Add the GLKView as a subview. This line adds the GLKView as a subview of the main window.
Since we marked the App Delegate as GLKView’d delegate, we need to mark it as implementing the GLKViewDelegate protocol. So switch to AppDelegate.h and modify the @interface line as follows:
@interface AppDelegate : UIResponder |
One step left! Switch back to AppDelegate.m, add the following code right before the @end:
#pragma mark - GLKViewDelegate - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(1.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); } |
The first line calls glClearColor to specify the RGB and alpha (transparency) values to use when clearing the screen. We set it to red here.
The second line calls glClear to actually perform the clearing. Remember that there can be different types of buffers, such as the render/color buffer we’re displaying, and others we’re not using yet such as depth or stencil buffers. Here we use the GL_COLOR_BUFFER_BIT to specify what exactly to clear – in this case, the current render/color buffer.
That’s it! Compile and run, and with just 7 lines of code we have OpenGL rendering to the screen!
Those of you who are completely new to OpenGL might not be very impressed, but those of you who have done this before will be very happy with the convenience here :]
We only set a few properties of GLKView here (context and delegate), but I wanted to mention the other properties and methods on GLKView that might be useful to you in the future.
This is an optional reference div and is for informational purposes only. If you want to keep coding away, feel free to skip to the next div! :]
context and delegate
We already covered these in the previous div, so I won’t repeat here.
drawableColorFormat
Your OpenGL context has a buffer it uses to store the colors that will be displayed to the screen. You can use this property to set the color format for each pixel in the buffer.
The default value is GLKViewDrawableColorFormatRGBA8888, which means 8 bits are used for each pixel in the buffer (so 4 bytes per pixel). This is nice because it gives you the widest possible range of colors to work with, which often makes your app look nicer.
But if your app can get away with a lower range of colors, you might want to switch this to GLKViewDrawableColorFormatRGB565, which makes your app consume less resources (memory and processing tieme).
drawableDepthFormat
Your OpenGL context can also optionally have another buffer associated with it called the depth buffer. This helps make sure that objects closer to the viewer show up in front of objects farther away.
The way it works by default is OpenGL stores the closest object to the viewer at each pixel in a buffer. When it goes to draw a pixel, it checks the depth buffer to see if it’s already drawn something closer to the viewer, and if so discards it. Otherwise, it adds it to the depth buffer and the color buffer.
You can set this property to choose the format of the depth buffer. The default value is GLKViewDrawableDepthFormatNone, which means that no depth buffer is enabled at all.
But if you want this feature (which you usually do for 3D games), you should choose GLKViewDrawableDepthFormat16 or GLKViewDrawableDepthFormat24. The tradeoff here is with GLKViewDrawableDepthFormat16 your app will use less resources, but you might have rendering issues when objects are very close to each other.
drawableStencilFormat
Another optional buffer your OpenGL context can have is the stencil buffer. This helps you restrict drawing to a particular portion of the screen. It’s often useful for things like shadows – for example you might use the stencil buffer to make sure the shadows to be cast on the floor.
The default value for this property is GLKViewDrawableStencilFormatNone, which means there is no stencil buffer, but you can enable it by setting it to the only alternative – GLKViewDrawableStencilFormat8.
drawableMultisample
The last optional buffer you can set up through a GLKView property is the multisampling buffer. If you ever try drawing lines with OpenGL and notice “jagged lines”, multisampling can help with this issue.
Basically what it does is instead of calling the fragment shader one time per pixel, it divides up the pixel into smaller units and calls the fragment shader multiple times at smaller levels of detail. It then merges the colors returned, which often results in a much smoother look around edges of geometry.
Be careful about setting this because it requires more processing time and meomry for your app. The default value is GLKViewDrawableMultisampleNone, but you can enable it by setting it to the only alternative – GLKViewDrawableMultisample4X.
drawableHeight/drawableWidth
These are read-only properties that indicate the integer height and width of your various buffers. These are based on the bounds and contentSize of the view – the buffers are automatically resized when these change.
snapshot
This is a handy way to get a UIImage of the view’s current context.
bindDrawable
OpenGL has yet another buffer called a frame buffer, which is basically a collection of all the other buffers we talked about (color buffer, depth buffer, stencil buffer etc).
Before your glkView:drawInRect is called, GLKit will bind to the frame buffer it set up for you behind the scenes. But if your game needs to change to a different frame buffer to perform some other kind of rendering (for example, if you’re rendering to another texture), you can use the bindDrawable method to tell GLKit to re-bind back to the frame buffer it set up for you.
deleteDrawable
GLKView and OpenGL take a substantial amount of memory for all of these buffers. If your GLKView isn’t visible, you might find it useful to deallocate this memory temporarily until it becomes visible again. If you want to do this, just use this method!
Next time the view is drawn, GLKView will automatically re-allocate the memory behind the scenes. Quite handy, eh?
enableSetNeedsDisplay and display
I don’t want to spoil the surprise – we’ll explain these in the next div! :]
Let’s try to update our GLKView periodically, like we would in a game. How about we make the screen pulse from red to black, kind of like a “Red Alert” effect!
Go to the top of AppDelegate.m and modify the @implementation line to add two private variables as follows:
@implementation AppDelegate { float _curRed; BOOL _increasing; } |
And initialize these in application:didFinishLaunchingWithOptions:
_increasing = YES; _curRed = 0.0; |
Then go to the glkView:drawInRect method and update it to the following:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { if (_increasing) { _curRed += 0.01; } else { _curRed -= 0.01; } if (_curRed >= 1.0) { _curRed = 1.0; _increasing = NO; } if (_curRed <= 0.0) { _curRed = 0.0; _increasing = YES; } glClearColor(_curRed, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); } |
Every time drawInRect is called, it updates the _curRed value a little bit based on whether it’s increasing or decreasing. Note that this code isn’t perfect, because it doesn’t take into effect how long it takes between calls to drawInRect. This means that the animation might be faster or slower based on how quickly drawInRect is called. We’ll discuss a way to fix this later in the tutorial.
Compile and run and… wait a minute, nothing’s happening!
By default, the GLKView only updates itself on an as-needed basis – i.e. when views are first shown, the size changes, or the like. However for game programming, you often need to redraw every frame!
We can disable this default behavior of GLKView by setting enableSetNeedsDisplay to false. Then, we can control when the redrawing occurs by calling the display method on GLKView whenever we want to update the screen.
Ideally we would like to synchronize the time we render with OpenGL to the rate at which the screen refreshes.
Luckily, Apple provides an easy way for us to do this with CADisplayLink! It’s really easy to use so let’s just dive in. First add this import to the top of AppDelegate.m:
#import
|
Then add these lines to application:didFinishLaunchingWithOptions:
view.enableSetNeedsDisplay = NO; CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render:)]; [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; |
Then add a new render function as follows:
- (void)render:(CADisplayLink*)displayLink { GLKView * view = [self.window.subviews objectAtIndex:0]; [view display]; } |
Compile and run, and you should now see a cool pulsating “red alert” effect!
You know that code we just wrote in that last div? Well you can just forget about it, because there’s a much easier way to do so by using GLKViewController :]
The reason I showed you how to do it with plain GLKView first was so you understand the point behind using GLKViewController – it saves you from writing that code, plus adds some extra neat features that you would have had to code yourself.
So try out GLKViewController. Modify your application:didFinishLaunchingWithOptions to look like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; EAGLContext * context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; GLKView *view = [[GLKView alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; view.context = context; view.delegate = self; //[self.window addSubview:view]; _increasing = YES; _curRed = 0.0; //view.enableSetNeedsDisplay = NO; //CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render:)]; //[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; GLKViewController * viewController = [[GLKViewController alloc] initWithNibName:nil bundle:nil]; // 1 viewController.view = view; // 2 viewController.delegate = self; // 3 viewController.preferredFramesPerSecond = 60; // 4 self.window.rootViewController = viewController; // 5 self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; } |
Feel free to delete the commented lines – I just commented them out so it is easy to see what’s no longer needed. There are also four new lines (marked with comments):
1. Create a GLKViewController. This creates a new instance of a GLKViewController programatically. In this case, it has no XIB associated.
2. Set the GLKViewController’s view. The root view of a GLKViewController should be a GLKView, so we set it to the one we already created.
3. Set the GLKViewController’s delegate. We set the current class (AppDelegate) as the delegate of the GLKViewController. This means that the GLKViewController will notify us each frame so we can run game logic, or when the game pauses (a nice built-in feature of GLKViewController we’ll demonstrate later).
4. Set the preferred FPS. The GLKViewController will call your draw method a certain number of times per second. This number gives a hint to the GLKViewController how often you’d like to be called. Of course, if your game takes a long time to render frames, the actual number may be lower than this.
The default value is 30 FPS. Apple’s guidelines are to set this to whatever your app can reliably support to the frame rate is consistent and doesn’t seem to stutter. This app is very simple so can easily run at 60 FPS, so we set it to that.
Also as an FYI, if you want to see the actual number of times the OS will attempt to call your update/draw methods, check the read-only framesPerSecond property.
5. Set the rootViewController. We want this view controller to be the first thing that shows up, so we add it as the rootViewController of the window. Note that we no longer need to add the view as a subview of the window manually, because it’s the root view of the GLKViewController.
Notice that we no longer need the code to run the render loop and tell the GLView to refresh each frame – GLKViewController does that for us in the background! So go ahead and comment out the render method as well.
Also remember that we set the GLKViewController’s delegate to the current class (AppDelegate), so let’s mark it as implementing GLKViewControllerDelegate. Switch to AppDelegate.h and replace the @implementation with the following line:
@interface AppDelegate : UIResponder |
The final step is to update the glkView:drawInRect method, and add the implementation for GLKViewController’s glkViewControllerUpdate callback:
#pragma mark - GLKViewDelegate - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(_curRed, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); } #pragma mark - GLKViewControllerDelegate - (void)glkViewControllerUpdate:(GLKViewController *)controller { if (_increasing) { _curRed += 1.0 * controller.timeSinceLastUpdate; } else { _curRed -= 1.0 * controller.timeSinceLastUpdate; } if (_curRed >= 1.0) { _curRed = 1.0; _increasing = NO; } if (_curRed <= 0.0) { _curRed = 0.0; _increasing = YES; } } |
Note that we moved the code to change the current color from the draw method (where it didn’t really belong) to the update method (intended for game/app logic).
Also notice that we changed the amount the red color increments from a hardcoded value to a calculated value, based on the amount of time since the last update. This is nice because it guarantees the animation will always proceed at the same speed, regardless of the frame rate.
This is another of those convenient things GLKViewController does for you! We didn’t have to write special code to store the time since the last update – it did it for us! There are some other time-based properties, but we’ll discuss those later.
So far, we’ve manually created the GLKViewController and GLKView because it was a simple way to introduce you to how they work. But you probably wouldn’t want to do it this way in a real app – it’s much better to leverage the power of Storyboards, so you can include this view controller anywhere you want in your app hierarchy!
So let’s do a little refactoring to accomplish that. First, let’s create a subclass of GLKViewController that we can use to contain our app’s logic. So create a new file with the iOS\Cocoa Touch\UIViewController subclass template, name the class HelloGLKitViewController, as a subclass of GLKViewController (you can type this in even though it’s not in the dropdown). Make sure Targeted for iPad and With XIB for user interface are both unselected, and create the file.
Open up HelloGLKitViewController.m, and start by adding a private category on the class to contain the instance variables we need, and a new property to store the context:
@interface HelloGLKitViewController () { float _curRed; BOOL _increasing; } @property (strong, nonatomic) EAGLContext *context; @end @implementation HelloGLKitViewController @synthesize context = _context; |
Then implement viewDidLoad and viewDidUnload as the following:
- (void)viewDidLoad { [super viewDidLoad]; self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!self.context) { NSLog(@"Failed to create ES context"); } GLKView *view = (GLKView *)self.view; view.context = self.context; } - (void)viewDidUnload { [super viewDidUnload]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } self.context = nil; } |
In viewDidLoad, we create an OpenGL ES 2.0 context (same as we did last time in the App Delegate) and squirrel it away. Our root view is a GLKView (we know this because we set it up this way in the Storyboard editor), so we cast it as one. We then set its context to the OpenGL context we just created.
Note that we don’t have to set the view controller as the view’s delegate – GLKViewController does this automatically behind the scenes.
In viewDidUnload, we just do the opposite to clean up. We have to make sure there’s no references left to our context, so we check to see if the current context is our context, and set it to nil if so. We also clear out or reference to it.
At the bottom of the file, add the implementations of the glkView:drawInRect and update callbacks, similar to before:
#pragma mark - GLKViewDelegate - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(_curRed, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); } #pragma mark - GLKViewControllerDelegate - (void)update { if (_increasing) { _curRed += 1.0 * self.timeSinceLastUpdate; } else { _curRed -= 1.0 * self.timeSinceLastUpdate; } if (_curRed >= 1.0) { _curRed = 1.0; _increasing = NO; } if (_curRed <= 0.0) { _curRed = 0.0; _increasing = YES; } } |
Note that the update method is called plain “update”, because now that we’re inside the GLKViewCotroller we can just override this method instead of having to set a delegate. Also the timeSinceLastUpdate is access via “self”, not a passed in view controller.
With this in place, let’s create the Storyboard. Create a new file with the iOS\User Interface\Storyboard template, choose iPhone for the device family, and save it as MainStoryboard.storyboard.
Open MainStoryboard.storyboard, and from the Objects panel drag a View Controller into the grid area. Select the View Controller, and in the Identity Inspector set the class to HelloGLKitViewController:
Also, select the View inside the View Controller, and in the Identity Inspector set the Class to GLKView.
To make this Storyboard run on startup, open HelloGLKit-Info.plist, control-click in the blank area, and select Add Row. From the dropdown select Main storyboard file base name, and enter MainStoryboard.
That pretty much completes everything we need, but we still have some old code in AppDelegate that we need to clean up. Start by deleting the _curRed and _increasing instance variables from AppDelegate.m. Also delete the glkView:drawInRect and glkViewControllerUpdate methods.
And delete pretty much everything from application:didFinishLaunchingWithOptions so it looks like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { return YES; } |
And modify the AppDelegate @interface to strip out the two GLKit delegates since we aren’t using those anymore:
@interface AppDelegate : UIResponder |
That’s it! Compile and run your app, and you’ll see the “red alert” effect working as usual.
At this point, you’re getting pretty close to the setup you get when you choose the OpenGL Game template with the Storyboard option set (except it has a lot of other code in there you can just delete if you don’t need it). Feel free to choose that in the future when you’re creating a new OpenGL project to save a little time – but now you know how it works from the ground up!
Now that we’re all nicely set up in a custom GLKViewController subclass, let’s play around with one of the neat features of GLKViewController – pausing!
To see how it works, just add the following to the bottom of HelloGLKitViewController.m:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { self.paused = !self.paused; } |
Compile and run the app, and now whenever you tap the animation stops! Behind the scenes, GLKViewController stops calling your update method and your draw method. This is a really handy way to implement a pause button in your game.
In addition to that, GLKViewController has a pauseOnWillResignActive property that is by default set to YES. This means when the user hits the home button or receives an interruption such as a phone call, your game will be automatically paused! Similarly, it has a resumeOnDidBecomeActive property that is by default set to YES, which means when the user comes back to your app, it will automatically unpause. Handy, that!
We’ve covered almost every property of GLKViewController by now, except for the extra time info properties I discussed earlier:
Let’s add some code to try these out. Add the following code to the top of touchesBegan:
NSLog(@"timeSinceLastUpdate: %f", self.timeSinceLastUpdate); NSLog(@"timeSinceLastDraw: %f", self.timeSinceLastDraw); NSLog(@"timeSinceFirstResume: %f", self.timeSinceFirstResume); NSLog(@"timeSinceLastResume: %f", self.timeSinceLastResume); |
Play around with it so you’re familiar with how they work. As you can see, these can be pretty convenient!