《iPhone 3D 编程》第一章:快速入门指南

****************************************************************************

申明:本系列教程原稿来自网络,翻译目的仅供学习与参看,请匆用于商业目的,如果产生商业等纠纷均与翻译人、该译稿发表人无关。转载务必保留此申明。

内容:《iPhone 3D编程》第一章:快速入门指南

原文地址:http://ofps.oreilly.com/titles/9780596804824/chquick.html

译文地址:http://blog.csdn.net/favormm/article/details/6888328

更好的排版下载地址:

****************************************************************************



第一章:快速入门指南

在本章,将从零基础开始,教你学会编写第一个应用程序。这个应用程序就是”HelloArrow”, 它绘制了一个箭头符号, 并能随着朝向的改变而旋转。你将学会如何用OpenGL ES 的API来绘制箭头,而OpenGL ES只是iPhone所支持的图形技术中的一种,于是在开始开发之前你可能会有困惑,哪一门图形技术是你所需要的?其实并没有明显的区分,某一技术是iPhone专有的,而某一技术归属于Mac OX开发的。

苹果公司把iPhone的公有API架构为四层:Cocoa Touch, Media Services, Core Services, and CoreOS,使其简单明了。Mac OS X的架构则的许多延伸,但是任然可以笼统的分为四层,如图1.1。

图1.1.MacOS X 与 iPhone 开发架构

在最底层,Mac OS X与iPhone共享他们的核心架构与核心子系统,而Darwin泛指这些共享的组件。

尽管这两个平台的架构都如此相识,但是在处理OpenGL这一层是有一定区别的。在图1.1中,用粗体显示了几个OpenGL相关的类名。其中NSOpenGLView是Mac OS X中特有的,所以iPhone中没有,而EAGLContext与CAEGLLayer只有iPhone中才有。除了这些区别外, OpenGL 的API在这两个平台上也有不同,比如,Mac OS X支持 全生态的OpenGL,而iPhone则支持裁剪过的OpenGL, 叫着OpenGL ES。

iPhone支持的图形技术:

Quartz 2D Rendering Engine

支持alpha通道,层与多采样抗锯齿的失量库。这个库在MacOS X上一样可用。如果你的应用想运用这个技术,就必须关联Quartz Core.framework(framework是苹果公司对库与资源的一种打包方式)

Core Graphics

Quartz的C语言接口,同样在Mac OS X也是有效的。

UIKit

iPhone上原生态的窗口框架,除此之外,UIKit还封装了Quartz的Objective_C版。在Mac OS X上与此框架相似的是Cocoa的组件AppKit。

Cocoa Touch

iPhone开发架构中的层概念,它包括了UIKit与其它几个framework.

Core Animation

Objective-C封装的可以轻松实现复杂动画的framework.

OpenGL ES

渲染2D与3D图形并支持硬件加速的底层c语言的API 集.

EAGL

一些建立OpenGLES与UIKit之间桥梁的API. 其中EAGL的类(如CAEGLLayer)是在Quartz Core framework中定义,而其它的类(如EAGLContext)则是在OpenGL ES framework中定义的。

本书主要讲解OpenGL ES,它是唯一一个在上面例表中出现,但不是苹果公司特有的技术。OpenGL ES的标准是由KhronosGroup这个公司制定。不同的OpenGL ES生产商都支持同样的核心API,这样很轻松就可以写出可移植代码。生产商也可以添加一些已经正式定义好了的扩展接口到API中,而iPhone支持大量的这些的扩展,在本书的后面章节将会覆盖这些内容。

转向苹果开发阵营

很明显,你需要一台Mac机开发iPhone应用,然后上App Store. 具有PC开发背景的开发人员完全可以消除心中的恐惧,据我PC到Apple转变的经验,除了开始由于键盘不同造成不习惯外,其它完全适应。

Xcode是苹果公司推荐的开发环境。如果你是Xcode新手,那你最好把它当着邮件客户端而非IDE。它的布局很直观,易用,学完快捷键后,我更感觉它的工作效率,并且用它工作很有趣。比如,当你打完一个结束分隔符如”)”,那么与之对应的开始分隔符”(”就会立刻变大,就好像浮出屏幕一样。这个效果很微妙,感觉也很实用,唯一不足就是缺少一个如动物的音效。可能下一代的Xcode,苹果会引入该功能。

Objective-C

现在是时候来说说被忽略了的Objective-C了。有些时候,你可能会听说:如果想开发iPhone就必须学习Objective-C。其实不然,只要是不与UIKit打交道的部份,你完全可以用纯C/C++的方式来写。这一点在OpenGL的开发当中尤其可以得到证明,因为它是CAPI。本书中的代码都是用C++写的,只有在iPhone系统与OpenGL ES建立关系的时候才会用到Objective-C。

苹果公司采用Objective-C得渊源于NeXT, NeXT是乔布斯成立的另一家公司,其拥有的技术在当时是首屈一指,可谓超前时代很多很多。但是它还未能平民化而被苹果公司于1997年所收购。因此,到现在为止,你可以在苹果的API中看到有NS前缀,当然iPhone开发也不例外。

有的人说Objective-C并没有C++那样复杂那么强大,其实这也不是什以坏事。在许多情况下,用正确的工具做正确事,Objective-C也有对号入坐的时候。由于它是C的超集,所以学习起来一点不难。

但是,在3D图形开发中, 我觉得某些C++的功能是不可缺少的。运算符重载让向量计算像原生计算方法一样成为可能。模板可以根据数字参数来形成向量或是矩阵。更重要的是,C++被广泛用于各个平台,而且在许多方面,游戏开发中都用得上。

OpenGL ES简明历史

在1982年,斯坦副在学一个叫JimClark的教授创办了世界上第一家计算机图形公司:Silicon Graphics ComputerSystems(硅谷图形计算机系统), 也就是后面的SGI家公司。由于SGI的工程师需要一套标准的3D移动与操作的方法,所以就开始设计出一套叫IrisGL的API。在90年代, SGI整理并发布于众,做为行业标准,于是OpenGL就诞生了。

在近年,图形学技术的飞速发展远远超出了摩尔定律。.[1]OpenGL在保证向后兼容的情况下更新了无数代。于是许多的开发者都认为API过于臃肿,特别是随着手机移动设备革命的带动,精简OpenGL的需求已史无前例的明显。于是在2003的秋季SIGGRAPH会议中,Khronos组织声明了 OpenGL for Emebedded Systems(OpenGL ES)。

OpenGL ES一出现,便受到许多产商的推宠,在许多设备上得到技持,如iPhone,Android, Symbian, Playstation 3.

苹果的所有设备上都至少支持OpenGLES API 1.1版本,交在其核心标准上加入了强劲的扩展,包括顶点缓冲对象,多纹理支持, 这两个扩展都会在后面章节中所介绍。

在2007年3月,Khronos组织发布了OpenGL ES 2.0的标准,它的出现,颠覆性的打破了向后兼容的规则,因为它剔除了许多固定通道的渲染功能,取而代之的是shadinglanguage。新的标准使操控图形的API更加简洁,同时把具特色的控制交到了开发者手里,因此许多开发者(包括我)都觉得ES2.0比ES1.1更优雅。有了这两套API,那么对同一个问题就有两种不同的解决方案。用ES2.0,就算是写一个简单的Hello World也需要做更多的额外工作。在很长的一段时间内OpenGLES 1.1的追求者可能还会继续使用,由于它的运行时低负荷。

选择适当的OpenGL ES版本

苹果的新款手持设备,如iPhone3GS,同时支持ES1.1与ES2.0,因为这些设备上有可编辑化图形管道来运行图形命令,而传统的则是运行定点数学运算。老的设备如第一代iPod touch,iPhone与iPhone 3G只有一个固定的图形渲染通道,因此只支持ES1.1。

在你开始写第一行代码之前,请确定你的图形需求。当然用最新最完善的API固然是好事,但是你得记住,支持ES 1.1的设备占大多数,因此它可能为你的应用打开更多的市场,同时开发ES 1.1应用的工作量会更少,前提是你的图形需求不高。

当然,许多高级特效可能只能用ES2.0实现,正如我前面所说, 我相信它在开发当中更优雅更有效。

做个小总结,你可以从下面四种方法中选择来开发你的应用:

  1. 只用OpenGL ES 1.1。
  2. 只用OpenGL ES 2.0。
  3. 在运行时判断是否支持ES 2.0,如果支持则用,如果不支持就用ES 1.1。
  4. 为ES 1.1与ES 2.0分别发布一个独立版本。(这方法可能有点冗余)

在本书中我将采用第三种方法,本节的HelloArrow示例你将会看到。一个明智的选择!

开始吧

前提是你已有了一架Mac电脑,接着第一步就是去苹网iPhone开发官网并下载SDK。只有拥有了SDK(免费的),你才有“利器”来开发复杂的应用程序,并在iPhone模拟器上测试它。

iPhone模拟器不能模拟全部功能,比如重力感应,也不能全部反映iPhone设备的OpenGL ES 真实能力。比如, OpenGL的平滑线条功能能使渲染的时候达到多采样抗锯齿的效果,而真机上则不行。另一方面,真机有一些扩展特性,而模拟器上则不一定有。(随便说一下,我们将在本书后面章节介绍多采样的缺点。)

说了这么多,现在告诉你,你并不需要真机,因为我确保所有的示例都能在模拟器上运行,退一步说,即使模拟器不支持一些功能,这种情况当然很少,我都会巧妙的方法解决。

如果你有一部iPhone,并愿意支付费用加入苹果iPhone开发大家庭,这样就能部署你所开发的应用到真机上。(在写本书的时候这费用是$100,泽注:明明是$99/年)。其实加入开发会员并不是很痛苦的过程,我申请的时候苹果很快就接受了我的申请。如果你申请的时候花费了很长的时间,那么我建意你在等待的这段时间里,先用模拟器开发着。其实在我开发过程当中,基本每天都用模拟器开发,因为它调试运行的速度远比在真机上要快。

本书是以教程的方式撰写。可能由于你所用的开发工具版本不同,那么本书所写步骤与你的操作可能有细微差别。特别是涉及到Xcode的用户界面的细微偏差,比如,一个菜单变了名字,名是移动了位置。但是,书中的示便代码是经过设计,可以向前兼容的。

安装iPhone SDK

iPhone的SDK可以在这儿下载:http://developer.apple.com/iphone/

它是一个以.dmg为后缀的文件, 这是苹果标准的磁盘文件格式。当你下载安以后,它会在Finder的窗口中自动打开,如果没有,那么你去磁盘上找到该文件,并打开它。这个磁盘镜像文件常常包括三个文件:一个“关于”的pdf文件,一个子目录,一个安装包实体,安装包实体的图标是一个纸盒子。双击打开安装实体,并进行多个下一步操作。在先择安装组件的时候,默认就行了。当你不想要这些组件的时候,在磁盘上找到它们并删除就行了。

小知识:

作为一个苹果开发者,Xcode可能是你最常用的工具,我建意你将其拖到屏幕下方的dock栏上。你可以在/Developer/Application/Xcode目录下找到Xcode。

小知识:

如果你用惯了PC,那么开始用苹果的时候,会觉得它的窗口系统很不习惯。我建意你用苹果内置的Expose或Space桌面管理器。Expose就像是无限延伸你的窗口,Space感觉就像是多个虚拟桌面。我用过许多虚拟桌面管理器,觉得Space是最好的。

用xCode编译OpenGL的模板应用程序

当第一次打开Xcode的时候,它会弹出一个欢迎对话框。选择上面的Create a new Xcode project按钮。(如果你的对话框被关闭了没有显示出来,那么可以选择菜单里的File->New Project来创建新工程。)现在你会有一个如图1.2的对话框出现,它包括Xcode提供的工程模版。我们要用到的是OpenGL ES Application这个模版,注意到没,它是在iPhoneOS这一栏下面。这个模版并没有什么特别的,只不过它支持OpenGL,作为一个新手,选择它是明智的。

图1.2

选择模版后,会有一个对话框要求你输入工程名字。完成之后,你就可以看到Xcode的工作窗口了。在Build菜单里有一个Build and Run,选择它开始编译并运行,你也可以按快捷键⌘-Return。编译完成后,模拟器将会被启动,其中有一个方形的图形在模拟器上上下移动,如图1.3所示。如果你想退出这个应用程序,直接按⌘-Q即可。

图1.3 OpenGL模版应用

部署到真机

这一步并不是必须的。如果你想要布署到真机上,那么你必须注册苹果的iPhone Developer Program。这样才允许你真机调试。获取这个准证是一个繁杂的过程,但是幸好一台设备只需做一次。现在苹果推出了一个简单的方法来处理这些流程。你可以登录iPhoneDev Center (http://developer.apple.com/iphone/), 并进入iPhone Developer Program Portal里进行操作。

当你真机得到认证后,你可以打开Xcode的Organizer窗口(快捷键是Control-⌘-O),并展开左边Provisioning Profiles这一栏,确保你的设备名列其中。

现在你可以回到Xcode的主窗口,在左上角Overview这个下拉列表中选择你的真机,然后编译运行(⌘-Return),于是在你的iPhone上就会出一移动的方形图形。

固定通道渲染Hello Arrow

在前面的小节中,熟悉了开发环境,苹果提供的OpenGL工程模板应用。但是如果要理解其工程原理, 还得从其础学习。本节将用OpenGL ES 1.1的方法,从头到尾开发一个简单应用。OpenGL ES 1.x的另一个叫法是fixed-function,与之对应的是建立在shader基础上的OpenGL ES 2.0 。我们将在本章的后面内容介绍如何修改为支持shader的应用。

为了与本书的主题相符,我对经典的HelloWorld做了一点点变化,现在就开始吧。在后面的内容中你会学到,在OpenGL中渲染的形状可以用三角形构造出来。 比如,我们可以用两个重叠的三角形绘制一个简单的图形,如图1.4所示。如有雷同,纯属巧合。

图1.4 由两个三角形组成的箭头形状

为了增加趣味性,本示例的箭头始终指向上,即使用户改变方向。

架构你的3D应用

如果你喜欢用Objective-C,那么你可以通过任何手段在任何处使用它。由于本书考虑到跨平台的代码重用, 于是只有在万不得已的情况下才用Objective-C。图1.5展示了两种架构来重用C/C++所写的代码,因为iPhone上的glue是用Objective-C写的。右边的方法是从rendering engine中分离出一个application engine(虽然同属逻辑部份), 而本中稍微复杂的示例就是采用这种方法 。

图1.5 3D应用架构

图1.6中所介绍的方法是,设计一套通用的渲染接口,并确保各个平能可用。本书代码中中的IRenderingEngine就是这个计设,当然你可任意命名。

图1.6 跨平台OpenGLES 应用

有了IRenderingEngine这个接口,你就可以创建多个渲染引擎,如图1.7所示。这样就可以达到“支持ES2.0的时候用2.0,不支持的时候用ES 1.x”,这方法就是前面所说的方法三。 Hello Arrow就用这个方法。

图1.7 同时支持ES2.0与ES 1.1的应用

随着我们关于HelloArrow代码的讲解,你将学到关于图1.7中的点点滴滴,你将会创建三个类:

RenderingEngine1 与RenderingEngine2 (可移植的C++)

大部份的工作都在这个类中,其中对OpenGL ES的调用也在其中。RenderingEngine1用 ES 1.1 whileRenderingEngine2用 ES 2.0。

HelloArrowAppDelegate (Objective-C)

这是一个继承自NSObject并遵循UIApplicationDelegate协议的Objective-C类。(“遵循协议“与java或C#中的”接口实现“类似。)这个类中没有用OpenGL 或 EAGL,它只是简单的初始化GLView并在应用退出的时候释放内存。

GLView (Objective-C)

继承自标准的UIView类,并用EAGL去初始化OpenGL所需的渲染surface。

从头开始

启动Xcode并用最简单的一个模板:Windows-BasedApplication创建工程,命名这个工程为HelloArrow。Xcode捆绑了一个叫Interface Builder的工具,它可以用来设计与UIKit(Mac OS X下是AppKit)相关的用户界面。本书中不打算计解它,因为3D应用中不常用。为了执行效率,苹果不建意UIkit与OpenGL混用。

注意

对于一些简单的3D应用,也不需要遵循这条规则,你可以向你的OpenGL view添加一些UIKit控件也无防碍。我们将在后面一章节“OpenGL ES与UIKit混用”中介绍。

选择步骤:创建一个干净工程

下面的步骤将移除工程Interface Builder的支持。你可以选择不这样做,但我习惯性的用一个干净的工程(即没有Interface Builder的工程)。

1. Interface Builder生成的文件是xib文件,它是一个xml类型的文件,负责定义UI成员。由于你创建的是一个OpenGL应用,根本不需要这个文件,所以可以删除之。在左边的Groups & Files组中,找到Resources这个文件夹(有些情况下是Resources-iPhone),删除所有以.xib为后缀的文件,当提示的时候,选择移到回收站。

2. xib文件一般会被编译成nib的二进制文件,它在运行时被用来组建UI。为了让OS不加载nib文件,你需要在应用属性中将其删除。在工程Resources目录下找到HelloArrow-Info.plist文件,双击打开它,删除带有Main nib file这样的这一行(在靠下面的位置处)。你可以先选择这一行,然后按Delete键。

3. 由模版生成的工程,在nib中会自动关联应用的代理,由于我们不需要nib文件了,所以需要手传递字符串来动设置应用代理。在Other Sources里,打开main.m文件,你会发现UIApplicationMain这个方法的最后一个参数为nil,我们将其修改为应用的代理类(如,@"HelloArrowAppDelegate")。@这个前缀说明这是一个Objective-C的字符串,并非C-style的字符串。

4. 由模板生成的工程有一个属性表示应用代理,Interface Builder与之关联。现在不需要了。打开HelloArrowAppDelegate.h(在Classes目录中)删掉@property这一行来删除属性声明,打开HelloArrowDelegate.m文件删掉@synthesize这一行来删除属性定义。

连接OpenGL与Quartz库

在苹果开发阵营里的framework就相当库,从技术角度说它是资源的捆绑。Bundle就是一个特殊的目录,表现为一个文件的属性,这些与MacOS X上 的都差不多。比如,应用程序往往就是bundles,找到Applications目录下的一个应用程序,在它的图标上右键弹出菜单项,有一项是show package contents,点击它你会看到其表壳下的内容。

你需要加入一些需要的framework到工程当中。选择Frameworks这个group,然后点击Action图标,或鼠标右击或按住control+鼠标左击来弹出菜单。然后选择Add->Existing Frameworks。选择OpenGLES.Framework并点击Add按钮。同时会弹出一个对话框 ,一切按默认选择,接受就行。然后以同样的方式加入QuartzCore.framework。

注意

可能你会问,我们不是写OpenGL ES的应用程序吗,为何还要用Quartz呀?这是因为Quartz拥有展现到屏幕的层对象,OpenGL也需要这个层对象, 它是CAEGLLayer的一个实例, CAEGLLayer派生于CALayer, 而这些类都是定义在QuartzCore framework中的。

子类化UIView

UIView控制屏幕中的一个矩形区域,处理用户事件,充当子view的容器。大部份的标准控件,如按钮,划块,输入框都是UIView的子孙类。在本书的示例中,我们应该避免用这些控件,由于UI部份需求量简单,我们完全可以用OpenGL自绘简单的控钮与各种小窗口。

由于在iPhone中,所有图形绘制都必须在一个view中进行,所以我们的HelloArrow必须定义一个UIView的子类。选对Classes这个目标,然后在Xcode的工具栏点击Action,在弹出的菜单中选择Add->New file。在CocoaTouch Class这个分类下,选择Objective-C类模版, 并在Subclassof menu中选择UIView。随后弹出的对话框中输 入名字GLView.mm,并选择同时生成相应的头文件。.mm的后缀表示这个文件同时支持c++与Objective-C,在GLView.h中你可以看到如下内容:

#import

@interface GLView : UIView {

}

@end

对于C/C++高手而言,这种语法有点感冒,等一下看到方法实现的语法后更有此感受。但是不用担心, 如果习惯了将会觉得非常轻松。

#import与#include的功能差不多,只不过它不可能产生在同一个文件包括两次头文件的错误,与 C/C++中加入了#pragmaonce的功能类似。

Objective-C的关键字都是以”@”为前缀的。@interface表示类的声明开始,@end表示类的声明结束。一个文件里可以包括多个类的声明,于是可以出现多个@interface程序块。

现在你可能都已猜到,上面的代码片段其实就是定义了一个类,类名是GLView,继承自UIView。有一点需要明确的是,数据的声明应放在大括号内, 而方法的声明则应放在结束在括号与@end之间,如下代码:

#import

@interface GLView : UIView {

// Protected fields go here...

}

// Public methods go here..

@end

在数据区声明的数据,默认是保护型的,当然你可以用关键字@provate改为private型。继续上面的代码,我们来完善它,如示例1.1所示, 我们需要引入几个与OpenGL相关的头文件。

示例1.1 GLView类的定义

#import

#import

#import

#import

#import

@interface GLView : UIView {

EAGLContext* m_context;

}

- (void) drawView;

@end

上面加入的m_context是用来管理我们的OpenGL 上下文的,它是一个EAGL类对象。而EAGL是苹果特有的API,是它让iPhoner操作系统与OpenGL关联起来的。

注意

每一次你调用OpenGL的API, 不只是修改状态,还作用了上下文。就算在一个支持多线程的系统上, 也只能同时只能有一个当前上下文。对于iPhone,由于移动设备的资源限制,加之你的应用几乎不可能使用多个上下文,所以我不建意用多个上下文。

如果你是C/C++背景的程序员,你可能会觉得drawView这个方法声明得有点奇怪。如果你对UML语法熟悉的话,你将不会有这种奇怪感觉了, 但是在这儿与UML中的“-”表示私有方法”+”表示公有方法还是有些差别,在Objective-C中,”-”表示实例方法,”+”表示类方法。(在Objective-C中的类方法与C++中的静态方法有些类拟,不同的是,在Objective-C中,类本身就是真真的对象。)

再来看看Xcode生成的GLView.mm文件。在@implementation与@end中间的就是GLView类的定义。Xcode自动生成了三个方法:initWithFrame, drawRect(有可能被注释了), dealloc。注意这三个方法都没有在头文件声明,而是自动生成的。照此看来,我们发现Objective-C中的方法与C中的方法用法都差不多,用的时候都需要提前声明。我通常把所有主方法都声明在头文件中,这样与C++中类声明的方法保持一致。

让我们仔细来看一看第一个方法:

- (id) initWithFrame: (CGRect) frame{

if (self = [super initWithFrame:frame]) {

// Initialization code

}

return self;

}

这是一个Objective-C的初始化方法,有点类似C++中的构造函数。返回值类型由一个小括号包住,有点像C中的强制类型转换。if那一句同时完成了几个处理:首先调用基类的initWithFrame,并将返回结果赋值给self, 最后再判断是否成功。

In Objective-C parlance, you don't call methods on objects;you send messages to objects. The square bracket syntax denotes a message.Rather than a comma-separated list of values, arguments are denoted with awhitespace-separated list of name-value pairs. The idea is that messages canvaguely resemble English sentences. For example, consider this statement, whichadds an element to aNSMutableDictionary:

在Objective-C的用法当中,还值得一提的是,你并不是调用某个实例的方法,而是向这个实例发送消息。中括号表示一个消息。参数列表不再是以逗号分开的方式,而是用以空格分隔开的名字-值的方式表示。这种方式的好处是可以产生可读性的英语句子。比如,加入一个元素到NSMutableDictionary:

[myDictionary setValue: 30 forKey: @"age"];

如果你试着去读这句代码,将会生成一句英语句子,当然需要适当的排序。

到现在为止,Objective-C相关知识介绍得差不多了。回到HelloArrow这个应用上来。在GLView.mm中加入layerClass这个方法,代码片段如下:

+ (Class) layerClass{

return [CAEAGLLayer class];

}

这儿重写了layerClass方法,并返回支持OpenGL类型的layer。这个类方法有点类似其它语方中的typeof操作。它返回的是的对象表示类型本身,而非一个类型的实例。

注意

"+"前缀表示重写的这个方法是类方法,并不是成员方法。这种重写的特性是Objective-C具有的,其它语言很少这样。

现在,回到initWithFrame这个方法中,我们在if的执行体中初始化EAGL, 代码如示例1.2。

示例1.2 EAGL实始化

- (id) initWithFrame: (CGRect) frame

{

if (self = [super initWithFrame:frame]) {

CAEAGLLayer* eaglLayer = (CAEAGLLayer*) super.layer; //[1]

eaglLayer.opaque = YES; //[2]

m_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1]; //[3]

if (!m_context || ![EAGLContext setCurrentContext:m_context]) { //[4]

[self release];

return nil;//[5]

}

// OpenGL Initialization

}

return self;

}

代码分析:

[1] 获取基类(UIView)的layer属性,并将它从CALayer强制转换为 CAEAGLLayer。这样做是安全的,因为我们重写了layerClass方法。

[2] 设置获取到layer的opaque属性为YES,表示我们不再用Quartz来处理半透明度。在开发OpenGL相关的应用时,这是苹果建意的方法, 你不必担心透明度问题,因为OpenGL可以处理alpha融合。

[3] 创建EAGLContext对象,启关联OpenGL ES的版本号,在此我们用的是ES1.1。

[4] 设置当前EAGLContext, 这样一来,在当前线程上的OpenGL调用都与此上下文相关。

[5] 如果上下文创建失败或设置当前上下文失财, 就结束并返回nil。

在示例1.2中,实例化EAGLContext的方法alloc-init的设计模式在Objective-C中是非常常见的。在Objective-C中生成一个实例往往需要两步:分配空间与实始化。但是,许多类的类方法使得生成实例更为简单。比如,用utf-8编码的方式转换NSString, 传统的方法是:

NSString* destString = [[NSString alloc] initWithUTF8String:srcString];

但是我更喜欢这样写:

NSString* destString = [NSString stringWithUTF8String:srcString];

不只是因为它更简洁,还因为它加放了自动释放机制,因此不需要再发送release消息给对象了。

接着完善OpenGL的实始化。用示例1.3的代码代替上面的注释//Open GL Initialization

示例1.3 OpenGL 实始化

GLuint framebuffer, renderbuffer;

glGenFramebuffersOES(1, &framebuffer);

glGenRenderbuffersOES(1, &renderbuffer);

glBindFramebufferOES(GL_FRAMEBUFFER_OES, framebuffer);

glBindRenderbufferOES(GL_RENDERBUFFER_OES, renderbuffer);

[m_context renderbufferStorage:GL_RENDERBUFFER_OES

fromDrawable: eaglLayer];

glFramebufferRenderbufferOES(

GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES,

GL_RENDERBUFFER_OES, renderbuffer);

glViewport(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame));

[self drawView];

示例1.3中,开始处有两个OpenGL类型变量,一个是renderbuffer,另一个是framebuffer。简要说明一下,renderbuffer是一个2维结构,里面存一些数据(在此存的是颜色数据),framebuffer是捆绑了多个renderbuffer的结构。在后面的章节中,你将学到多更关于framebufferobjects(FBOs)的知识。

注意

在OpenGL ES 1.1标准中,FBOs并不在其中, 但它做为高级功能而在OpenGL扩展中出现,当然所有iPhone都支持这个扩展。在OpenGL ES 2.0 标准中,FBOs是原生态支持的。在这么简单的HelloArrow应用中,都需要如些高级的功能,感觉有点奇怪。其实所有的iPhone OpenGL应用,它们绘制图形并显示到屏幕上的过程都需要FBOs的参与。

细心的读者可能发现,readerbuffer与framebuffer都是GLuint类型,这种类型是OpenGL用来管理各种对象的。当然你也可以用unsigned int代替GLuint,因为他们本来是一样的,但是我建意你不要这么做。如果用到OpenGL相关的API的时候,还是建意用GL前缀的变量进行参数的传递。因为这样会使你的代码更具有可读性,知道哪些地方是与OpenGL有关的。

从示例1.3中我们可以看到,创建好framebuffer与renderbuffer后,紧接着就将它们与渲染通道进行绑定,当然我们也可以在后续操作中对其进行修改或取消绑定。绑定完成后,向EAGLContext的实例发送一个renderbufferStorage消息就可以创建一个storage。

注意

关于离屏页,你得用glRenderbufferStorage这个OpenGL的API来创建之,这样一来,你的renderbuffer就与一个EAGL layer关联起来。本书后面内容中会更多的涉及离屏页。

接着一行代码里,glFramebufferRenderbufferOES将renderbuffer依附到framebuffer。

接着来看glViewport这个API,你可能不理解它的作用,其实你现在可以把它想为设定坐标系,在第二章的数学与抽象中你会更加清楚它的来胧去脉。

最后一行调用了drawView这个方法,那么我们就来实现这个方法,代码如下:

- (void) drawView

{

glClearColor(0.5f, 0.5f, 0.5f, 1);

glClear(GL_COLOR_BUFFER_BIT);

[m_context presentRenderbuffer:GL_RENDERBUFFER_OES];

}

OpenGL的“clear”机制可以帮我们将buffer填充为单一纯色。首先选定填充的颜色为灰色,有四个值(红,黄,蓝,alpha)。接着就用选定的颜色填充buffer。最后一行,让EAGLContext对象将renderbuffer的内容显示到屏幕上。大多数的OpenGL程序都是先渲染到一个缓冲区内,然后以原子操作的方式显示到屏幕,就像现在我们这样。

Xcode提供的drawRect方法已对你没用了,因为它是基于UIKit的应用程序里刷新机制所调用的方法,在3D应用中,你需要更为精确的方法来控制图形绘制。

到此,你差不多有了一个可以看效果的OpenGLES程序,但是收尾工作还没做完。在GLView对象销毁的时候,你必须释放一些空间。你得修改dealloc方法为如下代码:

- (void) dealloc

{

if ([EAGLContext currentContext] == m_context)

[EAGLContext setCurrentContext:nil];

[m_context release];

[super dealloc];

}

现在你可以编译运行了,是不是还是不能看到灰色背景呀?这是正常的,先别忙,因为我们还有些事没做,还得修改应用代理类。

修改应用代理

The application delegate template (HelloArrowAppDelegate.h) that Xcode provided contains nothing morethan an instance ofUIWindow. Let's add a pointer to an instance oftheGLViewclass along with a couple methoddeclarations (new/changed lines are shown in bold):

由Xcode模版生成的的应用代理类(HelloArrowAppDelegate.h)里只有一个UIWindow变量。现在我们加入GLView类的一个变量(新加入/修改了的代码以粗体显示):

#import

#import "GLView.h"

@interface HelloArrowAppDelegate : NSObject {

UIWindow*m_window;

GLView* m_view;

}

@property (nonatomic, retain) IBOutlet UIWindow *m_window;

@end

如果你是按着前面的小节中创建干净工程的方法来创建工程,你就不会看到@property这一行代码。InterfaceBuilder就是用Objective-C的属性机制来关联对象的,但是在本书中我们都不会用它。再次简要说明一下,@property关键字声明属性,@synthesize关键字定义附属方法。

注意到没,Xcode模版生成的是window成员变量,我反它重命名为m_window。这种命名方式将贯穿本书。

注意

我建意用Xcode的Refactor功能重命名变量,因为它可以帮你与之对应的属性(如果它存在)。选中window这个变量,鼠标右键,并选择Refactor。如果你没按前面的方法来创建一个干净的工程,那你必须用这种方法来重命名,因为xib文件已与window建立了关联。

现在来分析 HelloArrowAppDelegate.m这个文件。还记得吗,我们创建工程的时候是选择的”Window-Based Application”这个模版,模版就帮我们生成了代理类的基本框架,它实现了applicationDidFinishLaunching与dealloc这两个方法。

注意

由于你需要这个方件同时包含C++与Objective-C代码,所以你得修为后缀名为.mm。在相应的文件上鼠标右键,在弹出菜单中选择Rename。

最终代码如示例1.4

#import "HelloArrowAppDelegate.h"

#import

#import "GLView.h"

@implementation HelloArrowAppDelegate

- (BOOL) application: (UIApplication*) application

didFinishLaunchingWithOptions: (NSDictionary*) launchOptions

{

CGRect screenBounds = [[UIScreen mainScreen] bounds];

m_window = [[UIWindow alloc] initWithFrame: screenBounds];

m_view = [[GLView alloc] initWithFrame: screenBounds];

[m_window addSubview: m_view];

[m_window makeKeyAndVisible];

return YES;

}

- (void) dealloc

{

[m_view release];

[m_window release];

[super dealloc];

}

@end

示例1.4 中构建了window与view两个对象,都是全屏的。

如果你不是控前面的方法创建干净的工程,你得做如下小修改:

在@implementation:后面加入一行代码

@synthesize m_window;

前面说过,@synthesize这个关键字是定义属性的附属方法,Interface Builder就用这些方法去处理的。

编译并运行,这回是不是可以看到灰色背景了呀? 高兴吧!

设置应用图标与启动画面

应用程序的图标是可以自定义的,创建一个57*57大小且格式为PNG的图片,将其放在Xcode工程的Resources分组下。如果你的图片不是在工程目录下,那么Xcode会弹出一个对话框问你是否copy到工程目录,我们选择”Copy itemsinto destinaiton group’s folder(if needed)”。然后打开HelloArrow-Info.plist(也在Resources分组下),找到Icon file这一行, 在后面输入你的PNG文件名。

iPhone会给你的图标自动加上圆角与光泽效果。如果你不想要这个效果,那么打开HelloArrow-Inof.plist文件,随便单击一个右边的+按钮,在左边列选择”Icon alreadyincludes gloss and bevel effects”, 右边输入YES。如果你的图标没有这种光泽效果,请不要这样做,因为苹果希望所有的图标在SpringBoard(iPhone内置应用)中都保持一致。

为了在Spotlight搜索中系统设置界面中看到应用图标,苹果建意同时提供一张29*29的图片。方法在上面介绍过了,只不过这张图片名字必须为Icon-Small.png,也不面要再修改plist文件。

对于应用的启动画面,方法与上面的小图标的方法差不多,只是名字必须为Default.png,也不需要修改plist文件。如果你想很好的全屏效果,那么这张图片的大小得是320*480, 其它大小都会使图片拉伸,效果很丑。其实苹果的文档说了,这张图片根本不算什么启动画面,这样做的目的只是为了更好的用户体验。并不需要你有多么有创造性的logo,苹果最初只要让你模拟程序的启动画面而已。当然, 现在有很多应用程序都忽略这条了。

处理地状态栏

现在你的应用将屏幕填充为了灰色,但是状态栏仍然显示在屏幕的顶部。一种解决办法是在didFinishLaunchingWithOptions:中加如下面代码:

[application setStatusBarHidden: YES animated: NO];

这种方法有一个问题,就是在启动画面结束之前状态栏仍然存在。下面让我们来让它在一开始的时候就去掉状态栏的显示。打开HelloArrowInfo.plist文件,新建一行,选择”Statusbar is initially hidden”, 然后勾上后面的选择框。

当然,大多数情况下,为了让用户知道当前电量与网络连接情况,状态栏是显示的。如果你的应用背景是黑色的,你可以在plist文件中新增一行并选择”Status barstyle”,在后面一列选择black style。如果不是黑色背景,那么semi-transparentstyle更加适合你。

定义并使用RenderingEngine 接口

到此,我们已有准备好了HelloArrow所需的大部份工作,如果按照图1.7中的架构,我们现在还缺少绘制引擎。那么我们加入一个C++的接口文件到工程。选中Classes分组,鼠标右键,弹出菜单中选择Add->New, 然后选择Cand C++, 再在右边选择Header File。我们命名这个新加文件为IRenderingEngine.hpp。注意文件后缀,.hpp表示这个文件只支持C++语法,不支持Objective-C语法。成功加入文件后,在其中写入示例1.5的代码。

示例1.5 IRenderingEngine.hpp

// Physical orientation of a handheld device, equivalent to UIDeviceOrientation

enum DeviceOrientation {

DeviceOrientationUnknown,

DeviceOrientationPortrait,

DeviceOrientationPortraitUpsideDown,

DeviceOrientationLandscapeLeft,

DeviceOrientationLandscapeRight,

DeviceOrientationFaceUp,

DeviceOrientationFaceDown,

};

// Creates an instance of the renderer and sets up various OpenGL state.

struct IRenderingEngine* CreateRenderer1();

// Interface to the OpenGL ES renderer; consumed by GLView.

struct IRenderingEngine {

virtual void Initialize(int width, int height) = 0;

virtual void Render() const = 0;

virtual void UpdateAnimation(float timeStep) = 0;

virtual void OnRotate(DeviceOrientation newOrientation) = 0;

virtual ~IRenderingEngine() {}

};

示例1.5中定义的接口,运用了一些C++中面象的方法,本书后面内容也会覆盖这些知识:

所有的接口方法都是纯虚函数。

由于接口的方法往往都是公有的,所以在这儿接口是用struct的类型定义的。(回忆一下,C++中,struct的成员访问默认是公开的,而class类型的成员访问默认是保护的。)

所有的接口都以I字母开始。

接口中只有方法,没有数据域。

接口类的创建都是通过工厂创建的设计模式创建。在这儿是通过CreateRender1创建的。

必须有一个虚析构函数以保证正确释放内存。

关于设备方向的枚举,可能你会觉得多余了,因为在iPhoneSDK的头文件(叫UIDevice.h)中存在一份类似的。其实不多余,因为我们这儿写的IRenderingEngine接口考虑了跨平台性。

因为我们的view类中会用到rendering engine接口,所以在GLView.h中得引入IRenderingEngine并声明一个该类型的指针变量,还要加入一些关于旋转与动画的变量与方法。完整的代码参看示例1.6。新增的变量与方法都用粗体显示。说明一下,有两个关于OpenGLE ES 1.1的#imports被移到RenderingEngine.hpp中去了,而EAGL相关的头文件并不是OpenGL 标准的东西,但是创建OpenGL ES上下文的时候得用到它们。

示例1.6 GLView.h

#import "IRenderingEngine.hpp"

#import

#import

@interface GLView : UIView {

@private

EAGLContext* m_context;

IRenderingEngine* m_renderingEngine;

float m_timestamp;

}

- (void) drawView: (CADisplayLink*) displayLink;

- (void) didRotate: (NSNotification*) notification;

@end

示例1.7是GLView类的实现。调用rendering engine的部份已粗休高亮了。注意GLView中现在没有任何OpenGL的方法了,我们会把所有OpenGL的调用都放在rendering engine中进行。

示例1.7 GLView.mm

#import

#import "GLView.h"

#import "mach/mach_time.h"

#import // <-- for GL_RENDERBUFFER only

@implementation GLView

+ (Class) layerClass

{

return [CAEAGLLayer class];

}

- (id) initWithFrame: (CGRect) frame

{

if (self = [super initWithFrame:frame]) {

CAEAGLLayer* eaglLayer = (CAEAGLLayer*) super.layer;

eaglLayer.opaque = YES;

m_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];

if (!m_context || ![EAGLContext setCurrentContext:m_context]) {

[self release];

return nil;

}

m_renderingEngine = CreateRenderer1();

[m_context

renderbufferStorage:GL_RENDERBUFFER

fromDrawable: eaglLayer];

m_renderingEngine->Initialize(CGRectGetWidth(frame), CGRectGetHeight(frame));

[self drawView: nil];

m_timestamp = CACurrentMediaTime();

CADisplayLink* displayLink;

displayLink = [CADisplayLink displayLinkWithTarget:self

selector:@selector(drawView:)];

[displayLink addToRunLoop:[NSRunLoop currentRunLoop]

forMode:NSDefaultRunLoopMode];

[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];

[[NSNotificationCenter defaultCenter]

addObserver:self

selector:@selector(didRotate:)

name:UIDeviceOrientationDidChangeNotification

object:nil];

}

return self;

}

