第十二章 使用Quartz和OpenGL绘图
有时应用程序需要能够自定义绘图。一个库是Quartz 2D,她是Core Graphics框架的一部分;另一个库是OpenGL ES,她是跨平台的图形库。OpenGL ES是跨平台图形库OpenGL的简化版。OpenGL ES是OpenGL的一个子集,OpenGL ES是专门为iPhone之类的嵌入式系统(因此缩写字母为“ES”)设计的。
12.1 图形世界的两个视图
Quartz是一组函数、数据类型以及对象,专门设计用于直接在内存中对视图或图像进行控制。
Quartz将正在绘制的视图或图像视为一个虚拟的画布,并遵循所谓的“绘画者模型”。如果绘画者将整个画布涂为红色,然后将画布的下半部分涂为蓝色,那么画布将变为一半红色、一半蓝色或紫色。如果颜料是不透明的,应该为蓝色,如果颜料是半透明的,应该为紫色。每个绘图操作都将被应用于画布,并且处于之前所有绘图操作之上。
另一方面,OpenGL ES以状态机的形式实现。OpenGL ES不允许执行直接影响视图、窗口或图像的操作,他维护一个虚拟的三维世界。当向这个世界中添加对象时,OpenGL会跟踪所有对象的状态。虽然OpenGL ES没有提供虚拟画布,但是却提供了一个进入其世界的虚拟窗口。可以向该世界中添加对象并定义虚拟窗口相对于该世界的位置。然后,OpenGL根据配置方式以及各种对象彼此相对的位置绘制视图,并通过该窗口呈现给用户。这个概念有点抽象。感到迷惑的话,就通过后面的示例理解吧。
Quartz相对比较容易使用。他提供了各种直线、形状以及图像绘制函数。但Quartz 2D仅限于二维绘图。尽管许多Quartz函数会在绘图时利用硬件加速,但无法保证在Quartz中执行的任何操作都得到了加速。
OpenGL同时提供了二维和三维绘图。他经过专门设计,目的是为了充分利用硬件加速。由于他可以跟踪虚拟世界的状态,因此还非常适合用于编写游戏和其他复杂的、图形密集的程序。
12.2 本章的绘图应用程序
我们将分别使用Quartz 2D和OpenGL ES来构建应用程序。
该应用程序的特点是顶部和底部各有一个工具栏,每个工具栏都有一个分段控件。顶部的控件用于更改图形颜色,底部的控件用于更改要绘制的形状。当用户触击和拖动对象时,程序将用所需颜色绘制所选形状。为了最大程度地降低应用程序的复杂性,一次只绘制一种形状。
12.3 Quartz绘图方法
使用Quartz绘制图形时,通常会向绘制图形的视图中添加绘图代码。例如,可能会创建UIView的子类,并向该类的drawRect:方法中添加Quartz函数调用。drawRect:方法是UIView类定义的一部分,并且每次需要重绘视图时都会调用该方法。如果在drawRect:中插入Quartz代码,则会先调用该代码,然后重绘视图。
12.3.1 Quartz 2D的图形上下文
在Quartz 2D中,和在其他Core Graphics中一样,绘图是在“图形上下文”中进行的,通常,只称为上下文。每个视图都有相关联的上下文。要在某个视图中绘图时,你将检索当前上下文,使用此上下文进行各种Quartz图形调用,并且让此上下文负责将图形呈现到视图上。
下面这行代码将检索当前上下文:
CGContextRef context=UIGraphicsGetCurrentContext();
说明:我们使用Core Graphics C函数,而不是使用Obj-C对象来绘图。Core Graphics的API是基于C的,因此在本章的此部分中编写的大多数代码将由C函数调用组成。
定义图形上下文之后,可以将其传递给各种Core Graphics函数来进行绘图。例如:以下代码将在上下文中绘制一条2像素宽的直线:
CGContextSetLineWidth(context,2.0);
CGContextSetStrokeColorWithColor(context,[UIColor redColor].CGColor);
CGContextMoveToPoint(context,100.0f,100.0f);
CGContextAddLineToPoint(context,200.0f,200.0f);
//之前的操作就像用不可见的墨水在书写一样,下一步是告知Quartz使用CGContextStrokePath()绘制直线
CGContextStrokePath(context);
12.3.2 坐标系
左上角为(0,0),向下移动时y增加,向右移动时x增加。
在OpenGL Es中,(0,0)位于左下角,向上移动时y增加,向右移动时x增加。
使用OpenGL时,必须将视图坐标系转换为OpenGL坐标系。这很容易。稍后讲解
12.3.3 指定颜色
UIKit提供了一个Obj-C类:UIColor。你不能在Core Graphics调用中直接使用UIColor对象,但可以使用他的CGColor属性从UIColor实例中检索CGColor引用。
1,iPhone显示的颜色理论
在现代计算机图形中,通常使用argb来表示颜色。在Quartz 2D中,这些值都是CGFloat类型(与iPhone上的float相同),并且只能在0和1中取值。
2,比所看到的颜色还多
除了rgb之外,还有一个a--Alpha,表示透明程度。
对于大多数操作来说,我们不必担心所使用的颜色模型。我们只需从UIColor对象中传递CGColor,Core Graphics会处理任何所需的转换。在使用OpenGL ES时,记住由于OpenGL ES需要采用RGBA来指定颜色,因此Quartz支持其他颜色模型,这一点非常重要。
UIColor的便利方法创建的实例都是使用RGBA颜色模型。
如果不使用便利方法。下面代码:
return [UIColor colorWithRed:1.0f green:0.0f blue:0.0f alpha:1.0f];
12.3.4 在上下文中绘制图像
使用Quartz 2D,可以直接在上下文中绘制图像。这是Obj-C类(UIImage)的另一个示例,你可以使用此类作为操作Core Graphics数据结构(CGImage)的备用选项。此UIImage类包含将图像绘制到当前上下文中的方法。你需要确定此图像出现在上下文中的位置,方法是:指定一个CGPoint来确定图像的左上角或者指定一个CGRect来框住图像,并根据需要调整图像大小使其适合该框。可以在当前上下文中绘制一个UIImage,如下所示:
CGPoint drawPoint=CGPointMake(100.0f,100.0f);
[image drawAtPoint:drawPoint];
12.3.5 绘制形状:多边形、直线和曲线
例如绘制一个椭圆:
CGRect theRect=CGMakeRect(0,0,100,100);
CGContextAddEllipseInRect(context,theRect);
CGContextDrawPath(context,kCGPathFillStroke);
12.3.6 Quartz 2D 工具示例:模式、梯度、虚线模式
Quartz 2D不像OpenGL那么昂贵,却提供了许多吸引人的工具,尽管这些工具中的许多工具不在本书的讨论范围之内,但你应该知道他们的存在。例如,Quartz 2D支持用梯度填充多边形,而不只是用纯色,并且不仅仅支持实线,而且还支持虚线模式。
12.4 构建QuartzFun应用程序
在Xcode中,使用基于视图的应用程序模版创建一个新项目---QuartzFun。
我们将在视图中执行自定义绘图,因此需要创建一个UIView子类。在该子类中,我们将通过覆盖drawRect:方法进行绘图。创建一个新的Cocoa Touch Classes文件,并选择UIView subclass模版。命名为QuartzFunView.m。
与之前一样,我们将定义一些常量,但这次定义的常量是多个类所需要的,并且不是特定于某个类的。我们将只为常量创建头文件,因此通过以下访问创建一个新文件:从Other栏中选择Empty File 模版,命名为Constants.h。
我们还需要创建两个文件,让UIColor类提供返回随机颜色的方法,即为UIColor创建一个类别。同样使用Empty File模版创建两个文件,将他们分别命名为UIColor-Random.h和.m。
12.4.1 创建随机颜色
UIColor-Random.h:
#import <UIKit/UIKit.h>
@interface UIColor(Random)
+ (UIColor *)randomColor;
@end
UIColor-Random.m:
#import "UIColor-Random.h"
@implementation UIColor(Random)
+ (UIColor *)randomColor
{
static BOOL seeded=NO;
if (!seeded) {
seeded=YES;
srandom(time(NULL));
}
CGFloat red=(CGFloat)random()/(CGFloat)RAND_MAX;
CGFloat blue=(CGFloat)random()/(CGFloat)RAND_MAX;
CGFloat green=(CGFloat)random()/(CGFloat)RAND_MAX;
return [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
}
@end
12.4.2 定义应用程序常量
修改Constants.h:
typedef enum {
kLineShape=0,
kRectShape
kEllipseShape,
kImageShape
} ShapeType;
typedef enum {
kRedColorTab=0,
kBlueColorTab,
kYellowColorTab,
kGreenColorTab,
kRranomColorTab
} ColorTabIndex;
#define degreesToRadian(x) (3.14159265358979323846 * (x) / 180.0)
12.4.3 实现QuartzFunView框架
修改QuartzFunView.h:
#import <UIKit/UIKit.h>
#import "Constants.h"
@interface QuartzFunView:UIView {
CGPoint firstTouch;
CGPoint lastTouch;
UIColor *currentColor;
ShapeType shapeType;
UIImage *drawImage;
BOOL useRandomColor;
}
@property ...
@end
切换到QuartzFunView.m:
#import "QuartzFunView.h"
#import "UIColor-Random.h"
@implementation QuartzFunView
@synthesize ...
- (id)initWithCoder:(NSCoder *)coder
{
if ((self=[super initWithCoder:coder])) {
self.currentColor=[UIColor redColor];
self.useRandomColor=NO;
if (drawImage==nil)
self.drawImage=[UIImage imageNamed:@"iphone.png"];
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if (useRandomColor)
self.currentColor=[UIColor randomColor];
UITouch *touch=[touches anyObject];
firstTouch=[touch locationInView:self];
lastTouch=[touch locationInView:self];
[self setNeedsDisplay];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch=[touches anyObject];
lastTouch=[touch locationInView:self];
[self setNeedsDisplay];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch=[touches anyObject];
lastTouch=[touch locationInView:self];
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
//Drawing code
}
...
@end
由于该视图是从nib中加载的,因此,我们首先实现initWithCoder:。请记住nib中的对象实例将存储为归档对象,这与我们在上一章将对象归档和加载到磁盘所使用的机制完全相同。因此,从nib中加载对象实例时,init:或initWithFrame:都不会调用。而是使用initWithCoder:,因此,任何初始化代码都需要在这里添加。
drawRect:方法是此应用程序的主体部分,目前仅包含一条注释。我们首先需要完成应用程序设置,然后再添加绘图代码。
12.4.4 向视图控制器中添加输出口和操作
单击QuartzFunViewController.h:
#import <UIKit/UIKit.h>
@interface QuartzFunViewController:UIViewController {
IBOutlet UISegmentedControl *colorControl;
}
@property (nonatomic,retain) UISegmentedControl *colorControl;
- (IBAction)changeColor:(id)sender;
- (IBAction)changeShape:(id)sender;
@end
切换到QuartzFunViewController.m:
#import "QuartzFunViewController.h"
#import "QuartzFunView.h"
#import "UIColor-Random.h"
#import "Constants.h"
@implementation QuartzFunViewController
@synthesize colorControl;
- (IBAction)changeColor:(id)sender {
UISegmentedControl *control=sender;
NSInteger index=[control selectedSegmentIndex];
QuartzFunView *quartzView=(QuartzFunView *)self.view;
switch (index) {
case kReadColorTab:
quartzView.currentColor=[UIColor redColor];
quartzView.useRandomColor=NO;
break;
...
case kRandomColorTab:
quartzView.useRandomColor=YES;
break;
default:
break;
}
}
- (IBAction)changeShape:(id)sender {
UISegmentedControl *control=sender;
[(QuartzView *)self.view setShapeType:[control selectedSegmentInedx]];
if ([control selectedSegmentIndex]==kImageShape)
colorControl.hidden=YES;
else
colorControl.hidden=NO;
}
...
@end
12.4.5 更新QuartzFunViewController.xib
打开QuartzFunViewController.xib,先更改视图的类,单击View图标,cmd+4打开身份检查器,将该类从UIView改为QuartzFunView。
接下来,从库中找到Navigation Bar。确保你控制的是Navigation Bar,而非Navigation Controller。将其紧贴在视图窗口的顶部。接下来,从库中找到Segmented Control,并将该控件拖动到Navigation Bar的顶部。放下该控件之后,他应该仍然处于选中状态。捕捉此分段控件任何一侧的调整大小的点,调整他的大小,以便它占据导航栏的整个宽度。你不会看到任何蓝色引导线,但这种情况下,IB会限制该栏的最大大小,因此只需拖动他直到不能再进一步展开为止。
按Cmd+1调出属性检查器,并将分段数量从2改为5.依次双击各分段,将他们的标签分别改为Red、Blue、Yellow、Green和Random。
按住Ctrl,将File's Owner拖到分段控件上,选择colorControl输出口。接下来,选中分段控件,Cmd+2打开连接检查器,从Value Changed事件拖到File's Owner选择changeColor:操作。
现在,从库中拖出Toolbar,放置在窗口底部。然后拖出另一个Segmented Control到工具栏上。
结果是,分段控件在工具栏中居中有点困难,因此我们将提供一点帮助。将Flexible Space bar Button Item从库中拖到位于分段控件左侧的工具栏上。接下来,将另一个Flexible Space bar Button Item拖到位于分段控件右侧的工具栏上。当我们调整该工具栏的大小时,这些项目将使分段控件位于工具栏的中心。单击分段控件将其选中,并调整其大小以使他适合此工具栏,其中左侧两侧各留一点空间。
接下来,Cmd+1打开属性检查器,并将分段从2改为4.标题分别改为Line、Rect、Ellipse和Image。将Value Changed事件连接到File's Owner的ChangeShape:方法。
编译运行应用程序,确保一切正常。目前你还不能在屏幕上绘制形状,但分段控件可以工作。
12.4.6 绘制直线
返回Xcode,编辑QuartzFunView.m,并有以下代码替换空的drawRect:方法。
- (void)drawRect:(CGRect)rect {
if (currentColor==nil)
self.currentColor=[UIColor redColor];
CGContextRef context=UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context,2.0);
CGCOntextSetStrokeColorWithColor(context,currentColor.CGColor);
CGContextSetFillColorWithColor(context,currentColor.CGColor);
CGRect currentRect=CGRectMake (
(firstTouch.x>lastTouch.x)?lastTouch.x:firstTouch.x,
(firstTouch.y>lastTouch.y)?lastTouch.y:firstTouch.y,
fabsf(firstTouch.x-lastTouch.x),
fabsf(firstTouch.y-lastTouch.y)
);
switch (shapeType) {
case kLineShape:
CGContextMoveToPoint(context,firstTouch.x,firstToch.y);
CGContextAddLineToPoint(context,lastTouch.x,lastTouch.y);
CGContextStrokePath(context);
break;
case kRectShape:
CGContextAddRect(context,currentRect);
CGContextDrawPath(context,kCGPathFillStroke);
break;
case kEllipseShape:
CGContextAddEllipseInRect(context,currentRect);
CGContextDrawPath(context,kCGPathFillStroke);
break;
case kImageShape: {
CGFloat horizontalOffset=drawImage.size.width /2;
CGFloat verticalOffset=drawImage.size.height /2;
CGPoint drawPoint=CGPointMake(lastTouch.x-horizontalOffset,lastTouch.y-verticalOffset);
[drawImage drawAtPoint:drawPoint];
break;
}
//注意,这里使用了花括号。GCC在case语句之后的第一行中声明变量时遇到了问题。这些花括号是我们告诉GCC停止抱怨的一种方式。
default:
break;
}
}
由于Quartz 2D是Core Graphics的一部分。因此在构建和运行之前,需要将Core Graphics框架添加到项目中。
在该应用程序中,你不会注意到速度减慢,但是在更复杂的应用程序中,你会看到某些延迟。该问题由QuartzFunView.m中的方法touchesMoved:和touchesEnded:引起。这两个方法都有下面这行代码:
[self setNeedsDisplay];
很明显,这是我们告知视图重新绘制自身的方式。为避免在拖动期间多次强制重新绘制整个视图,我们可以使用setNeedsDisplayInRect:方法。setNeedsDisplayInRect:是一个NSView方法,该方法会将视图区域的一个矩形部分标记为需要重新显示。我们需要重新绘制的不仅仅是firstTouch和lastTouch之间的矩形,还有当前拖动所包围的任何屏幕部分。如果用户触摸屏幕,然后在屏幕上到处乱画,则只需要重新绘制firstTouch和lastTouch之间的部分,将许多不需要的已绘制的内容留在屏幕上。
答案是跟踪受CGRect实例变量中的特定拖动影响的整个区域。在touchesBegan:中,我们将该实例变量重置为仅用户触摸的点。然后在touchesMoved:和touchesEnded:中,使用一个Core Graphics函数获取当前矩形和存储的矩形的并集,然后存储所得到的矩形。此外,还使用该函数指定需要重新绘制的视图部分。该方法为我们提供了受当前拖动影响的正在运行的全部区域。
我们立刻在drawRect:方法中计算当前矩形,以便绘制椭圆形和矩形形状。我们将该计算结果移动到新方法中,以便在所有3个额外i只中使用此新方法,而没有重复代码。准备好了吗?让我们开始吧!
对QuartzFunView.h进行以下更改:
...
{
...
CGRect redrawRect;
}
...
@property (readonly) CGRect currentRect;
@property CGRect redrawRect;
@end
切换到QuartzFunView.m:
...
@synthesize redrawRect;
@dynamic currentRect;
- (CGRect)currentRect {
return CGRectMake (
(firstTouch.x>lastTouch.x)?lastTouch.x:firstTouch.x,
(firstTouch.y>lastTouch.y)?lastTouch.y:firstTouch.y,
fabsf(firstTouch.x-lastTouch.x),
fabsf(firstTouch.y-lastTouch.y)
);
}
- (void)drawRect:(CGRect)rect {
...
switch (shapeType) {
case kLineShape:
...
case kRectShape:
CGContextAddRect(context,self.currentRect);
CGContextDrawPath(context,kCGPathFillStroke);
break;
case kEllipseShape:
CGContextAddEllipseInRect(context,self.currentRect);
CGContextDrawPath(context,kCGPathFillStroke);
break;
case kImageShape:
...
default:
break;
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if (useRandomColor)
self.currentColor=[UIColor randomColor];
UITouch *touch=[touches anyObject];
firstTouch=[touch locationInView:self];
lastTouch=[touch locationInView:self];
if (shapeType==kImageShape) {
CGFloat horizontalOffset=drawImage.size.width/2;
CGFloat verticalOffset=drawImage.size.height /2;
redrawRect=CGRectMake(firstTouch.x-horizontalOffset,firstTouch.y-verticalOffset,drawImage.size.width,drawImage.size.height);
}
else
redrawRect=CGRectMake(firstTouch.x,firstTouch.y,0,0);
[self setNeedsDisplay];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch=[touches anyObject];
lastTouch=[touch locationInView:self];
if (shapeType==kImageShape) {
CGFloat horizontalOffset=drawImage.size.width/2;
CGFloat verticalOffset=drawImage.size.height /2;
redrawRect=CGRectMake(firstTouch.x-horizontalOffset,firstTouch.y-verticalOffset,drawImage.size.width,drawImage.size.height);
}
else
redrawRect=CGRectUnion(redrawRect,self.currentRect);
redrawRect=CGRectInSet(redrawRect,-2.0,-2.0);
[self setNeedsDisplayInRect:redrawRect];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch=[touches anyObject];
lastTouch=[touch locationInView:self];
if (shapeType==kImageShape) {
CGFloat horizontalOffset=drawImage.size.width/2;
CGFloat verticalOffset=drawImage.size.height /2;
redrawRect=CGRectMake(firstTouch.x-horizontalOffset,firstTouch.y-verticalOffset,drawImage.size.width,drawImage.size.height);
}
redrawRect=CGRectUnion(redrawRect,self.currentRect);
[self setNeedsDisplayInRect:redrawRect];
}
@end
仅增加了几行代码,我们就减少了重新绘制视图所需的大量工作(不再需要擦除和重新绘制未受当前拖动影响的视图部分)。像这样妥善处理iPhone宝贵的处理器周期,可以在应用程序性能方面产生巨大差别,尤其是当应用程序变得更加复杂时。
12.5 一些OpenGL ES基础知识
对OpenGL ES的详细介绍本身就是一本书,因此我们在此不对其进行讨论。我们使用OpenGL ES重新创建我们的Quartz 2D应用程序,只是为了让你对其有个基本了解,并且向你提供一些示例代码。
说明:准备在你自己的应用程序中添加OpenGL时,请顺便浏览一下http:www.khronos.org/opengles/,该网页是OpenGL ES标准组的主页。更好的做法是访问此页并搜索单词“tutorial”:http://www.khronos.org/developers/resources/opengles/。
让我们开始创建应用程序吧
构建GLFun应用程序
在Xcode中创建一个基于视图的新应用程序,命名为GLFun。为了节省时间,将文件Constants.h、UIColor-Random.h、UIColor-Random.m和iPhone.png从Quartz-Fun项目复制到这个新项目中。
打开GLFunViewController.h并进行和QuartzFunViewController.h相同的修改:
声明一个IBOutlet UISegmentedControl *colorControl;
- (IBAction)changeColor:(id)sender;
- (IBAction)changeShape:(id)sender;
切换到GLFunViewController.m并进行以下更改:
- (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;
...
}
}
- (IBAction)changeShape:(id)sender {
UISegmentedControl *control=sender;
[(GLFunView *)self.view setShapeType:[control selectedSegmentIndex]];
if ([control selectedSegmentIndex]==kImageShape)
[colorControl setHidden:YES];
else
[colorControl setHidden:NO];
}
这里唯一不同的是我们引用一个名为GLFunView的视图,而不是QuartzFunView的视图。进行绘图的代码包含在UIView的子类中。
继续操作之前,你需要在项目中添加几个文件。在12 GLFun文件夹中,你可以找到4个文件,名称分别为Texture2D.h、Texture2D.m、OpenGLES2DView.h和OpenGLES2DView.m。这些文件中的代码是由苹果公司编写的,他们使得在OpenGL ES中绘制图像更容易。如果你愿意,可以在自己的程序中随意使用这些文件。
OpenGL ES本身并没有sprite或图像;他具有纹理,但纹理必须绘制在对象上。在OpenGL ES中绘制图像的方法是绘制一个多边形,然后将纹理映射到该多边形上,以便他与多边形的大小完全匹配。Texture2D将相对比较复杂的过程封装到一个易于使用的类中。
OpenGLES2DView是一个UIView子类,他使用OpenGL进行绘图。我们设置此视图的目的是便于在一对一的基础上映射OpenGL ES的坐标系和视图的坐标系。OpenGL Es是一个三维系统。OpenGLES2DView将OpenGL 3-D世界映射到2-D视图的像素。注意,尽管视图的OpenGL上下文之间是一对一的关系,但是y坐标仍然是翻转的,因此我们必须将y坐标从视图坐标系转换为OpenGL坐标系。
若要使用OpenGLES2DView类,首先要将其子类化,然后实现draw方法进行实际绘图。还可以在视图中实现所需的任何其他方法,如与触摸有关的方法。
使用UIView子类模版创建一个新文件,命名为GLFunView.m。现在,你可以双击GLFunViewController.xib,然后设计其界面。
完成之后,保存返回Xcode。单击GLFunView.h:
#import <UIKit/UIKit.h>
#import "Constants.h"
#import "Texture2D.h"
#import "OpenGLES2DView.h"
@interface GLFunView:OpenGLES2DView {
CGPoint firstTouch;
CGPoint lastTouch;
UIColor *currentColor;
BOOL useRandomColor;
ShapeType shapeType;
Texture2D *sprite;
}
@property ...
@end
此类与QuartzFunView.h非常相似,但他不使用UIImage来存放图像,我们使用Texture2D来简化将图像绘制到OpenGL ES上下文中的过程。我们还将超类从UIView改成OpenGLES2DView,以便视图支持使用OpenGL ES进行二维绘图。
切换到GLFunView.m:
- (id)initWithCoder:(NSCoder *)coder
{ if (self=[super initWithCoder:coder]) {
self.currentColor=[UIColor redCorlo];
self.useRandomColor=NO;
}
return self;
}
- (void)draw
{
glLoadIdentity();
glClearColor(1.0f,1.0f,1.0f,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);
switch (shapeType) {
case kLineShape:{
if (sprite) {
[sprite release];
self.sprite=nil;
}
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:{
if (sprite) {
[sprite release];
self.sprite=nil;
}
//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:{
if (sprite) {
[sprite release];
self.sprite=nil;
}
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))*xradius)+xOffset;
vertices[i+1]=(sin(degreesToRadian(i))*yradius)+yOffset;
}
glVertexPointer(2,GL_FLOAT,0,vertices);
glDrawArrays(GL_TRIANGLE_FAN,0,360);
break;
}
case kImageShape:
if (sprite==nil) {
self.sprite=[[Texture2D alloc] initWithImage:[UIImage imageNamed:@"iphone.png"]];
glBindTexture(GL_TEXTURE_2D,sprite.name);
}
[sprite drawAtPoin:CGPointMake(lastTouch.x,self.frame.size.height-lastTouch.y)];
break;
default:
break;
}
glBindRenderbufferOES(GL_RENDERBUFFER_OES,viewRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
}
- (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
解析:
在initWithCoder:方法中,我们没有创建Texture2D对象。由于我们绘制的形状没有纹理,因此不需要加载纹理。如果加载纹理,OpenGL ES将在绘制多边形时尝试使用纹理。因此,我们需要采取一些步骤以确保在绘制其他形状时不加载纹理。处理此问题的首选方法就是延迟加载纹理。
下面是draw方法:
首先,我们重置虚拟机世界以便删除任何旋转、转换或已经应用于他的其他变换:
glLoadIdentity();
接下来,我们将背景清除为白色:
glClearColor(1.0f,1.0f,1.0f,1.0f);
glClear(GL_COLOR_BUFFER_BIT);
之后,我们必须通过分割UIColor并从中拖出各个RGB组件来设置OpenGL绘图颜色。
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);
若要绘制直线,我们需要两个顶点,这意味着我们需要包含4个元素的数组。在Quartz中,我们使用CGPoint struct来存放这些点。在OpenGL中,点未嵌入到struct中,相反,我们用组成需要绘制的形状的所有点来填充数组。因此,若要在OpenGL ES中绘制一条从点(100,150)到点(200,250)的直线,我们将创建一个如下所示的顶点数组:
vertex[0]=100;
vertex[1]=150;
vertex[2]=200;
vertex[3]=250;
我们的数组格式为{x1,y1,x2,y2,x3,y3}。
该方法中的下一段代码将两个CGPoint结构转换为顶点数组:
//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;
定义顶点数组之后,指定线宽,使用方法glVertextPointer()将该数组传递到OpenGL ES中,并通知OpenGL ES绘制数组:
glLineWidth(2.0);
glVertexPointer(2,GL_FLOAT,0,vertices);
glDrawArrays(GL_LINES,0,2);
在OpenGL ES中完成绘图之后,我们必须告诉他渲染其缓冲器,并且告诉我们的视图上下文显示新渲染的缓冲器:
glBindRenderbufferOES(GL_RENDERBUFFER_OES,viewRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
为了阐明在OpenGL中绘图的过程由3个步骤组成。首先,在上下文中绘图。其次,完成所有绘图之后,将上下文呈现到缓冲器中。第三,呈现渲染缓冲器,即当像素实际绘制到屏幕上时。
正如你所见,OpenGL示例比较长,当查看绘制椭圆的过程时,Quartz 2D和OpenGL ES之间的差别变得更加明显。OpenGL ES不知道如何绘制椭圆。OpenGL是OpenGL ES的老大哥甚至前辈,他有许多生成常见的二维和三维形状的便利函数,而这些便利函数只是从OpenGL ES分离出来的一部分功能,这使得OpenGL更加精简并且更加适合在嵌入式设备中使用。因此,更多责任落在了开发人员的身上。
为了绘制椭圆,我们将定义一个顶点数组,该数组存放720个GLfloat,这将存放360个点的x和y位置,围绕圆一度一个。我们可以更改点数来提高或降低此圆的平滑度。
GLfloat vertices[720];
接下来,我们将根据存储在firstTouch和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;
然后,我们将围绕圆进行循环,计算围绕圆的正确的点:
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))*xradius)+xOffset;
vertices[i+1]=(sin(degreesToRadian(i))*yradius)+yOffset;
}
最后,我们将顶点数组提供给OpenGL ES,通知OpenGL ES绘制并渲染他,然后通知上下文呈现新渲染的图像:
glVertexPointer(2,GL_FLOAT,0,vertices);
glDrawArrays(GL_TRIANGLE_FAN,0,360);
glBindRenderbufferOES(GL_RENDERBUFFER_OES,viewRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
绘制矩形的方法就不再赘述了。
绘制图像的方法也不再赘述。
不需要告知OpenGL ES将更新屏幕的哪些部分。他会计算出来并且利用硬件加速以最高效的方式绘制。但是,在编译运行之前,需要就爱那个这两个框架链接到你的项目。按照第五章介绍的添加Core Graphics框架的说明,然后选择OpenGLES.framework和QuartzCore.framework。
提示:如果你想创建一个全屏的OpenGL ES应用程序,不必手动构建他。Xcode为你提供了一个实用的模版。该模版为你设置了屏幕和缓冲器,甚至将一些示例绘图和动画代码放置到类中,以便你可以看到放置你的代码的位置。这个模版是OpenGL ES Application模版。
12.6 小结
本章介绍的只是一小点皮毛。