您的位置:首页 > 移动开发 > IOS开发

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

2014-03-01 14:36 537 查看
http://jimmyzhouj.blog.51cto.com/2317513/882349/

引子:

最近要学习iOS 上的OpenGL ES的内容,在互联网上找了一些教程来看。发现关于OpenGL ES2.0的教程不多。想起了知名的Nehe OpenGL 教程,就上nehe.gamedev.net上找了找,发现他们有一个用于移动设备的OpenGL ES教程,并且是基于OpenGL ES 2.0的。现在还只有两篇教程,我先把这两篇翻译出来,以供大家学习。有什么问题需要交流的,可以联系我: jimmyzhouj@gmail.com

免责声明:本教程翻译原稿均来自互联网,仅供学习交流之用,请勿进行商业传播。同时,转载时不要移除本声明。如产生任何纠纷,均与本博客所有人和发表该翻译稿之人无任何关系。谢谢合作!

IOS Lesson 01 – 设置GL ES

原文链接地址:http://nehe.gamedev.net/tutorial/ios_lesson_01__setting_up_gl_es/44001/

大家好,现在开始我们新的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++的类(青色框)。





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

我们要钩到操作系统的run loop中去,尽可能频繁的重绘frame。在之后关于动画的课程中这是必须的。绘画动作是在Lesson对象中的draw方法中实现的,或者更精确的说,是在Lesson01对象中实现的,因为在类Lesson中,init()和draw()都是虚函数。

简单浏览

我们开始一步一步实现。你可以从这里(http://code.google.com/p/nehe/downloads/list) 获取代码,然后打开项目文件Lesson01.xcodeproj。

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

你会在项目左侧看到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中。

#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;
}


这个方法在每个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)才能看到其中的内容。

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

Lesson01AppDelegate

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

#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


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

窗口创建后,触发的第一个事件是didFinishLauchingWithOptions。代码如下,在Lesson01AppDelegate.mm中。

#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之间的。下面添加的颜色模型允许我们描述计算机显示器可以显示的所有颜色。





(图片来源: http://en.wikipedia.org/wiki/File:RGB_farbwuerfel.jpg)
我们将清除颜色设为红色。这意味着将红色通道设为最大强度,绿色和蓝色通道设为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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: