当今启幕大家新的iOS OpenGL ES连串教程,在此篇作品里

最近有好几个使用Qt的朋友问起 Qt for iOS 的事情,因为我在这方面的经验特别少,写不出系统的文章来,非常抱歉,不能给出令人满意的答复,推荐大家去看 Jason’s Home ,在我博客左侧边栏的友情链接里也有,他提供了 Qt for iOS 的一些非常有意义的文章,而且是基于实践的,他的 App 已经在 App Store 中上线。

         [ jimmyzhouj 翻译]  Nehe iOS OpenGL ES 2.0教程

至于我呢,在这篇文章里,简单介绍一些如何混合 Qt 与 OC 编程。

  引子:          最近要学习iOS 上的OpenGL ES的内容,在互联网上找了一些教程来看。发现关于OpenGL ES2.0的教程不多。想起了知名的Nehe OpenGL 教程,就上nehe.gamedev.net上找了找,发现他们有一个用于移动设备的OpenGL ES教程,并且是基于OpenGL ES 2.0的。现在还只有两篇教程,我先把这两篇翻译出来,以供大家学习。有什么问题需要交流的,可以联系我: [email protected]。   免责声明:本教程翻译原稿均来自互联网,仅供学习交流之用,请勿进行商业传播。同时,转载时不要移除本声明。如产生任何纠纷,均与本博客所有人和发表该翻译稿之人无任何关系。谢谢合作!     IOS Lesson 01 – 设置GL ES 原文链接地址:   大家好,现在开始我们新的iOS OpenGL ES系列教程!   前言        当我们开始这个教程的时候,你该知道,我对Objective-C和iOS是一个新手,所以如果有什么地方我弄错了,或者什么地方可以用更好的方法来实现,请告诉我。我只是在与OS交互的时候才使用Objective-C,所有和OpenGL相关的代码都是用C++实现的。我会尽量解释我们看到的所有地方,但我也假设你已经有基本的编程知识,并且懂得面向对象编程的基本概念。 教程附带的代码可以在iOS4,iOS5上工作,所以就算是在iPhone 4S上也能正常运行。 我们准备使用XCode4,如果你还没有安装的话,请到MAC APPStore上下载并安装它。如果有人希望能够在Windows或者Linux机器上开发iPhone App,我只能遗憾的告诉你不能这么做。 我们知道,第一课看起来很恐怖,它可能是这一系列教程中最无聊的一课,到本节课结束也看不到什么很酷的东西… :(  不过,这节课包含了很多有价值的内容,并且理解程序框架的不同部分之间的交互是非常重要的。所以你需要把它通读一遍,你不需要理解所有的内容,当你之后想要知道细节的话,可以返回来再仔细看。按照 NeHe的的好习惯,我会尽量详细解释每一行代码!       概要   让我们先看看需要使用到的类。就像我之前说的,我们需要一些Objective-C的类紫色框)和一些C++的类青色框)。  

我要说的内容呢,大部分在 Qt 帮助里都有,请大家到索引模式下,键入”Qt for iOS”,找到 Qt for iOS 这篇文章来看。它介绍了搭建开发环境编译应用混合OC编程这三个方面,已经非常详细了。

 

如果你不想啃英文,那可以接着我的文章往下看。

图片 1

项目设置

既然要聊 Qt 混合 OC 编程,首先要简单介绍一下 Objective-C 。我只有一句话:Go,问搜索引擎去。因为我所知实在有限,怕误导了您。当然如果您不怕,往下看吧。

和所有的C/C++程序一样,应用程序的起点是main方法。我们执行UIApplicationMain,用InterfaceBuilder配置一个包含了EAGLView的UIWindow,并且用Lesson01AppDelegate去处理所有的事件。Window是UIApplicationMain自动创建的,可以显示我们的view。View包含了我们的OpenGL conext,可以通过这个context访问到用OpenGL ES来绘画的canvas。

Objective-C源文件介绍

首先我要说一下 Objective-C 的源文件,后缀是.m 或 .mm ,在 .mm 文件里,可以直接使用 C++ 代码。所以,我们要混合 Qt 代码与 Objective-C 代码,就需要在 Qt 项目里加入 mm 文件。