- (void) didRotate: (NSNotification*) notification

{

UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];

m_renderingEngine->OnRotate((DeviceOrientation) orientation);

[self drawView: nil];

}

- (void) drawView: (CADisplayLink*) displayLink

{

if (displayLink != nil) {

float elapsedSeconds = displayLink.timestamp - m_timestamp;

m_timestamp = displayLink.timestamp;

m_renderingEngine->UpdateAnimation(elapsedSeconds);

}

m_renderingEngine->Render();

[m_context presentRenderbuffer:GL_RENDERBUFFER];

}

@end

这个工程当中Objective-C相关的部份已完成了,由于renderingengine还没写完,所以无法编译通过。在此对示例1.7中的代码进行简单的总结:

·在initWithFrame这个方法中,用工厂类的方式创建C++实现的rendering engine。接着还注册了两个事件处理,一个是”display link”,每当屏刷新的时候就会触发事件处理,还有一个是方向改变的时候触发事件处理。

·在didRotate这个事件处理方法中,将iPhone特有的设备方向枚举类型强制转化为我们的跨平台的方向枚举类型,并传递到renderingengine中进一步处理。

·在屏幕刷新回调方法drawViw中,两次调用的时间差,并传递到rendering engine的UpdateAnimation这个方法中去。这样就可以在rendering engine中控制动画或其它模拟物理特性。

