iPhone开发基础教程笔记(十二)--第十二章 使用Quartz和OpenGL绘图

第十二章 使用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 小结
本章介绍的只是一小点皮毛。

你可能感兴趣的:(iPhone开发基础教程笔记(十二)--第十二章 使用Quartz和OpenGL绘图)