我们要钩到操作系统的run loop中去,尽可能频繁的重绘frame。在之后关于动画的课程中这是必须的。绘画动作是在Lesson对象中的draw方法中实现的,或者更精确的说,是在Lesson01对象中实现的,因为在类Lesson中,init)和draw)都是虚函数。   简单浏览   我们开始一步一步实现。你可以从这里( 获取代码,然后打开项目文件Lesson01.xcodeproj。

pro 文件配置

Qt SDK for Mac ,安装之后, Qt Creator 会使用 XCode 提供的编译工具链来编译代码,能够正确编译 mm 文件,也可以链接 iOS 的库文件。

而要混合 Objective-C 代码,需要更改一下 pro 文件。一个是添加 mm 文件,一个是连接针对 iOS 的库文件。

添加源文件,使用 OBJECTIVE_SOURCES 这个变量,比如这样子:

OBJECTIVE_SOURCES += ocview.mm

链接库 XCode 提供的库,则需要使用 QMAKE_LFLAGS ,类似这样子:

ios {
    QMAKE_LFLAGS    += -framework OpenGLES
    QMAKE_LFLAGS    += -framework GLKit
    QMAKE_LFLAGS    += -framework QuartzCore
    QMAKE_LFLAGS    += -framework CoreVideo
    QMAKE_LFLAGS    += -framework CoreAudio
    QMAKE_LFLAGS    += -framework CoreImage
    QMAKE_LFLAGS    += -framework CoreMedia
    QMAKE_LFLAGS    += -framework AVFoundation
    QMAKE_LFLAGS    += -framework AudioToolbox
    QMAKE_LFLAGS    += -framework CoreGraphics
    QMAKE_LFLAGS    += -framework UIKit
}

上面是我使用 Qt 针对 iOS 编程的配置。我使用了很多针对 iOS 的库,所以添加了很多 framework 。

“ -framework UIKit ”这种参数,是经由 Makefile 传递给 Clang 的参数,-framework 是用来指示要链接某个框架(或者说库)的关键字,它后面跟的是框架(库)名。

需要注意的是,我们使用针对 iOS 的库,不是通过“ LIBS += ”这种方式来引入哦。当然,你自己通过 Qt 实现的 .a 库,依然需要使用“ LIBS += ”这种方式。

         注意:你之后创建你自己的OpenGL ES 项目的时候,你可以使用project wizard。生成的项目的结构有一点复杂,不过总体构成是一样的。Draw方法会在yourProjectNameViewController:drawFrame()里。我们的代码只是简单的将他们都清除了,并且将我们的OpenGL代码和window隔离开了而已。

指定plist文件

有时你需要为你的项目指定 plist 文件, plist 文件全名是 Property List ,后缀是 .plist 。它用来定义 iOS 应用的属性,比如 Bundle(iOS上的一个应用被称为一个 Bundle ) 的显示名字、可执行文件名字、签名、证书等等,当然也可以保存一些配置数据。具体的介绍参考 iOS 开发的文档吧。

要在 pro 文件里添加 plist ,要使用 QMAKE_INFO_PLIST 关键字。如下面这样子:

QMAKE_INFO_PLIST += MultiWindow.plist

好啦,关于 pro 文件中与混合使用Objective-C 相关的配置项,大体就这些了。接下来我们看如何写 Objective C 代码啦。

  你会在项目左侧看到3个文件夹:Lesson01,Frameworks和Products。Lesson01还包含了2个子目录Supporting Files和Basecode,Lesson01包括了我们所有的代码。Frameworks包含了所有我们要用的或者项目需要使用的frameworks。从其他语言或者操作系统转过来的开发者可以叫它们是libraries。Products列出类所有要生成的applications,现在就只有一个Lesson01.app。 在我们之后的课程的代码中,我们主要和LessonXX 类打交道,每次AppDelegate都会生成一个当前课程的实例。在第一课,我们先详细看一看Basecode和Supporting Files。   让我们按照代码执行时的访问顺序来看看这些文件。就像我之前指出的一样,所有程序都是从main方法开始执行的。它在Lesson01/Supporting Files/main.m中。  

混合使用Objective C 代码

乖乖,很惶恐啊,这是我的弱项,没写过多少 Objective-C 代码。所以,请不要问我 OC 有关的问题,我真不知道……

#import <UIKit/UIKit.h>   //standard main method, looks like that for every iOS app.. int main(int argc, char *argv[]) {     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];     NSLog(@"Running app");     int retVal = UIApplicationMain(argc, argv, nil, nil);     [pool release];     return retVal; } 

背景

我的示例,是在 QML 的界面上叠加iOS原生的界面,即 UIView、UIWindow之类的。因为 OC 是 C 的近亲,和 C++ 有着天然的血缘,混合起来特别方便哈,比 Android 上使用 JNI 编程好用多了。

不过有一点, OC 都适用 [] 这种语法来调用函数,使用 XCode 的话,语法提示和自动完成功能非常强大,基本不用思考的就能找到你要用的函数。而 Qt Creator 么,嘿嘿,就没这么好相与了,纯粹要手写哦。我当时都是开着 XCode 看 API 文档找的,比较痛苦。

这个方法在每个iPhone app里都差不多是一样的。NSAutoreleasePool是Objective-C的垃圾收集系统所需要的,我们打印一条log,然后是重要的部分:将应用程序的参数传给UIApplicationMain方法,然后将控制权也转给它。当最后程序的控制权转移回来的时候,我们释放garbage collection utility,用UIApplicationMain给出的返回值来结束程序。 就像它的名字一样,UIApplicationMain方法运行一个用户界面。在我们项目的设定中,选中Targets->Lesson01, 选Summary选项卡,MainInterface设置为MainWindow。这告诉App,程序启动的时候,我们要显示MainWindow.xib。这个文件也在Supporting Files目录下,可以在InterfaceBuilder中打开。 打开MainWindow.xib,在编辑器的左边栏,你可以看到Placeholder和Objects。在Objects下,有Lesson01AppDelegate和一个内嵌了View的Window。如果你没有做过任何GUI编程,你可以认为这个就像你最喜欢的Office软件(=window),打开了一个文档(=view)。现在你可以在这些文档(=views,包含了UI元素比如buttons,text fields或者OpenGL ES canvas)中切换,而不需要关闭整个程序。不过你需要有一个打开的文档(view)才能看到其中的内容。

QQuickView 是什么

我测试时的示例,用的是 Qt Quick App 项目模板,使用 QQuickView 来加载 QML 文档。这里也以此为例来说明。

首先要说 QQuickView 到底是什么。

QQuickView 呢,其实是一个 UIView 。UIView 则是 iOS 开发框架里很多界面元素的根儿,比如 UIWindow 就是 UIView 的子类。

Qt 的 QQuickView 是一个 UIView ,创建了 QQuickView 实例后,就有了一个 UIView ,然后 Qt 玩了一些魔法,拿到了 UIView 的 OpenGL Context ,跑起了 Qt 的事件循环,在这个 OpenGL Context 上从零开始绘制了自己的场景和 UI 系统。

就这么简单,你可以查阅 Qt 源码来进一步了解。

需要注意的是, QML 界面元素的渲染,与 UIView 这种原生界面的渲染,不在一个线程中。而且 iOS 对 OpenGL ES 的支持很好,你可以同时使用多个 OpenGL Context 。更好的是,你可以窗口模式来创建一个 OpenGL Context 。不像 Android 版本哦, Qt 使用 OpenGL 的时候都是全屏模式,局部更新不支持,所以我们在 Android 上使用 QML 里的 Camera 和 VideoOutput 来开发拍照应用时, VideoOutput 必须是全屏模式(必须fill_parent)。而在 iOS 上,则没有这个限制了。看来 iOS 还是很美好的啦。

我靠,扯得有点儿远,最近写技术文章少了,越来越罗嗦了。言归正传吧。

因为 QQuickView 实际上就是一个 UIView ,所以可以强制转换为 UIView ,然后使用 OC 的方法来创建新的 UIView 或者 UIWindow ,这样就有了原生的 UI 组件了,你可以在这个原生的 UI 组件上使用 OpenGL 绘制自己的东西或者添加其它原生的控件,非常美好。

不过要说明的是,通过这种方法创建出来的 iOS 原生界面元素,会始终在 QML 界面之上,把 QML 场景里的界面元素给盖住。

当你control-click(或者右键)Lesson01AppDelegate的时候,你会看见2个定义好的outlets:glView和window,它们分别关联到InterfaceBuilder中的View和Window。一个Outlet可以让app delegate代码中的一个变量包含了它所关联的InterfaceBuilder中元素的引用。

混合代码

要使用 OC 的类库,需要在 mm 文件内包含相关的头文件,又有几部分工作要做。一个是在 pro 文件里加入 SDK 路径,使用 INCLUDEPATH 变量即可,不多说了。另外一点是在 mm 文件内包含 Objective-C 的头文件,与 C++ 头文件一个理儿,不过要使用 #import 哦。类似酱紫:

#import <UIKit/UIKit.h>
#import <GLKit/GLKit.h>

包含了头文件,就可以使用 Objective-C 类库了。比如我要在 QQuickView 上面创建一个新的 iOS 原生的 UIView ,.mm 文件里可以这样:

void addOCView(QQuickWindow *w)
{
    UIView *view = reinterpret_cast<UIView *>(w->winId());

    CGRect  viewRect = CGRectMake(10, 10, 100, 100);
    UIView* myView = [[UIView alloc] initWithFrame:viewRect];
    [myView setBackgroundColor:[UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0]];
    [view addSubview: myView];
}

如你所见,我写了一个 addOCView 方法,它的参数是 QQuickView 。在 addOCView 方法里,我把 QQuickView 强制转换为 UIView 来使用。

我创建了一个新的 UIView ,设置了它的背景颜色,然后把它添加为 QQuickView 的子窗口。诺,就这么简单了。

说下 main.cpp ,看它如何使用 addOCView() 方法。代码如下:

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQuickView viewer;
    viewer.setResizeMode(QQuickView::SizeRootObjectToView);
    viewer.setSource(QUrl("qrc:/main.qml"));
    viewer.show();

    addOCView(&viewer);

    return app.exec();
}