·drawView这个方法中还调用了rendering engine中的Render方法,然后将renderbuffer显示到屏幕。

注意

在写本书的时候,苹果建意大家用CADisplayLink来触发OpenGL的绘制。还有一种方法就是用NSTimer触发。如果你想你的应用在iPhone OS 3.1以前的版本,那么你说去研究一下NSTimer,因为CADisplayLink是OS 3.1才加入的新支持。

实现Rendering Engine

在本小节,我们将实现一个IRenderingEngine接口的定义。选中Classes分组,鼠标右键,在弹出菜单中选择Add->Newfile, 然后在左边选中C and C++ 分类,右边选中C++File这个模板, 新建文件命名为RenderingEngine1.cpp,由于我们将在cpp文件里直接定义类,所以不需要生成相应的头文件,于是确保”Also create RenderingEngine1.h”未被选中。然后在文件中写入示例1.8的代码。

示例1.8 RenderingEngine1 Class与工厂方法

#include

#include

#include "IRenderingEngine.hpp"

class RenderingEngine1 : public IRenderingEngine {

public:

RenderingEngine1();

void Initialize(int width, int height);

void Render() const;

void UpdateAnimation(float timeStep) {}

void OnRotate(DeviceOrientation newOrientation) {}

private:

GLuint m_framebuffer;

GLuint m_renderbuffer;

};

