****************************************************************************
申明:本系列教程原稿来自网络,翻译目的仅供学习与参看,请匆用于商业目的,如果产生商业等纠纷均与翻译人、该译稿发表人无关。转载务必保留此申明。
内容:《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实现,正如我前面所说, 我相信它在开发当中更优雅更有效。
做个小总结,你可以从下面四种方法中选择来开发你的应用:
- 只用OpenGL ES 1.1。
- 只用OpenGL ES 2.0。
- 在运行时判断是否支持ES 2.0,如果支持则用,如果不支持就用ES 1.1。
- 为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 while RenderingEngine2 用 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 <UIKit/UIKit.h>
@interface GLView : UIView { } @end |
对于C/C++高手而言,这种语法有点感冒,等一下看到方法实现的语法后更有此感受。但是不用担心, 如果习惯了将会觉得非常轻松。
#import与#include的功能差不多,只不过它不可能产生在同一个文件包括两次头文件的错误,与 C/C++中加入了#pragmaonce的功能类似。
Objective-C的关键字都是以”@”为前缀的。@interface表示类的声明开始,@end表示类的声明结束。一个文件里可以包括多个类的声明,于是可以出现多个@interface程序块。
现在你可能都已猜到,上面的代码片段其实就是定义了一个类,类名是GLView,继承自UIView。有一点需要明确的是,数据的声明应放在大括号内, 而方法的声明则应放在结束在括号与@end之间,如下代码:
#import <UIKit/UIKit.h>
@interface GLView : UIView { // Protected fields go here... } // Public methods go here.. @end |
在数据区声明的数据,默认是保护型的,当然你可以用关键字@provate改为private型。继续上面的代码,我们来完善它,如示例1.1所示, 我们需要引入几个与OpenGL相关的头文件。
示例1.1 GLView类的定义
#import <UIKit/UIKit.h> #import <OpenGLES/EAGL.h> #import <QuartzCore/QuartzCore.h> #import <OpenGLES/ES1/gl.h> #import <OpenGLES/ES1/glext.h> @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 of UIWindow. Let's add a pointer to an instance ofthe GLView class along with a couple methoddeclarations (new/changed lines are shown in bold):
由Xcode模版生成的的应用代理类(HelloArrowAppDelegate.h)里只有一个UIWindow变量。现在我们加入GLView类的一个变量(新加入/修改了的代码以粗体显示):
#import <UIKit/UIKit.h> #import "GLView.h"
@interface HelloArrowAppDelegate : NSObject <UIApplicationDelegate> { 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 <UIKit/UIKit.h> #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 <OpenGLES/EAGL.h> #import <QuartzCore/QuartzCore.h>
@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 <OpenGLES/EAGLDrawable.h> #import "GLView.h" #import "mach/mach_time.h" #import <OpenGLES/ES2/gl.h> // <-- 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 <OpenGLES/ES1/gl.h> #include <OpenGLES/ES1/glext.h> #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, UpdateAnimation
and OnRotate
are implemented withstubs; you'll add support for the rotation feature after we get up and running.
Example 1.9 shows more of thecode from RenderingEngine1.cpp
with 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.8 InterleavedArrays
到此,先恭喜你一下,你已完成了一个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 <OpenGLES/ES1/gl.h> #include <OpenGLES/ES1/glext.h> #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.15 RenderingEngine2声明
#include <OpenGLES/ES2/gl.h> #include <OpenGLES/ES2/glext.h> #include <cmath> #include <iostream> #include "IRenderingEngine.hpp"
#define STRINGIFY(A) #A #include "../Shaders/Simple.vert" #include "../Shaders/Simple.frag"
static const float RevolutionsPerSecond = 1;
class RenderingEngine2 : 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.19 ApplyOrtho()与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涉及到的一些概念。如果你对图形学已非常熟悉,那你可以跳过它。