一点儿都不惊喜是吧,就是直接调用了 addOCView 哦。哈哈,确实如此了。

  Lesson01AppDelegate   我们来看第一个重要的代码文件:Lesson01AppDelegate.h

iOS 原生界面与 QML 元素的位置映射

混合使用 iOS 原生界面时,也可以达到原生界面与 QML 界面的无缝集成。关键就在于计算 QML 界面元素的位置,然后根据 QML 界面元素的位置来设置原生界面的位置。

#import <UIKit/UIKit.h>   //we want to create variables of these classes, but don't need their implementation yet, //so we just tell the compiler that they exist - called forward declaration @class EAGLView; class Lesson;   //This is our delegate class. It handles all messages from the device's operating system @interface Lesson01AppDelegate : NSObject <UIApplicationDelegate> { @private     //we store a pointer to our lesson so we can delete it at program shutdown     Lesson *lesson; }   //we configure these variables in the interface builder (IB), thus they have to be declared as IBOutlet //properties get accessor methods by synthesizing them in the source file (.mm)   //in this window we will embed a view which acts as OpenGL context @property (nonatomic, retain) IBOutlet UIWindow *window;   //our main window, covering the whole screen @property (nonatomic, retain) IBOutlet EAGLView *glView;   @end 

QML元素位置换算

QML 提供了换算元素位置的方法,Item 有个方法,叫作 mapToItem() ,它可以把一个相对于 Qt Quick Item 的区域映射到另一个 Item 上,得到坐标了。比如你的 qml 文件是下面的样子:

Rectangle {

    ...

    Rectangle {
        id: videoLayer;
        anchors.margins: 8;
        anchors.left: parent.left;
        anchors.right: parent.right;
        anchors.top: parent.top;
        anchors.bottom: actionBar.top;
        color: "green";
    }

    ...

}