IRenderingEngine* CreateRenderer1()

{

return new RenderingEngine1();

}

For now,UpdateAnimationandOnRotateare implemented withstubs; you'll add support for the rotation feature after we get up and running.

Example1.9shows more of thecode fromRenderingEngine1.cppwith the OpenGLinitialization code.

现在UpdateAnimation与OnRotate的实现先放一下,等程序可以运行起来以后再来实现 。

示例1.9 是RenderingEngine1中一些初始化OpenGL的代码

struct Vertex {

float Position[2];

float Color[4];

};

// Define the positions and colors of two triangles.

const Vertex Vertices[] = {

{{-0.5, -0.866}, {1, 1, 0.5f, 1}},

{{0.5, -0.866}, {1, 1, 0.5f, 1}},

{{0, 1}, {1, 1, 0.5f, 1}},

{{-0.5, -0.866}, {0.5f, 0.5f, 0.5f}},

{{0.5, -0.866}, {0.5f, 0.5f, 0.5f}},

{{0, -0.4f}, {0.5f, 0.5f, 0.5f}},

};

RenderingEngine1::RenderingEngine1()

{

glGenRenderbuffersOES(1, &m_renderbuffer);

glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_renderbuffer);

}

void RenderingEngine1::Initialize(int width, int height)

