《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 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涉及到的一些概念。如果你对图形学已非常熟悉,那你可以跳过它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值