你想把原生的 UIView 定位到 id 为 videoLayer 的元素内部,可以类似下面换算一个坐标并使用它:

var coordinate = videoLayer.mapToItem(null, 8, 8, videoLayer.width - 16, videoLayer.height - 16);
winUtil.addUIView(coordinate.x, coordinate.y, coordinate.width, coordinate.height);

换算出的coordinate有 x 、 y 、 width 、 height 属性。当 mapToItem 的第一个参数为 null 时,换算的结果就是相对于 QQuickView 的。那我们在添加 UIView 时,就可以用这个换算结果来构造一个 CGRect 对象,用这个 CGRect 来初始化 UIView 的位置。

在Objective-C风格的类定义中,我们首先在头文件声明了interface,之后在相应的源文件(.mm)中使用implementation实现方法。从上面的代码中还可以看出,有一些成员变量,window, glView, lesson,这些变量会在AppDelegate初始化的时候被创建。要注意的是,properties定义为IBOutlet后,才能够被InterfaceBuilder使用。由于要处理窗口的事件,当程序开始运行的时候,会自动创建一个AppDelegate的对象实例。为了能处理窗口事件,AppDelegate实现了接口(或者叫protocol) UIApplicationDelegate。Protocol添加在类名的定义后,用<>符号包起来。   窗口创建后,触发的第一个事件是didFinishLauchingWithOptions。代码如下,在Lesson01AppDelegate.mm中。  

设置 UIView 的位置

前面示例代码中的 winUtil 是我在 C++ 内实现的一个辅助类,导出到了 QML 环境中。它的 addUIView 方法根据传入的坐标来设置原生 UIView 的位置。参考代码如下:

UIView *v = reinterpret_cast<UIView*>(view->winId());
    uiw = [[UIWindow alloc] initWithFrame:CGRectMake(x, y, width, height)];
    [v addSubview: uiw];

上面代码中,view 是 QQuickView 。这次我们创建 UIView 时使用了传入的 x 、 y 、 width 、 height,这样新建的 UIView 就和 QML 元素整合在一起了,看起来好像是一体的。

OK,这就是全部了。

#import "Lesson01AppDelegate.h"   #import "EAGLView.h" #include "Lesson01.h"   //now we implement all methods needed by our delegate @implementation Lesson01AppDelegate   // @synthesize window; @synthesize glView;   //this method tells us, that our application has started and we can set up our OpenGL things, //as the window is set up, and thus our glView is going to be displayed soon - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {     //for the first lesson we don't need a depth buffer as we're not drawing any geometry yet     [glView setDepthBufferNeeded:FALSE];           //we create our lesson which contains the OpenGL code     //(allocated with new -> has to be cleaned up with delete!)     lesson = new Lesson01();           //we tell our OpenGL view which lesson we want to use for rendering.     [glView setLesson:lesson];           return YES; } 

首先,我们配置了glView,然后创建了一个lesson对象的实例。要注意的是,我们定义的成员变量lesson是指向Lesson对象的指针,但是由于Lesson01是Lesson的子类,所以它们提供了相同的接口,可以这么使用。 还要注意,代码中synthesize了头文件定义的两个property,这会自动生产getter和setter方法。     Lesson01AppDelegate.mm中的其余代码部分处理了窗口触发的其他的事件,这些事件都和应用变为可见,或者成为后台程序有关。当变为可见时,我们让view周期性的刷新。当应用被移到后台或者关闭后,停止刷新。最后,当AppDelegate被释放时,它的dealloc方法被调用,我们在这个方法里释放之前分配的内存。在Objective-C里用release释放,在 C++里,用new来创建,用delete来删除。