{

// Create the framebuffer object and attach the color buffer.

glGenFramebuffersOES(1, &m_framebuffer);

glBindFramebufferOES(GL_FRAMEBUFFER_OES, m_framebuffer);

glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES,

GL_COLOR_ATTACHMENT0_OES,

GL_RENDERBUFFER_OES,

m_renderbuffer);

glViewport(0, 0, width, height);

glMatrixMode(GL_PROJECTION);

// Initialize the projection matrix.

const float maxX = 2;

const float maxY = 3;

glOrthof(-maxX, +maxX, -maxY, +maxY, -1, 1);

glMatrixMode(GL_MODELVIEW);

}

示例1.9中,定义了一个POD(plain old data)类型的数据结构,用来存放构成三角形的顶点数据。在下一章中你会学到,一个顶点在OpenGL中可以有多种属性。HelloArrow只用了两种属性:一个二维坐标,一个RGBA值。

在复杂的OpenGL应用中,顶点数据往往都是从外部文件中导入的。在这儿由于图形太简单,我们就直接在代码中生成顶点数据。两个三角形,六个顶点,第一个三角形是黄色,第二个是灰色。(参看图1.4)

在类的构造函数与Initialize方法中进行了framebuffer的初始化工作。在设用者(GLView)调用构造函数与Initialize之间,必须分配renderbuffer的storage,这一分配没有在rendering engine中进行的原因是它是Objective-C的语法,而renderingengine中只支持C/C++语法。

最后但很重要的一点,在Initialize中设置了视图变换与投影矩阵。投影矩阵定义了三维空间中可见场景。这些在下一章详细介绍。

这儿有一个步骤摘要表。

1.创建renderbuffer并绑定到固定渲染通道。

2.用EAGL layer创建一个renderbuffer的storage,必须要用到Objective-C的语法。

3.创建一个framebuffer并将renderbuffer附属于它。

4.用glViewport设置视图矩阵,用glOrthof设置投影矩阵。

示例1.10是Render的实现代码

示例1.10 Render实现

void RenderingEngine1::Render() const

{

glClearColor(0.5f, 0.5f, 0.5f, 1);

glClear(GL_COLOR_BUFFER_BIT); //[1]

glEnableClientState(GL_VERTEX_ARRAY); //[2]

glEnableClientState(GL_COLOR_ARRAY);

glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &Vertices[0].Position[0]); //[3]

glColorPointer(4, GL_FLOAT, sizeof(Vertex), &Vertices[0].Color[0]);

GLsizei vertexCount = sizeof(Vertices) / sizeof(Vertex);

glDrawArrays(GL_TRIANGLES, 0, vertexCount); //[4]

glDisableClientState(GL_VERTEX_ARRAY); //[5]

glDisableClientState(GL_COLOR_ARRAY);

}

在下一章会详细说明这些代码的作用,在此只简要说明一下。

[1] 将renderbuffer设为灰色。

[2] 启用顶点属性(位置与颜色)。

[3] 向OpenGL传递顶点与颜色属性值。请看图1.8

[4] glDrawArrays这个方法的第一个参数为GL_TRIANGLES,第二个为0表示从顶点数组第一个位置开始,第三个参数vertexCount表示顶点个数。这个方法调用这前,得先调用gl*Pointer这样的方法获取顶点属性,这个方法也是向目标页绘制三角形。

[5] 禁止这两个顶点属性,只有在绘制之前才会开启这些属性。在复杂的应用当中,可以在后续绘制中会用到复多的顶点属性,所以我们在用完顶点属性后得恢复到原始状态。由于我们这个应用简单,在此如果你不恢复也没事,但是我们得养成良好的编成习惯。

图1.8InterleavedArrays

到此,先恭喜你一下,你已完成了一个OpenGL程序。最终效果如图1.9所示

图1.9 HelloArrow!

处理设备方向变化

在本章开始,我就说了箭头符号会随设备方向的变化而变化。在示例1.7中的代码已注册了事件的回调方法,那么接下来的事就是在renderingengine中实现这个回调方法。

首先在RenderingEngine1类中加入一个float型的成员变量m_currentAngle。它表示角度,并非是弧度。注意UpdateAnimation与OnRotate的变化(不再中空函数而变成了声明)。

class RenderingEngine1 : public IRenderingEngine {

public:

RenderingEngine1();

void Initialize(int width, int height);

void Render() const;

void UpdateAnimation(float timeStep);

void OnRotate(DeviceOrientation newOrientation);

private:

float m_currentAngle;

GLuint m_framebuffer;

GLuint m_renderbuffer;

};

OnRotate的实现如下:

void RenderingEngine1::OnRotate(DeviceOrientation orientation)

{

float angle = 0;

switch (orientation) {

case DeviceOrientationLandscapeLeft:

angle = 270;

break;

case DeviceOrientationPortraitUpsideDown:

angle = 180;

break;

case DeviceOrientationLandscapeRight:

angle = 90;

break;

}

m_currentAngle = angle;

}

注意在switch语句中,Unknown,Portrait, FaceUp,FaceDown没有在分支语句当中,于是在这些情况下angle的值是为0。

现在,在Render方法中可以用glRotatef来旋转图形了,如示例1.11。新加代码已粗体显示。你会发现,还新加了glPushMatrix与glPopMatrix两行代码,这是为了防止图形变化的累积。在下一章你将会明白这些方法的意义(包括glRotatef)。

示例1.11 Render最终版

void RenderingEngine1::Render() const

{

glClearColor(0.5f, 0.5f, 0.5f, 1);

glClear(GL_COLOR_BUFFER_BIT);

glPushMatrix();

glRotatef(m_currentAngle, 0, 0, 1);

glEnableClientState(GL_VERTEX_ARRAY);

glEnableClientState(GL_COLOR_ARRAY);

glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &Vertices[0].Position[0]);

glColorPointer(4, GL_FLOAT, sizeof(Vertex), &Vertices[0].Color[0]);

GLsizei vertexCount = sizeof(Vertices) / sizeof(Vertex);

glDrawArrays(GL_TRIANGLES, 0, vertexCount);

glDisableClientState(GL_VERTEX_ARRAY);

glDisableClientState(GL_COLOR_ARRAY);

glPopMatrix();

}

让旋转有动画效果

现在HelloArrow这个程序可以响应设备的方向移动,但是美中不足的是,一般的应用旋转都是很平滑的,而我们这个是突然旋转90度。

苹果在UIViewController这个类中提供了方法支持平滑的旋转,但是那种方法在OpenGL ES的应用程序中不太适合。原因如下:

  • 考虑到效率问题,苹果建意避免混用Core Animation与 OpenGL ES。
  • 绝佳的条件是renderbuffer的大小与比例应用的生命周期内不变,这有助于简单代码与执行效率。
  • 在纯图形的应用程序中,开发者对动画与渲染需要有更完美的控制。

在示例1.12中在RenderEngine1类中加入了一个浮点类型变量m_desiredAngle,实现动画效果的时候有用。这个变量表示当前动画的结束角度,因此如果没有动画的时候m_currentAngle与m_desiredAngle应是相等的。

示例1.12中还加入了一个浮点型常量RevolutionsPerSecond来表示角速度,另外还加入了RotationDirection()这个私有方法,关于它在后面介绍。

示例1.12 RenderEngine1类的定义与实现

#include

#include

#include "IRenderingEngine.hpp"

static const float RevolutionsPerSecond = 1;

class RenderingEngine1 : public IRenderingEngine {

public:

RenderingEngine1();

void Initialize(int width, int height);

void Render() const;

void UpdateAnimation(float timeStep);

void OnRotate(DeviceOrientation newOrientation);

private:

float RotationDirection() const;

float m_desiredAngle;

float m_currentAngle;

GLuint m_framebuffer;

GLuint m_renderbuffer;

};

...

void RenderingEngine1::Initialize(int width, int height)

{

// Create the framebuffer object and attach the color buffer.

glGenFramebuffersOES(1, &m_framebuffer);

glBindFramebufferOES(GL_FRAMEBUFFER_OES, m_framebuffer);

glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES,

GL_COLOR_ATTACHMENT0_OES,

GL_RENDERBUFFER_OES,

m_renderbuffer);

glViewport(0, 0, width, height);

glMatrixMode(GL_PROJECTION);

// Initialize the projection matrix.

const float maxX = 2;

const float maxY = 3;

glOrthof(-maxX, +maxX, -maxY, +maxY, -1, 1);

glMatrixMode(GL_MODELVIEW);

// Initialize the rotation animation state.

OnRotate(DeviceOrientationPortrait);

m_currentAngle = m_desiredAngle;

}

现在去修改OnRotate中的代码,将里面的当前角度量变改为目标角度变量:

void RenderingEngine1::OnRotate(DeviceOrientation orientation)

{

float angle = 0;

switch (orientation) {

...

}

m_desiredAngle = angle;

}

在实现UpdateAnimation方法之前,让我们想想应用如何确定箭头符号的旋转方向,是顺时针还是逆时针呢?方法很简单,判断目标角度是否大于当前角度即可。如果用把设备从270度朝向改为0度朝向,增加的角度和小于360度。

关于RotationDirection(),用它来判读箭头符号是顺时针还是逆时针旋转。我们要控制m_currentAngle与m_desiredAngle这两个变量的值在[0,360)之间(0包括,360不包括)。

float RenderingEngine1::RotationDirection() const

{

float delta = m_desiredAngle - m_currentAngle;

if (delta == 0)

return 0;

bool counterclockwise = ((delta > 0 && delta <= 180) || (delta < -180));

return counterclockwise ? +1 : -1;

}

下面是UpdateAnimation的实现,参数是以秒为单位的时间步进。

void RenderingEngine1::UpdateAnimation(float timeStep)

{

float direction = RotationDirection();

if (direction == 0)

return;

float degrees = timeStep * 360 * RevolutionsPerSecond;

m_currentAngle += degrees * direction;

// Ensure that the angle stays within [0, 360).

if (m_currentAngle >= 360)

m_currentAngle -= 360;

else if (m_currentAngle < 0)

m_currentAngle += 360;

// If the rotation direction changed, then we overshot the desired angle.

if (RotationDirection() != direction)

m_currentAngle = m_desiredAngle;

}

是不是相当简单呀?但最后两行有待明说一下。因为角度是浮点型的,所以很容易跳过目标值,特别是时间步进值比较在的情况下。这两行的作用是,如果捕获到角度越界,就纠正其实到正确值。在这儿,你不是实现一个摇动的罗盘,所以只需简单纠正值即可, 不过摇动的罗盘也是一个很吸引人的iPhone应用呀!

现在你已完成了HelloArrow这个应用。完整源码,你将在本书的网站上随书源码中找到它(参看引子里的关于源码)。

用Shaders实现的Hello Arrow

在本小节,我们将创建一个支持ES2.0的rendering engine。这样我们就可以看到ES1.1与ES 2.0的区大区别。本人很赞同Khronos的ES 2.0不向后兼容ES 1.0的决定,这样使得学习起来不但简单不少还更加灵活。

由于前面良好的分层架构,现在可以很轻松的在保留ES 1.1功能的情况下加入ES 2.0支持。主要修改四处:

1.加入新文件到工程,用来编写vertex shader与fragment shader。

2.增加所需framework。

3.更新GLView的一些代码,让其使用ES 2.0的环境。

4.按照RenderingEngine1修改一份为RenderingEngine2。

下面的小节将详细讲解这些修改。关于第4外的修改,如果你不想参看RenderingEngine1面自己从头实现ES2.0的支持也可以。

Shaders

ES 2.0最大的特色就是shadinglanguage。Shaders分为两类,一类是vertexshader, 另一类是fragment shaders,它们以相对较小的代码段运行在图形处理芯片上。当你调用glDrawArrays后,vertex shader就负责移动顶点,而fragment则负责逐像素计算每个三角形的颜色。由于图形处理器的高度并行化, 可以同时进行数以千计的shader实例。

Shader叫着GLSL(OpenGL Shading Language),是用类C的语言来做开发,类C并不表示是C。GLSL的程序是不能在Xcode中编译的,而是在运行时iPhone自已编译。我们的应用程序以C语言的字符串形式向OpenGL API提交Shader, 然后OpenGL把它编译成机器码。

注意

有些OpenGL ES的设备允许你离线编译shaders,这样一来你的应用程序就可以向OpenGL接交二进制形式的shader,而并非字符串的方式。到目前为止,iPhone只支持运行时编译shader,它由ARM处理器编译并将结果传送到图形处理器去运行,所以ARM功不可没。

首先得在工程中新一个分组用来存放shaders。在Groups&Files上鼠标右键,在弹出菜单中选择Add->NewGroup,命名为”Shaders”。

然后在新建的Shaders分组上鼠标右键,在弹出菜单上选择Add->New file。在othercategory中选择Empty File模版,命名为Simple.vert,在Location字段中在HelloArrow后面加上/Shader。因为这个文件不需要布署到设备上去,所以你可以取消AddTo Targets的选择框。在弹出的对话框中选择create来创建Shader目录。再用同样的方法创建一个名为Simple.frag的文件。

在说这两个文件的代码之前,我选说一个小巧门。除了用I/O操作来读到shaders外, 以用#include的方式将他们嵌入到你的C/C++代码中。在C/C++中,多行的字符串通常比较繁琐,但是在这儿有一个宏可以让事情变得简单:

#define STRINGIFY(A) #A

本节的后面会看到,我们会将这个宏放在renderingengine 源码中#include shaders的上面。然后整个shader(包括换行)就以字符串的形式引入-并不需要在第一行上加上双引号!

虽然STRINGIFY这个宏方便操作简单的shaders,但是我不建意在产品中用这个方法。第一,苹果的shader编译器对行数的报告不一定正确。同时,gcc的预处理器在你shader里字义了functions的时候,很有可能发生冲突。一个通用的办法就是将shader从文件中读取到一个字符串中。用Objective-C中封装的stringWithContentsOfFile就可以轻松办到。

示例1.13与示例1.14分别是vertex shader与fragment shader。为了简洁起见,在这儿还是引用了STRINGIFY这个宏,但是在以后的shader开发中,会去除掉的。

示例1.13 Simple.vert

const char* SimpleVertexShader = STRINGIFY(

attribute vec4 Position;

attribute vec4 SourceColor;

varying vec4 DestinationColor;

uniform mat4 Projection;

uniform mat4 Modelview;

void main(void)

{

DestinationColor = SourceColor;

gl_Position = Projection * Modelview * Position;

}

);

可以看到,shader里声明了attribute ,varying ,uniform类型的变量,你可以简单的理解为shader与人外界的连接点。vertex shader里也只简单的传递了一个颜色值,并进行了标准的变换。关于变换是下一章的内容。示例1.14中的fragment shade更是简洁。

示例1.14 Simple.frag

const char* SimpleFragmentShader = STRINGIFY(

varying lowp vec4 DestinationColor;

void main(void)

{

gl_FragColor = DestinationColor;

}

);

同样的,把varying变量想像成连接点。这个fragment shader除了把传进过来的颜色设置一下,什么也没做。

Frameworks

请确保所有的framework都是用的SDK3.1的(或更高版本)。你可以在Xcode’s Group & Files栏选中相当的framework并鼠标右键,然后点击Get info, 这样就可以看到全路径了。

注意

这儿有一种快速修改的手动方法。首先得退出Xcode,然后在Finder中找到HelloArrow.xcodeproj,鼠标右键并择Show package contents。然后你会发现一个叫project.pbxproj的文件,用TextEdit打开它,找到SDKROOT这个宏,将它修改为正确的SDK路径即可。

GLView

可能你还记得,在创建OpenGL上下文的时候,传递了一个版本常量,这儿正是需要修改的部份。在Classes分组中打开GLView.mm并将下面代码:

m_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];

if (!m_context || ![EAGLContext setCurrentContext:m_context]) {

[self release];

return nil;

}

m_renderingEngine = CreateRenderer1();

修改为:

EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;

m_context = [[EAGLContext alloc] initWithAPI:api];

if (!m_context || ForceES1) {

api = kEAGLRenderingAPIOpenGLES1;

m_context = [[EAGLContext alloc] initWithAPI:api];

}

if (!m_context || ![EAGLContext setCurrentContext:m_context]) {

[self release];

return nil;

}

if (api == kEAGLRenderingAPIOpenGLES1) {

m_renderingEngine = CreateRenderer1();

} else {

m_renderingEngine = CreateRenderer2();

}

上面的代码是在不支持ES2.0的设备上用ES 1.1,支持的则用ES 2.0。当然也可以强制用ES 1.1,只需将theForceES1设为TRUE即可。将下面一行加入GLView.mm顶端。

const bool ForceES1 = false;

对于IRenderingEngine接口,只需要在IRenderingEngine.hpp中添加CreateRenderer2这个工厂创建方法,其它的并不需要做修改。

...

// Create an instance of the renderer and set up various OpenGL state.

struct IRenderingEngine* CreateRenderer1();

struct IRenderingEngine* CreateRenderer2();

// Interface to the OpenGL ES renderer; consumed by GLView.

struct IRenderingEngine {

virtual void Initialize(int width, int height) = 0;

virtual void Render() const = 0;

virtual void UpdateAnimation(float timeStep) = 0;

virtual void OnRotate(DeviceOrientation newOrientation) = 0;

virtual ~IRenderingEngine() {}

};

RenderingEngine 实现

Objective-C相关的部份已修改完了,现在继续修改核心。用Finder创建一个RenderingEngine1.cpp的拷贝(在工程中选中RenderingEngine1.cpp并鼠标右键,选中Reveal in Finder),并命名为RenderingEngine2.cpp。并把它加入到Xcode工程。右键选中Classes分组,交选择Add->Existing Files。接着按示例1.15进行修改。新加入或修改部份用粗体显示。

示例1.15RenderingEngine2声明

#include

#include

#include

#include

#include "IRenderingEngine.hpp"

#define STRINGIFY(A) #A

#include "../Shaders/Simple.vert"

#include "../Shaders/Simple.frag"

static const float RevolutionsPerSecond = 1;

classRenderingEngine2: public IRenderingEngine {

public:

RenderingEngine2();

void Initialize(int width, int height);

void Render() const;

void UpdateAnimation(float timeStep);

void OnRotate(DeviceOrientation newOrientation);

private:

float RotationDirection() const;

GLuint BuildShader(const char* source, GLenum shaderType) const;

GLuint BuildProgram(const char* vShader, const char* fShader) const;

void ApplyOrtho(float maxX, float maxY) const;

void ApplyRotation(float degrees) const;

float m_desiredAngle;

float m_currentAngle;

GLuint m_simpleProgram;

GLuint m_framebuffer;

GLuint m_renderbuffer;

};

可能你已想到,会修改Render()这个方法的。你可以比较一下示例1.11与示例1.16。

示例1.16 OpenGL ES 2.0 的Render()

void RenderingEngine2::Render() const

{

glClearColor(0.5f, 0.5f, 0.5f, 1);

glClear(GL_COLOR_BUFFER_BIT);

ApplyRotation(m_currentAngle);

GLuint positionSlot = glGetAttribLocation(m_simpleProgram, "Position");

GLuint colorSlot = glGetAttribLocation(m_simpleProgram, "SourceColor");

glEnableVertexAttribArray(positionSlot);

glEnableVertexAttribArray(colorSlot);

GLsizei stride = sizeof(Vertex);

const GLvoid* pCoords = &Vertices[0].Position[0];

const GLvoid* pColors = &Vertices[0].Color[0];

glVertexAttribPointer(positionSlot, 2, GL_FLOAT, GL_FALSE, stride, pCoords);

glVertexAttribPointer(colorSlot, 4, GL_FLOAT, GL_FALSE, stride, pColors);

GLsizei vertexCount = sizeof(Vertices) / sizeof(Vertex);

glDrawArrays(GL_TRIANGLES, 0, vertexCount);

glDisableVertexAttribArray(positionSlot);

glDisableVertexAttribArray(colorSlot);

}

正如你所看到的,1.1与2.0版本的Render()有很大区别,但总体来说,他们的操作都差不多。

在ES 2.0中,framebuffer对象不再是扩展功能,而是core API。幸运的是,OpenGL有严格的命名规则,因此修改非常机械,只需要简单的去掉OES后缀即可。对于方法,后缀是”OES”,对于常量后缀是”_OES”,这样一来修改将非常容易:

RenderingEngine2::RenderingEngine2()

{

glGenRenderbuffers(1, &m_renderbuffer);

glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);

}

Initialize是最后一个需要修改的公有方法,见示例1.17。

示例1.17 RenderingEngine2 的Initalize

void RenderingEngine2::Initialize(int width, int height)

{

// Create the framebuffer object and attach the color buffer.

glGenFramebuffers(1, &m_framebuffer);

glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);

glFramebufferRenderbuffer(GL_FRAMEBUFFER,

GL_COLOR_ATTACHMENT0,

GL_RENDERBUFFER,

m_renderbuffer);

glViewport(0, 0, width, height);

m_simpleProgram = BuildProgram(SimpleVertexShader, SimpleFragmentShader);

glUseProgram(m_simpleProgram);

// Initialize the projection matrix.

ApplyOrtho(2, 3);

// Initialize rotation animation state.

OnRotate(DeviceOrientationPortrait);

m_currentAngle = m_desiredAngle;

}

这个方法里调用了BuildProgram这个私有方法,而BuildProgram的实现中先后调用了BuildShader这个私有方法。在OpenGL的技术中,program就是一个将多个shader连接在一起的模型。这些方法的实现见示例1.18。

示例 1.18 BuildProgram()与BuildShader()

GLuint RenderingEngine2::BuildShader(const char* source, GLenum shaderType) const

{

GLuint shaderHandle = glCreateShader(shaderType);

glShaderSource(shaderHandle, 1, &source, 0);

glCompileShader(shaderHandle);

GLint compileSuccess;

glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);

if (compileSuccess == GL_FALSE) {

GLchar messages[256];

glGetShaderInfoLog(shaderHandle, sizeof(messages), 0, &messages[0]);

std::cout << messages;

exit(1);

}

return shaderHandle;

}

GLuint RenderingEngine2::BuildProgram(const char* vertexShaderSource,

const char* fragmentShaderSource) const

{

GLuint vertexShader = BuildShader(vertexShaderSource, GL_VERTEX_SHADER);

GLuint fragmentShader = BuildShader(fragmentShaderSource, GL_FRAGMENT_SHADER);

GLuint programHandle = glCreateProgram();

glAttachShader(programHandle, vertexShader);

glAttachShader(programHandle, fragmentShader);

glLinkProgram(programHandle);

GLint linkSuccess;

glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess);

if (linkSuccess == GL_FALSE) {

GLchar messages[256];

glGetProgramInfoLog(programHandle, sizeof(messages), 0, &messages[0]);

std::cout << messages;

exit(1);

}

return programHandle;

}

在示例1.18中,用到了控制台I/O相关方法来显示shader编译时所生的错误。不管你的shader是如何简单,你最好都处理这些错误,这对你有好处,这一点你得相信。在iPhone屏幕上是不会显示这些控制台信息的,但是你可以在Xcode的GDB窗口看到,通过菜单Run->Console可以打开GDB窗口。图1.10就在控制台窗口中显示出错误信息。

图 1.10 调试控制台

图1.10中显示了当前用的OpenGLES版本,现在我们要加入这些信息, 打开GLView类,并加入如下粗体代码:

if (api == kEAGLRenderingAPIOpenGLES1) {

NSLog(@"Using OpenGL ES 1.1");

m_renderingEngine = CreateRenderer1();

} else {

NSLog(@"Using OpenGL ES 2.0");

m_renderingEngine = CreateRenderer2();

}

在Objective-C中是用NSLog来输出诊断信息的,它会自动在输出字符串关加上时间戳与自动换行。(回忆一下:Objective-C中的字符串用@这个前缀来区别C的字符串。)

再来看看RenderingEngine2.cpp的内容,还有ApplyOrthof与ApplyRotation两个方法没有实现。由于ES 2.0没有glOrthof与glRotatef这两个API,所以我们得自大实现。(在下一章,我们会建立一个简单的数学库来完成这些功能。)调用glUniformMatrix4fv就是向shader中的uniform变量传值。

示例 1.19ApplyOrtho()与ApplyRotatation()

void RenderingEngine2::ApplyOrtho(float maxX, float maxY) const

{

float a = 1.0f / maxX;

float b = 1.0f / maxY;

float ortho[16] = {

a, 0, 0, 0,

0, b, 0, 0,

0, 0, -1, 0,

0, 0, 0, 1

};

GLint projectionUniform = glGetUniformLocation(m_simpleProgram, "Projection");

glUniformMatrix4fv(projectionUniform, 1, 0, &ortho[0]);

}

void RenderingEngine2::ApplyRotation(float degrees) const

{

float radians = degrees * 3.14159f / 180.0f;

float s = std::sin(radians);

float c = std::cos(radians);

float zRotation[16] = {

c, s, 0, 0,

-s, c, 0, 0,

0, 0, 1, 0,

0, 0, 0, 1

};

GLint modelviewUniform = glGetUniformLocation(m_simpleProgram, "Modelview");

glUniformMatrix4fv(modelviewUniform, 1, 0, &zRotation[0]);

}

不要被上面代码中的矩阵吓到,我们在下一章介绍。

最后将RenderingEngine2.cpp里的所有RenderingEngine1的字符串全改为RenderingEngine2(同时把工厂创建的方法名改为CreateRenderer2)。这样就完成了所有的修改,来支持ES2.0。很明显示,ES 2.0比ES 1.0更接近底层。(译注:”closer to the metal”是ATI的第一代GPGPU技术,见http://en.wikipedia.org/wiki/Close_to_Metal)。

结束语

在本章,我们步入了iPhone OpenGL ES开发的世界,实现了一些基础框架,在本书后面章节中会继续完善,并从零开始完成了一个应用程序 — 同时支持两个版本的OpenGL ES!

在下一章,我们将学习一些图形学基础知识,并阐述Hell Arrow涉及到的一些概念。如果你对图形学已非常熟悉,那你可以跳过它。



你可能感兴趣的:(《iPhone 3D 编程》第一章:快速入门指南)