Create a new view-based application in Xcode, and call it GLFun. To save time, copy the files Constants.h, UIColor-Random.h, UIColor-Random.m, and iphone.png from the Quartz- Fun project into this new project. Open GLFunViewController.h, and make the following
24594ch12.indd 424 6/25/09 6:06:14 PM
Download at Boykma.Com
CHAPTER 12: Drawing with Quartz and OpenGL 425
changes. You should recognize them, as they’re identical to the changes we made to QuartzFunViewController.h earlier:
#import <UIKit/UIKit.h>
@interface GLFunViewController : UIViewController { UISegmentedControl *colorControl;
}
@property (nonatomic, retain) IBOutlet UISegmentedControl *colorControl;
- (IBAction)changeColor:(id)sender;
- (IBAction)changeShape:(id)sender;
@end
Switch over to QuartzFunViewController.m, and make the following changes at the beginning of the file. Again, these changes should look very familiar to you:
#import "GLFunViewController.h"
#import "Constants.h"
#import "GLFunView.h"
#import "UIColor-Random.h"
@implementation GLFunViewController
@synthesize colorControl;
- (IBAction)changeColor:(id)sender { UISegmentedControl *control = sender;
NSInteger index = [control selectedSegmentIndex];
GLFunView *glView = (GLFunView *)self.view;
switch (index) { case kRedColorTab:
glView.currentColor = [UIColor redColor];
glView.useRandomColor = NO;
break;
case kBlueColorTab:
glView.currentColor = [UIColor blueColor];
glView.useRandomColor = NO;
break;
case kYellowColorTab:
glView.currentColor = [UIColor yellowColor];
glView.useRandomColor = NO;
break;
case kGreenColorTab:
glView.currentColor = [UIColor greenColor];
glView.useRandomColor = NO;
break;
case kRandomColorTab:
24594ch12.indd 425 6/25/09 6:06:14 PM
Download at Boykma.Com
CHAPTER 12: Drawing with Quartz and OpenGL
426
glView.useRandomColor = YES;
break;
default:
break;
} }
- (IBAction)changeShape:(id)sender { UISegmentedControl *control = sender;
[(GLFunView *)self.view setShapeType:[control selectedSegmentIndex]];
if ([control selectedSegmentIndex] == kImageShape) [colorControl setHidden:YES];
else
[colorControl setHidden:NO];
} ...
Let’s not forget to deal with memory cleanup:
...
- (void)viewDidUnload {
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
self.colorControl = nil;
[super viewDidUnload];
}
- (void)dealloc {
[colorControl release];
[super dealloc];
} ...
The only difference between this and QuartzFunController.m is that we’re referencing a view called GLFunView instead of one called QuartzFunView. The code that does our drawing is contained in a subclass of UIView. Since we’re doing the drawing in a completely different way this time, it makes sense to use a new class to contain that drawing code.
Before we proceed, you’ll need to add a few more files to your project. In the 12 GLFun folder, you’ll find four files named Texture2D.h, Texture2D.m, OpenGLES2DView.h, and OpenGLES2DView.m. The code in the first two files was written by Apple to make drawing images in OpenGL ES much easier than it otherwise would be. The second file is a class we’ve provided based on sample code from Apple that configures OpenGL to do two-dimensional drawing. OpenGL configuration is a complex topic that entire books have been written on, so we’ve done that configuration for you. You can feel free to use any of these files in your own programs if you wish.
24594ch12.indd 426 6/25/09 6:06:14 PM
Download at Boykma.Com
CHAPTER 12: Drawing with Quartz and OpenGL 427
OpenGL ES doesn’t have sprites or images, per se; it has one kind of image called a texture.
Textures have to be drawn onto a shape or object. The way you draw an image in OpenGL ES is to draw a square (technically speaking, it’s two triangles), and then map a texture onto that square so that it exactly matches the square’s size. Texture2D encapsulates that rela- tively complex process into a single, easy-to-use class.
OpenGLES2DView is a subclass of UIView that uses OpenGL to do its drawing. We set up this view so that the coordinate systems of OpenGL ES and the coordinate system of the view are mapped on a one-to-one basis. OpenGL ES is a three-dimensional system. OpenGLES2DView
maps the OpenGL 3-D world to the pixels of our 2-D view. Note that, despite the one-to-one relationship between the view and the OpenGL context, the y coordinates are still flipped, so we have to translate the y coordinate from the view coordinate system, where increases in y represent moving down, to the OpenGL coordinate system, where increases in y represent moving up.
To use the OpenGLES2DView class, first subclass it, and then implement the draw method to do your actual drawing, just as we do in the following code. You can also implement any other methods you need in your view, such as the touch-related methods we used in the QuartzFun example.
Create a new file using the Cocoa Touch Class template, select Objective-C class and NSObject for Subclass of, and call it GLFunView.m, making sure to have it create the header file.
Single-click GLFunView.h, and make the following changes:
#import <Foundation/Foundation.h>
#import "Constants.h"
#import "OpenGLES2DView.h"
@class Texture2D;
@interface GLFunView : NSObject {
@interface GLFunView : OpenGLES2DView { CGPoint firstTouch;
CGPoint lastTouch;
UIColor *currentColor;
BOOL useRandomColor;
ShapeType shapeType;
Texture2D *sprite;
}
@property CGPoint firstTouch;
@property CGPoint lastTouch;
@property (nonatomic, retain) UIColor *currentColor;
@property BOOL useRandomColor;
24594ch12.indd 427 6/25/09 6:06:14 PM
Download at Boykma.Com
CHAPTER 12: Drawing with Quartz and OpenGL
428
@property ShapeType shapeType;
@property (nonatomic, retain) Texture2D *sprite;
@end
This class is similar to QuartzFunView.h, but instead of using UIImage to hold our image, we use a Texture2D to simplify the process of drawing images into an OpenGL ES context. We also change the superclass from UIView to OpenGLES2DView so that our view becomes an OpenGL ES–backed view set up for doing two-dimensional drawing.
Switch over to GLFunView.m, and make the following changes.
#import "GLFunView.h"
#import "UIColor-Random.h"
#import "Texture2D.h"
@implementation GLFunView
@synthesize firstTouch;
@synthesize lastTouch;
@synthesize currentColor;
@synthesize useRandomColor;
@synthesize shapeType;
@synthesize sprite;
- (id)initWithCoder:(NSCoder*)coder {
if (self = [super initWithCoder:coder]) { self.currentColor = [UIColor redColor];
self.useRandomColor = NO;
self.sprite = [[Texture2D alloc] initWithImage:[UIImage
imageNamed:@"iphone.png"]];
glBindTexture(GL_TEXTURE_2D, sprite.name);
}
return self;
}
- (void)draw { glLoadIdentity();
glClearColor(0.78f, 0.78f, 0.78f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
CGColorRef color = currentColor.CGColor;
const CGFloat *components = CGColorGetComponents(color);
CGFloat red = components[0];
CGFloat green = components[1];
CGFloat blue = components[2];
glColor4f(red,green, blue, 1.0);
24594ch12.indd 428 6/25/09 6:06:14 PM
Download at Boykma.Com
CHAPTER 12: Drawing with Quartz and OpenGL 429
switch (shapeType) { case kLineShape: {
glDisable(GL_TEXTURE_2D);
GLfloat vertices[4];
// Convert coordinates vertices[0] = firstTouch.x;
vertices[1] = self.frame.size.height - firstTouch.y;
vertices[2] = lastTouch.x;
vertices[3] = self.frame.size.height - lastTouch.y;
glLineWidth(2.0);
glVertexPointer(2, GL_FLOAT, 0, vertices);
glDrawArrays (GL_LINES, 0, 2);
break;
}
case kRectShape: {
glDisable(GL_TEXTURE_2D);
// Calculate bounding rect and store in vertices GLfloat vertices[8];
GLfloat minX = (firstTouch.x > lastTouch.x) ? lastTouch.x : firstTouch.x;
GLfloat minY = (self.frame.size.height - firstTouch.y >
self.frame.size.height - lastTouch.y) ? self.frame.size.height - lastTouch.y :
self.frame.size.height - firstTouch.y;
GLfloat maxX = (firstTouch.x > lastTouch.x) ? firstTouch.x : lastTouch.x;
GLfloat maxY = (self.frame.size.height - firstTouch.y >
self.frame.size.height - lastTouch.y) ? self.frame.size.height - firstTouch.y :
self.frame.size.height - lastTouch.y;
vertices[0] = maxX;
vertices[1] = maxY;
vertices[2] = minX;
vertices[3] = maxY;
vertices[4] = minX;
vertices[5] = minY;
vertices[6] = maxX;
vertices[7] = minY;
glVertexPointer (2, GL_FLOAT , 0, vertices);
glDrawArrays (GL_TRIANGLE_FAN, 0, 4);
break;
}
case kEllipseShape: {
glDisable(GL_TEXTURE_2D);
24594ch12.indd 429 6/25/09 6:06:14 PM
Download at Boykma.Com
CHAPTER 12: Drawing with Quartz and OpenGL
430
GLfloat vertices[720];
GLfloat xradius = (firstTouch.x > lastTouch.x) ? (firstTouch.x - lastTouch.x)/2 :
(lastTouch.x - firstTouch.x)/2;
GLfloat yradius = (self.frame.size.height - firstTouch.y >
self.frame.size.height - lastTouch.y) ? ((self.frame.size.height - firstTouch.y) -
(self.frame.size.height - lastTouch.y))/2 : ((self.frame.size.height - lastTouch.y) - (self.frame.size.height - firstTouch.y))/2;
for (int i = 0; i < 720; i+=2) {
GLfloat xOffset = (firstTouch.x > lastTouch.x) ? lastTouch.x + xradius
: firstTouch.x + xradius;
GLfloat yOffset = (self.frame.size.height - firstTouch.y >
self.frame.size.height - lastTouch.y) ? self.frame.size.height - lastTouch.y + yradius :
self.frame.size.height - firstTouch.y + yradius;
vertices[i] = (cos(degreesToRadian(i/2))*xradius) + xOffset;
vertices[i+1] = (sin(degreesToRadian(i/2))*yradius) + yOffset;
}
glVertexPointer(2, GL_FLOAT , 0, vertices);
glDrawArrays (GL_TRIANGLE_FAN, 0, 360);
break;
}
case kImageShape:
glEnable(GL_TEXTURE_2D);
[sprite drawAtPoint:CGPointMake(lastTouch.x,
self.frame.size.height - lastTouch.y)];
break;
default:
break;
}
glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
}
- (void)dealloc {
[currentColor release];
[sprite release];
[super dealloc];
}
24594ch12.indd 430 6/25/09 6:06:14 PM
Download at Boykma.Com
CHAPTER 12: Drawing with Quartz and OpenGL 431
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (useRandomColor)
self.currentColor = [UIColor randomColor];
UITouch* touch = [[event touchesForView:self] anyObject];
firstTouch = [touch locationInView:self];
lastTouch = [touch locationInView:self];
[self draw];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
lastTouch = [touch locationInView:self];
[self draw];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject];
lastTouch = [touch locationInView:self];
[self draw];
}
@end
You can see that using OpenGL isn’t, by any means, easier or more concise than using Quartz 2D. Although it’s more powerful than Quartz, you’re also closer to the metal, so to speak.
OpenGL can be daunting at times.
Because this view is being loaded from a nib, we added an initWithCoder: method, and in it, we create and assign a UIColor to currentColor. We also defaulted useRandomColor to
NO. and created our Texture2D object.
After the initWithCoder: method, we have our draw method, which is where you can really see the difference in the approaches between the two libraries. Let’s take a look at process of drawing a line. Here’s how we drew the line in the Quartz version (we’ve removed the code that’s not directly relevant to drawing):
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 2.0);
CGContextSetStrokeColorWithColor(context, currentColor.CGColor);
CGContextMoveToPoint(context, firstTouch.x, firstTouch.y);
CGContextAddLineToPoint(context, lastTouch.x, lastTouch.y);
CGContextStrokePath(context);
24594ch12.indd 431 6/25/09 6:06:14 PM
Download at Boykma.Com
CHAPTER 12: Drawing with Quartz and OpenGL
432
Here are the steps we had to take in OpenGL to draw that same line. First, we reset the virtual world so that any rotations, translations, or other transforms that might have been applied to it are gone:
glLoadIdentity();
Next, we clear the background to the same shade of gray that was used in the Quartz ver- sion of the application:
glClearColor(0.78, 0.78f, 0.78f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
After that, we have to set the OpenGL drawing color by dissecting a UIColor and pulling the individual RGB components out of it. Fortunately, because we used the convenience class methods, we don’t have to worry about which color model the UIColor uses. We can safely assume it will use the RGBA color space:
CGColorRef color = currentColor.CGColor;
const CGFloat *components = CGColorGetComponents(color);
CGFloat red = components[0];
CGFloat green = components[1];
CGFloat blue = components[2];
glColor4f(red,green, blue, 1.0);
Next, we turn off OpenGL ES’s ability to map textures:
glDisable(GL_TEXTURE_2D);
Any drawing code that fires from the time we make this call until there’s a call to
glEnable(GL_TEXTURE_2D) will be drawn without a texture, which is what we want. If we allow a texture to be used, the color we just set won’t show.
To draw a line, we need two vertices, which means we need an array with four elements. As we’ve discussed, a point in two-dimensional space is represented by two values, x and y. In Quartz, we used a CGPointstruct to hold these. In OpenGL, points are not embedded in
structs. Instead, we pack an array with all the points that make up the shape we need to draw. So, to draw a line from point (100, 150) to point (200, 250) in OpenGL ES, we would cre- ate a vertex array that looked like this:
vertex[0] = 100;
vertex[1] = 150;
vertex[2] = 200;
vertex[3] = 250;
Our array has the format {x1, y1, x2, y2, x3, y3}. The next code in this method converts two
CGPoint structs into a vertex array:
24594ch12.indd 432 6/25/09 6:06:15 PM
Download at Boykma.Com
CHAPTER 12: Drawing with Quartz and OpenGL 433
GLfloat vertices[4];
vertices[0] = firstTouch.x;
vertices[1] = self.frame.size.height - firstTouch.y;
vertices[2] = lastTouch.x;
vertices[3] = self.frame.size.height - lastTouch.y;
Once we’ve defined the vertex array that describes what we want to draw (in this example, a line), we specify the line width, pass the array into OpenGL ES using the method
glVertexPointer(), and tell OpenGL ES to draw the arrays:
glLineWidth(2.0);
glVertexPointer (2, GL_FLOAT , 0, vertices);
glDrawArrays (GL_LINES, 0, 2);
Whenever we finish drawing in OpenGL ES, we have to tell it to render its buffer, and tell our view’s context to show the newly rendered buffer:
glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
To clarify, the process of drawing in OpenGL consists of three steps. First, you draw in the context. Second, once all your drawing is done, you render the context into the buffer. And third, you present your render buffer, which is when the pixels actually get drawn onto the screen.
As you can see, the OpenGL example is considerably longer. The difference between Quartz 2D and OpenGL ES becomes even more dramatic when we look at the process of drawing an ellipse. OpenGL ES doesn’t know how to draw an ellipse. OpenGL, the big brother and predecessor to OpenGL ES, has a number of convenience functions for generating common two- and three-dimensional shapes, but those convenience functions are some of the func- tionality that was stripped out of OpenGL ES to make it more streamlined and suitable for use in embedded devices like the iPhone. As a result, a lot more responsibility falls into the developer’s lap.
As a reminder, here is how we drew the ellipse using Quartz 2D:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 2.0);
CGContextSetStrokeColorWithColor(context, currentColor.CGColor);
CGContextSetFillColorWithColor(context, currentColor.CGColor);
CGRect currentRect;
CGContextAddEllipseInRect(context, self.currentRect);
CGContextDrawPath(context, kCGPathFillStroke);
24594ch12.indd 433 6/25/09 6:06:15 PM
Download at Boykma.Com
CHAPTER 12: Drawing with Quartz and OpenGL
434
For the OpenGL ES version, we start off with the same steps as before, resetting any move- ment or rotations, clearing the background to white, and setting the draw color based on
currentColor:
glLoadIdentity();
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_TEXTURE_2D);
CGColorRef color = currentColor.CGColor;
const CGFloat *components = CGColorGetComponents(color);
CGFloat red = components[0];
CGFloat green = components[1];
CGFloat blue = components[2];
glColor4f(red,green, blue, 1.0);
Since OpenGL ES doesn’t know how to draw an ellipse, we have to roll our own, which means dredging up painful memories of Ms. Picklebaum’s geometry class. We’ll define a vertex array that holds 720 GLfloats, which will hold an x and a y position for 360 points, one for each degree around the circle. We could change the number of points to increase or decrease the smoothness of the circle. This approach looks good on any view that’ll fit on the iPhone screen but probably does require more processing than strictly necessary if all you are drawing is smaller circles.
GLfloat vertices[720];
Next, we’ll figure out the horizontal and vertical radii of the ellipse based on the two points stored in firstTouch and lastTouch:
GLfloat xradius = (firstTouch.x > lastTouch.x) ? (firstTouch.x - lastTouch.x)/2 : (lastTouch.x - firstTouch.x)/2;
GLfloat yradius = (self.frame.size.height - firstTouch.y >
self.frame.size.height - lastTouch.y) ? ((self.frame.size.height - firstTouch.y) ủ (self.frame.size.height - lastTouch.y))/2 : ((self.frame.size.height - lastTouch.y) ủ (self.frame.size.height - firstTouch.y))/2;
Next, we’ll loop around the circle, calculating the correct points around the circle:
for (int i = 0; i < 720; i+=2) {
GLfloat xOffset = (firstTouch.x > lastTouch.x) ? lastTouch.x + xradius : firstTouch.x + xradius;
GLfloat yOffset = (self.frame.size.height - firstTouch.y >
self.frame.size.height - lastTouch.y) ?
self.frame.size.height - lastTouch.y + yradius : self.frame.size.height - firstTouch.y + yradius;
24594ch12.indd 434 6/25/09 6:06:15 PM
Download at Boykma.Com
CHAPTER 12: Drawing with Quartz and OpenGL 435
vertices[i] = (cos(degreesToRadian(i/2))*xradius) + xOffset;
vertices[i+1] = (sin(degreesToRadian(i/2))*yradius) + yOffset;
}
Finally, we’ll feed the vertex array to OpenGL ES, tell it to draw it and render it, and then tell our context to present the newly rendered image:
glVertexPointer (2, GL_FLOAT , 0, vertices);
glDrawArrays (GL_TRIANGLE_FAN, 0, 360);
glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
We won’t review the rectangle method, because it uses the same basic technique; we define a vertex array with the four vertices to define the rectangle, and then we render and present it. There’s also not much to talk about with the image drawing, since that lovely Texture2D
class from Apple makes drawing a sprite just as easy as it is in Quartz 2D. There is one impor- tant thing to notice there, though:
glEnable(GL_TEXTURE_2D);
Since it is possible that the ability to draw textures was previously disabled, we have to make sure it’s enabled before we attempt to use the Texture2D class.
After the draw method, we have the same touch-related methods as the previous version.
The only difference is that instead of telling the view that it needs to be displayed, we just the draw method. We don’t need to tell OpenGL ES what parts of the screen will be updated;
it will figure that out and leverage hardware acceleration to draw in the most efficient manner.