- (void)applicationWillResignActive:(UIApplication *)application {     //the app is going to be suspended to the background,     //so we should stop our render loop     [glView stopRenderLoop]; }   - (void)applicationDidEnterBackground:(UIApplication *)application {     //we could do something here when the application entered the background }   - (void)applicationWillEnterForeground:(UIApplication *)application {     //we could start preparing stuff for becoming active again }   - (void)applicationDidBecomeActive:(UIApplication *)application {     //we're on stage! so let's draw some nice stuff     [glView startRenderLoop]; }   - (void)applicationWillTerminate:(UIApplication *)application {     //before shutdown we stop the render loop     [glView stopRenderLoop]; }   //dealloc is the destructor in Objective-C, so clean up all allocated things - (void)dealloc {     [window release];     [glView release];     delete lesson;     [super dealloc]; }   @end 

  现在让我们来看包含OpenGL context 的创建和负责绘画的类。   EAGLView   正如我们所知,当运行UIApplicationMain时,会自动创建window和delegate。因为我们在delegate中定义了对应的outlet,当Window被创建时,它知道需要一个EAGLView作为Window所包含的View。所以Window会自动创建一个view来显示。我们先看EAGLView.h

//forward declarations again @class EAGLContext; class Lesson;   // This class combines our OpenGL context (which is our access to all drawing functionality) // with a UIView that can be displayed on the iOS device. It handles the creation and presentation // of our drawing surface, as well as handling the render loop which allows for seamless animations. @interface EAGLView : UIView { @private     // The pixel dimensions of the CAEAGLLayer.     GLint framebufferWidth;     GLint framebufferHeight;           // These are the buffers we render to: the colorRenderbuffer will contain the color that we will     // finaly see on the screen, the depth renderbuffer has to be used if we want to make sure, that     // we always see only the closest object and not just the one that has been drawn most recently.     // The framebuffer is a collection of buffers to use together while rendering, here it is either     // just the color buffer, or color and depth renderbuffer.     GLuint defaultFramebuffer, colorRenderbuffer, depthRenderbuffer;           // The display link is used to create a render loop     CADisplayLink* displayLink;           // Do we need a depth buffer     BOOL useDepthBuffer;           // The pointer to the lesson which we're rendering     Lesson* lesson;           // Did we already initialize our lesson?     BOOL lessonIsInitialized; }   // The OpenGL context as a property (has autogenerated getter and setter) @property (nonatomic, retain) EAGLContext *context;   // Configuration setters - (void) setDepthBufferNeeded:(BOOL)needed; - (void) setLesson:(Lesson*)newLesson;   //if we want OpenGL to repaint with the screens refresh rate, we use this render loop - (void) startRenderLoop; - (void) stopRenderLoop; @end 

  类EAGLView来源于UIView。作为UIView的子类,我们可以重写(overwrite)一些方法(在Objective-C中叫selector),下面我们会在源文件中看见。

     注解:并不是强制必须用EAGLView这个名字,但在iOS GL ES程序中通常都用它,因为context被称为EAGLContext。EAGL可能代表”Embedded AGL”,AGL是APPLE的OPENGL扩展。

正如我们所见,EAGLView封装了OpenGL ES context。OpenGL context被认为是允许使用OpenGL调用来绘画的许可。它跟踪记录了我们设定的所有状态,比如说当前的颜色,当前哪一幅图片被用来做纹理。Context要和canvas我们可以绘画的地方)配合起来使用。Canvas通过一种叫framebuffer的构造来实现。framebuffer由存储不同信息的多层buffer组成。我们常用到的两个层是color renderbuffer和depth renderbuffer。Color renderbuffer里面存储了每个像素点的每个color channel,就像JPEG图像一样。这是最终显示在屏幕上的内容。The depth renderbuffer记录了color buffer中的每个像素点离屏幕的距离。如果我们画了一座距离屏幕10单位远的房子和一个距离屏幕5单位远的人,则不管先画的是房子还是人,人总是显示在房子的前面。这被称为是深度测试。Depth buffer内容不会被显示。

EAGLView类的大部分成员变量如下:我们存储了framebuffer的宽度和高度(颜色缓存和深度缓存尺寸也和这一样),我们还保存的各个缓存的ID,用来作为在OpenGl中使用时的名字。   下面是CADisplayLink成员,它允许我们进入系统的主循环,并请求按一秒约60次的频率重绘。我们还有一个开关用于启用/禁用深度缓存。因为缓存很消耗显示芯片的宝贵内存,如果不需要深度的话,我们应该禁用深度缓存。 我们还需要一个指针指向我们的lesson对象,这样就可以调用draw()方法了。还需要一个标志来表示是否已经初始化了这个lesson对象。 之前一直在说的context被保存为EAGLContext  property. 还有4个方法是AppDelegate要使用的,这4个方法的名字已经很好的解释了它们的功能。      下面我们先看看EAGLView.mm中初始化的部分。

#import <QuartzCore/QuartzCore.h>   #import "EAGLView.h" #include "Lesson.h"   //declare private methods, so they can be used everywhere in this file @interface EAGLView (PrivateMethods) - (void)createFramebuffer; - (void)deleteFramebuffer; @end     //start the actual implementation of our view here @implementation EAGLView   //generate getter and setter for the context @synthesize context;   // We have to implement this method + (Class)layerClass {     return [CAEAGLLayer class]; } 

首先我们给类增加了几个私有方法的声明。如果不这么做的话,我们就必须把这几个方法的实现代码放在调用它们的代码的前面。为了保持头文件的简洁,我们不把声明放到头文件中,不过我们现在这么做也没有真正增加源码的可读性。

接下来我们开始实现EAGLView,合成(synthesize) context以自动生成getter和setter方法。然后需要重写UIView的layerClass方法,因为现在我们用的view不是标准的UI元素,而是要画到一个CAEAGL层上(CA指 CoreAnimation)。

//our EAGLView is the view in our MainWindow which will be automatically loaded to be displayed. //when the EAGLView gets loaded, it will be initialized by calling this method. - (id)initWithCoder:(NSCoder*)coder {     //call the init method of our parent view     self = [super initWithCoder:coder];           //now we create the core animation EAGL layer     if (!self) {         return nil;     }       CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;           //we don't want a transparent surface     eaglLayer.opaque = TRUE;           //here we configure the properties of our canvas, most important is the color depth RGBA8 !     eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:                                     [NSNumber numberWithBool:FALSE], kEAGLDrawablePropertyRetainedBacking,                                     kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,                                     nil];           //create an OpenGL ES 2 context     context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];           //if this failed or we cannot set the context for some reason, quit     if (!context || ![EAGLContext setCurrentContext:context]) {         NSLog(@"Could not create context!");         [self release];         return nil;     }       //do we want to use a depth buffer?     //for 3D applications we usually do, so we'll set it to true by default     useDepthBuffer = FALSE;           //we did not initialize our lesson yet:     lessonIsInitialized = FALSE;           //default values for our OpenGL buffers     defaultFramebuffer = 0;     colorRenderbuffer = 0;     depthRenderbuffer = 0;                 return self; } 

当view初始化的时候,会执行initWithCoder方法。这时候我们就开始设置context了。 首先我们初始化父类UIView,检查是否一切正常。然后我们从View的层中创建CAEAGLLayer并且将它设为可绘画的(不要问我这部分的细节:) 下一步是创建我们的OpenGL context,我们需要它是版本2的从iPhone 3S和iPod Touch3起支持),通过调用initWithAPI:kEAGLRenderingAPIOpenGLES2来实现。如果创建context成功,并且也可以将context设为当前context,我们接下来将成员变量设为缺省值。 还记得前面关于framebuffer的段落么?我们来创建canvas作为下面所有课程进行绘画工作的基础。

//on iOS, all rendering goes into a renderbuffer, //which is then copied to the window by "presenting" it. //here we create it! - (void)createFramebuffer {     //this method assumes, that the context is valid and current, and that the default framebuffer has not been created yet!     //this works, because as soon as we call glGenFramebuffers the value will be > 0     assert(defaultFramebuffer == 0);           NSLog(@"EAGLView: creating Framebuffer");           // Create default framebuffer object and bind it     glGenFramebuffers(1, &defaultFramebuffer);     glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebuffer);           // Create color render buffer     glGenRenderbuffers(1, &colorRenderbuffer);     glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer); 

首先检查是否还没有framebuffer。如果还没有,调用OpenGL的方法来产生一个framebuffer对应的ID,这样产生的ID可以确保是唯一的,并且ID总是大于0。得到ID后,我们将framebuffer和ID绑定。OpenGL会跟踪活动对象的大部分事情,比如活动的framebuffer,最后设定的颜色,活动的纹理或者活动着色器程序(shader program)等等。这导致下面所有关于framebuffer的API都影响当前绑定的framebuffer。 下面对color renderbuffer做同样的工作,产生一个ID并且绑定。  

//get the storage from iOS so it can be displayed in the view     [context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];     //get the frame's width and height     glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &framebufferWidth);     glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &framebufferHeight);           //attach this color buffer to our framebuffer     glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRenderbuffer); 

这个color renderbuffer是特殊的。我们希望我们绘画的颜色可以被用于UIView的颜色,这就是为什么之前我们要创建 CAEAGLLayer。现在我们需要得到layer,将它用于renderbuffer存储。这样做,我们不需要再一次拷贝缓存中的内容就可以将设置的颜色显示在UIView上。 这要通过调用context的 renderbufferStorage方法来实现。函数实现的很精巧,可以自动得到一个和view的尺寸相适应的buffer。再下面两行用来查询framebuffer的宽度和高度。

glFramebufferRenderbuffer是非常重要的。一个framebuffer由多个layer组成,这些layer被称为attachments。这里我们告诉OpenGL,当前绑定的framebuffer要附加一个名字为colorrenderbuffer的color buffer。GL_COLOR_ATTACHMENT_0表示一个framebuffer可以有几个color attachments,不过这个内容超过了本节课程的范围了。

//our lesson needs to know the size of the renderbuffer so it can work with the right aspect ratio if(lesson != NULL) {     lesson->setRenderbufferSize(framebufferWidth, framebufferHeight); } 

我们刚刚知道了实际的渲染窗口的大小,需要将这个参数传递给lesson对象,这样它就可以渲染全屏的尺寸了。  

if(useDepthBuffer)     {         //create a depth renderbuffer         glGenRenderbuffers(1, &depthRenderbuffer);         glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer);         //create the storage for the buffer, optimized for depth values, same size as the colorRenderbuffer         glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, framebufferWidth, framebufferHeight);         //attach the depth buffer to our framebuffer         glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer);     } 

当需要深度缓存的时候,我们可以像之前颜色缓存那样实现。这一次需要通过调用glRenderbufferStorage方法自己创建存储,需要提供一些参数,如存储何种类型的数据(DEPTH_COMPOMENT16是每个像素16位)和buffer需要多大。  

//check that our configuration of the framebuffer is valid     if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)         NSLog(@"Failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER)); } 

最后我们需要确认framebuffer已经准备好了。因为有很多种可能会出错的情况,所以最好对每一个framebuffer对象(短FBO)都执行检查。    

//deleting the framebuffer and all the buffers it contains - (void)deleteFramebuffer {     //we need a valid and current context to access any OpenGL methods     if (context) {         [EAGLContext setCurrentContext:context];                   //if the default framebuffer has been set, delete it.         if (defaultFramebuffer) {             glDeleteFramebuffers(1, &defaultFramebuffer);             defaultFramebuffer = 0;         }                   //same for the renderbuffers, if they are set, delete them         if (colorRenderbuffer) {             glDeleteRenderbuffers(1, &colorRenderbuffer);             colorRenderbuffer = 0;         }                   if (depthRenderbuffer) {             glDeleteRenderbuffers(1, &depthRenderbuffer);             depthRenderbuffer = 0;         }     } } 

在某个时间点,我们创建的所有东西都需要被删除掉,FBOs和renderbuffers也不例外。我们有一个有效的且是当前的context的话,就可以使用OpenGL函数了。通过调用glDeleteFramebuffers和glDeleteRenderbuffers来删除。        

//this is where all the magic happens! - (void)drawFrame {     //we need a context for rendering     if (context != nil)     {         //make it the current context for rendering         [EAGLContext setCurrentContext:context];                   //if our framebuffers have not been created yet, do that now!         if (!defaultFramebuffer)             [self createFramebuffer];           glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebuffer);                   //we need a lesson to be able to render something         if(lesson != nil)         {             //check whether we have to initialize the lesson             if(lessonIsInitialized == FALSE)             {                 lesson->init();                 lessonIsInitialized = TRUE;             }                           //perform the actual drawing!             lesson->draw();         }                   //finally, get the color buffer we rendered to, and pass it to iOS         //so it can display our awesome results!         glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);         [context presentRenderbuffer:GL_RENDERBUFFER];     }     else         NSLog(@"Context not set!"); } 

现在是整个app中最关键的部分,drawFrame方法。当我们的display link触发,每次frame被渲染的时候,都会调用这个方法。首先我们要确保有一个context,framebuffer已经创建并且绑定了,lesson对象已经创建并且初始化了。如果前面几条都实现了,我们调用lesson->draw()方法,这个方法是后面的课程着力聚焦的地方。最后几行很有趣。调用完lesson->draw()后,renderbuffers已经有需要渲染的内容了。为了告诉系统有新内容要显示,需要绑定color buffer并且要求context将它呈现出来,调用的方法是[context presentRenderbuffer:GL_RENDERBUFFER。 我们刚刚提到了display link。还记得我们从AppDelegate中调用startRenderLoop和stopRenderLoop,使得当应用在活动时,可以周期性的刷新。  

//our render loop just tells the iOS device that we want to keep refreshing our view all the time - (void)startRenderLoop {     //check whether the loop is already running     if(displayLink == nil)     {         //the display link specifies what to do when the screen has to be redrawn,         //here we use the selector (method) drawFrame         displayLink = [self.window.screen displayLinkWithTarget:self selector:@selector(drawFrame)];                   //by adding the display link to the run loop our draw method will be called 60 times per second         [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];         NSLog(@"Starting Render Loop");     } } 

为了开始周期性的更新屏幕,当确定没有设置好displayLink时,创建一个屏幕的CADisplayLink,并且告诉它重绘屏幕的时候它需要做什么。我们将target设为self,选择selector drawFrame传递给方法displayLinkWithTarget。为了得到大约每秒60次左右的刷新率,需要将displayLink添加到系统的runloop里去。代码的下一行实现这个功能,我们调用NSRunLoop 的静态方法currentRunloop来的到系统的runLoop,然后将它传递给displayLink的addToRunLoop方法。 现在我们可以渲染了!但是,当app进入后台的时候,如何停止渲染呢?

//we have to be able to stop the render loop - (void)stopRenderLoop {     if (displayLink != nil) {         //if the display link is present, we invalidate it (so the loop stops)         [displayLink invalidate];         displayLink = nil;         NSLog(@"Stopping Render Loop");     } } 

为了停止displayLink,我们只需要调用方法invalidate即可。它自动执行清理工作。方法执行完毕后,指针指向的是未初始化的内存。我们需要把指针指向nil,千万不要让指针成为野指针!     我们差不多将EAGLView分析完毕了,只剩下两个简单的setter方法,一个析构函数(在Objective-C中叫dealloc方法),和一个窗口事件的回调函数。

//setter methods, should be straightforward - (void) setDepthBufferNeeded:(BOOL)needed {     useDepthBuffer = needed; }   - (void) setLesson:(Lesson*)newLesson {     lesson = newLesson;     //if we set a new lesson, it is not yet initialized!     lessonIsInitialized = FALSE; }   //As soon as the view is resized or new subviews are added, this method is called, //apparently the framebuffers are invalid in this case so we delete them //and have them recreated the next time we draw to them - (void)layoutSubviews {     [self deleteFramebuffer]; }   //cleanup our view - (void)dealloc {     [self deleteFramebuffer];        [context release];     [super dealloc]; } 

两个setter方法很容易理解。当UIView发生变化时,比如UIView的尺寸变了,或者有新的添加进来了新的subview,回调函数layoutSubviews就会被触发。在回调函数里,我们只需要删除framebuffer就可以了,因为drawFrame方法中,如果在渲染时发现framebuffer不存在的话,会生成一个新的framebuffer。

Dealloc方法清除我们创建的一切,即framebuffers和context。我们调用deleteFramebuffer来清除framebuffer。我们用release方法来释放context,因为它是通过garbage collection来管理的。最后我们调用父类的dealloc方法,在这里父类是UIView。在Objective-C里需要手动调用父类的dealloc方法,而在C++里父类的析构函数会被自动调用。

    Lesson   我们终于研究到了将在以后的课程中起到重要作用的类了。Lesson负责绘制每一个简单frame,它还初始化一些OpenGL的东西,比如加载图片,加载shaders,或者向显示芯片传送几何数据。看Basecode/Lesson.h

#include <OpenGLES/ES2/gl.h> #include <OpenGLES/ES2/glext.h>   //this is our general lesson class, providing the two most important methods init and draw //which will be invoked by our EAGLView class Lesson { public:     //constructor     Lesson();     //the destructor has always to virtual!     virtual ~Lesson();           //abstract methods init and draw have to be defined in derived classes     virtual void init() = 0;     virtual void draw() = 0;           //we need to know the size of our drawing canvas (called renderbuffer here),     //so this method just saves the parameters in the member variables     virtual void setRenderbufferSize(unsigned int width, unsigned int height);       //all protected stuff will be visible within derived classes, but from nowhere else    protected:     //fields for the renderbuffer size     unsigned int m_renderbufferWidth, m_renderbufferHeight; }; 

前两行代码包含了OpenGL头文件,这些头文件定义了所有的API。然后定义了类,它的接口很简单。它有一个构造函数和一个虚析构函数。在C++里,所有在派生类中要重写的函数,必须在父类和派生类中都定义为虚函数。记住,构造函数不能是虚函数,但是析构函数一定是虚函数。 init方法和draw方法定义了类接口的核心功能。因为Lesson只是一个通用接口,我们不希望在这里实现这两个方法,而是打算放在Lesson的派生类(比如Lesson01)中实现。这就是为什么这两个函数不但是虚函数,还被设为0的原因。在C++中这样做暗示了这个类是抽象类。抽象类是不能实例化的,这样就避免了没有实现的函数被非法调用。 还有两个protected的成员变量,用于记录renderbuffer的尺寸。protected指的是它们只在在当前类或者其派生类中可见,其他地方则不可见。我们在EAGLView中的createFramebuffer中调用了setRenderbufferSize方法将参数传给lesson,在这里我们接收输入的参数。     Basecode/Lesson.mm的实现是相当简单的。构造函数只是将renderbuffer的尺寸设为0,析构函数什么清理也不用做,因为在这里我们没有分配任何存储空间。

#include "Lesson.h"   //Lesson constructor, set default values Lesson::Lesson():     m_renderbufferWidth(0),     m_renderbufferHeight(0) {       }   //Lesson destructor Lesson::~Lesson() {     //cleanup here }   //save the renderbuffer size in the member variables void Lesson::setRenderbufferSize(unsigned int width, unsigned int height) {     m_renderbufferWidth = width;     m_renderbufferHeight = height;           glViewport(0, 0, m_renderbufferWidth, m_renderbufferHeight); } 

在setRenderbuffSize函数里有了一个有趣的OpenGL函数调用。在把宽度和高度保存到成员变量后,我们调用了glViewport(int left, int bottom, int width, int height)。通过这个函数,我们告诉OpenGL我们打算绘制到屏幕的哪一部分。通过把起始点设为左下角,使用全部的宽度和高度,我们指明了需要用到整个屏幕。         Lesson01   现在我们已经知道了应用程序的每一小部分,除了实际绘图的OpenGL部分。现在该开始了解了,我希望从现在起可以有较少的代码和较多的解释和图片:)

//We derive our current lesson class from the general lesson class class Lesson01 : public Lesson { public:     //overwrite all important methods     Lesson01();     virtual ~Lesson01();           virtual void init();     virtual void draw(); }; 

每一课我们都会派生出一个新的LessonXX的类,逐步加入越来越多的OpenGl特性。这节课的Lesson.h非常简单,我们只是实现了在超类Lesson中定义的那些抽象方法。 这节课我们开始最简单的操作:清除屏幕。代码在Lesson01.mm中。我们首先定义构造函数和析构函数,这两个函数什么也不做。

#include "Lesson01.h"   //lesson constructor Lesson01::Lesson01() {     //initialize values }   //lesson destructor Lesson01::~Lesson01() {     //do cleanup } 

  当我们初始化lesson对象的时候,设定想要使用的颜色来覆盖原有的任何颜色。在OpenGL中,颜色被指定为红,绿,蓝颜色通道我们熟知的RGB颜色)的强度(intensity)。强度值是一个介于0(无强度)和1(最大强度)之间的浮点数。有时候(比如在JPEG图像中)强度值是介于0和255之间的。下面添加的颜色模型允许我们描述计算机显示器可以显示的所有颜色。

 

图片 2

(图片来源: )

  我们将清除颜色设为红色。这意味着将红色通道设为最大强度,绿色和蓝色通道设为0强度。

//initializing all OpenGL related things void Lesson01::init() {     NSLog(@"Init..");           //set the color we use for clearing our colorRenderbuffer to red     glClearColor(1.0, 0.0, 0.0, 1.0); } 

看到没,要传递4个参数给glClearColor?最后一个参数指明了alpha值(RGBA颜色),alpha值定义了opacity,这个值在绝大部分面上都设为1。alpha值可用于每一个像素点混合当前的颜色和新的颜色(称为blending),因为在这里我们将每个像素的值设为RGBA-tupel,不使用 blending,所以实际上alpha值是无关紧要的。 下面我们告诉OpenGL我们希望清除color buffer。这可以用glClear(GL_COLOR_BUFFER_BIT)命令来实现。因为我们在每一帧的开始都要用这个命令,所以把它放到draw()方法中。

//drawing a frame void Lesson01::draw() {     //clear the color buffer     glClear(GL_COLOR_BUFFER_BIT);           //everything should be red now! yay :) } 

祝贺你!你已经完成了你在iOS上的第一个OpenGL程序了。试试别的颜色,确信你已经理解了RGB颜色模型,下一次我们就真的要开始绘画了。   敬请关注! Carsten      

jimmyzhouj 翻译] Nehe iOS OpenGL ES 2.0教程 引子 : 最近要学习iOS 上的OpenGL ES的内容,在互联网上找了一些教程来看。发现关于OpenGL ES2.0的教程...

本文由美高梅4688官方网站发布于美高梅4688官方网站,转载请注明出处:当今启幕大家新的iOS OpenGL ES连串教程,在此篇作品里

您可能还会对下面的文章感兴趣: