[OpenGL ES 01]OpenGL ES之初体验
罗朝辉 ()
一,什么是 OpenGL ES?
OpenGL ES 是专门为手持设备制定的 3D 规范,它是 OpenGL 的简化版,该规范由制定,目前最新规范版本为 。 OpenGL ES 可以在不同手机系统上实现,也可以在浏览器上实现(Web GL)。目前较新的 iOS 支持OpenGL ES 2.0,在这里,我将介绍如何在 iOS 上使用 OpenGL ES 2.0。
二,在iOS上如何使用OpenGL ES?
1,准备工作
1),打开XCode(我使用的是4.2),创建一个 Empty Application。
2),命名为 Tutorial01,选择Device Family为iPhone,保持默认选中的use Automatic Reference Counting来使用自动引用计数。
3),添加需要用到的库,在iOS平台上进行OpenGL ES 开发,OpenGLES.framework和QuartzCore.framework这两个库是必须的,选中Target:Tutorial01,在Build phase->Link Binary With Libraries中点击 + 号来添加这两个库:
添加完毕,工程结构如下图,你可以把这两个 framework 拖到 Frameworks 文件夹中,谁也不想工程结构乱七八糟的吧?
4),至此,编译运行,模拟器是一片空白的!因为Empty Application模版就是Empty,里面甚至连一个Window都木有。因此,我们需要添加一个 Window。右击 Supporting Files文件夹,选择 New File->User Interface->Window:
输入名称:MainWindow
5),为了让 AppDelegate 与 Window 关联起来,我们还需要在MainWindow.xib中创建一个Object对象。选中MainWindow.xib,向其中拖入一个 Object 对象:
添加完毕,效果如下:
6),然后我们修改该 Object 的Custom Class为 AppDelegate,这样它在 xib 中代表代码中的 AppDelegate了。
7),为了将 Window与App Delegate 关联起来,我们需要在 AppDelegate.h中的代码 window 属性前添加 IBOutlet 修饰符:
@property (strong, nonatomic) IBOutlet UIWindow *window;
8),选中MainWindow.xib,右击 AppDelegate,将Outlet window拖拽到其上方的 Window上,这样AppDelegate中的window就与真实的 Window 关联起来。
9),同样,我们还需要修改File's Owner的 Custom Class 为 UIApplication,使用与8)中同样的拖拽技巧,将 File's Owner的 delegate 与 App Delegate 关联起来。
10),至此准备工作完毕,不妨编译运行一下,模拟器依然一片空白,那是因为我们还没有在 Window 上添加 view,下面我们将来添加一个 view。
2,设置 OpenGL ES 运行环境
1),虽然 iOS 5在 GLKit 中提供了方便使用 OpenGL ES 的辅助 GLKView,但在这里,我们还是从零开始手工打造我们自己 GL ES view,从而更进一步了解在 iOS 上 OpenGL ES 是使用的。在Tutorial01目录中 New File,选择 User Interface->View作为模版,命名为 OpenGLView:
2),修改 OpenGLView.h为:
#import#import #include #include @interface OpenGLView : UIView { CAEAGLLayer* _eaglLayer; EAGLContext* _context; GLuint _colorRenderBuffer; GLuint _frameBuffer;}@end
这些变量在后面会有介绍。
3),在 OpenGLView.m 中添加如下函数:
+ (Class)layerClass { // 只有 [CAEAGLLayer class] 类型的 layer 才支持在其上描绘 OpenGL 内容。 return [CAEAGLLayer class];}
为了让 UIView 显示 opengl 内容,我们必须将默认的 layer 类型修改为 CAEAGLLayer 类型(这种动态修改返回类类型的手段在 一文也有应用)。
4),默认的 CALayer 是透明的,我们需要将它设置为 opaque 才能看到在它上面描绘的东西。为此,我们使用匿名 category 技巧,在 OpenGLView.m的开头(在@implementation OpenGLView 的上面)添加匿名 category,并声明私有函数 setupLayer:
// 使用匿名 category 来声明私有成员@interface OpenGLView()-(void)setupLayer;@end
接着,在 @implementation 与 @end 之间,添加 setupLayer 的实现:
- (void)setupLayer{ _eaglLayer = (CAEAGLLayer*) self.layer; // CALayer 默认是透明的,必须将它设为不透明才能让其可见 _eaglLayer.opaque = YES; // 设置描绘属性,在这里设置不维持渲染内容以及颜色格式为 RGBA8 _eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];}
5),至此 layer 的配置已经就绪,下面我们来创建与设置与 OpenGL ES 相关的东西。首先,我们需要创建OpenGL ES 渲染上下文(在iOS中对应的实现为),这个 context 管理所有使用OpenGL ES 进行描绘的状态,命令以及资源信息。然后,需要将它设置为当前 context,因为我们要使用 OpenGL ES 进行渲染(描绘)。在匿名 category 中添加 -(void)setupContext; 声明,并在@implement与@end之间添加其实现。这与使用 Core Graphics 进行描绘必须创建 Core Graphics Context 的道理是一样。
- (void)setupContext { // 指定 OpenGL 渲染 API 的版本,在这里我们使用 OpenGL ES 2.0 EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2; _context = [[EAGLContext alloc] initWithAPI:api]; if (!_context) { NSLog(@"Failed to initialize OpenGLES 2.0 context"); exit(1); } // 设置为当前上下文 if (![EAGLContext setCurrentContext:_context]) { NSLog(@"Failed to set current OpenGL context"); exit(1); }}
6),创建 renderbuffer
有了上下文,openGL还需要在一块 buffer 上进行描绘,这块 buffer 就是 RenderBuffer(OpenGL ES 总共有三大不同用途的color buffer,depth buffer 和 stencil buffer,这里是最基本的 color buffer)。下面,我们依然创建私有方法 setupRenderBuffer 来生成 color buffer:
- (void)setupRenderBuffer { glGenRenderbuffers(1, &_colorRenderBuffer); glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer); // 为 color renderbuffer 分配存储空间 [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];
glGenRenderbuffers 的原型为:
void glGenRenderbuffers (GLsizei n, GLuint* renderbuffers)
它是为 renderbuffer 申请一个 id(或曰名字)。参数 n 表示申请生成 renderbuffer 的个数,而 renderbuffers 返回分配给 renderbuffer 的 id,注意:返回的 id 不会为0,id 0 是OpenGL ES 保留的,我们也不能使用 id 为0的 renderbuffer。
glBindRenderbuffer 的原型为:
void glBindRenderbuffer (GLenum target, GLuint renderbuffer)
这个函数将指定 id 的 renderbuffer 设置为当前 renderbuffer。参数 target 必须为 GL_RENDERBUFFER,参数 renderbuffer 是就是使用 glGenRenderbuffers 生成的 id。当指定 id 的 renderbuffer 第一次被设置为当前 renderbuffer 时,会初始化该 renderbuffer 对象,其初始值为:
width 和 height:像素单位的宽和高,默认值为0;
internal format:内部格式,三大 buffer 格式之一 -- color,depth or stencil;
Color bit-depth:仅当内部格式为 color 时,设置颜色的 bit-depth,默认值为0;
Depth bit-depth:仅当内部格式为 depth时,默认值为0;
Stencil bit-depth: 仅当内部格式为 stencil,默认值为0;
函数 - (BOOL)renderbufferStorage:(NSUInteger)target fromDrawable:(id<EAGLDrawable>)drawable; 在内部使用 drawable(在这里是 EAGLLayer)的相关信息(还记得在 setupLayer 时设置了drawableProperties的一些属性信息么?)作为参数调用了 glRenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height); 后者 glRenderbufferStorage 指定存储在 renderbuffer 中图像的宽高以及颜色格式,并按照此规格为之分配存储空间。在这里,将使用我们在前面设置 eaglLayer 的颜色格式 RGBA8, 以及 eaglLayer 的宽高作为参数调用 glRenderbufferStorage。
7),创建 framebuffer object
framebuffer object 通常也被称之为 FBO,它相当于 buffer(color, depth, stencil)的管理者,三大buffer 可以附加到一个 FBO 上。我们是用 FBO 来在 off-screen buffer上进行渲染。下面,我们依然创建私有方法 setupFrameBuffer 来生成 frame buffer:
- (void)setupFrameBuffer { glGenFramebuffers(1, &_frameBuffer); // 设置为当前 framebuffer glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer); // 将 _colorRenderBuffer 装配到 GL_COLOR_ATTACHMENT0 这个装配点上 glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderBuffer);}
setupFrameBuffer 大体与前面的 setupRenderBuffer 相同,由 glGenFramebuffers分配的 id也不可能是 0,id 为 0 的 framebuffer 是OpenGL ES 保留的,它指向窗口系统提供的 framebuffer,我们同样不能使用 id 为 0 的framebuffer,否则系统会出错。glFramebufferRenderbuffer的函数原型为:
void glFramebufferRenderbuffer (GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer)
该函数是将相关 buffer(三大buffer之一)attach到framebuffer上(如果 renderbuffer不为 0,知道前面为什么说glGenRenderbuffers 返回的id 不会为 0 吧)或从 framebuffer上detach(如果 renderbuffer为 0)。参数 attachment 是指定 renderbuffer 被装配到那个装配点上,其值是GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT中的一个,分别对应 color,depth和 stencil三大buffer。
8),当 UIView 在进行布局变化之后,由于 layer 的宽高变化,导致原来创建的 renderbuffer不再相符,我们需要销毁既有 renderbuffer 和 framebuffer。下面,我们依然创建私有方法 destoryRenderAndFrameBuffer 来销毁生成的 buffer:
- (void)destoryRenderAndFrameBuffer{ glDeleteFramebuffers(1, &_frameBuffer); _frameBuffer = 0; glDeleteRenderbuffers(1, &_colorRenderBuffer); _colorRenderBuffer = 0;}
9), 至此,理论也讲得足够多了,让我们来画点东西看看效果如何。下面,我们依然创建私有方法 render 来进行真正的描绘:
- (void)render { glClearColor(0, 1.0, 0, 1.0); glClear(GL_COLOR_BUFFER_BIT); [_context presentRenderbuffer:GL_RENDERBUFFER];}
glClearColor (GLclampf red, GLclampf green, GLclampf blue, GLclampfalpha) 用来设置清屏颜色,默认为黑色;glClear (GLbitfieldmask)用来指定要用清屏颜色来清除由mask指定的buffer,mask 可以是 GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT的自由组合。在这里我们只使用到 color buffer,所以清除的就是 clolor buffer。- (BOOL)presentRenderbuffer:()target 是将指定 renderbuffer 呈现在屏幕上,在这里我们指定的是前面已经绑定为当前 renderbuffer 的那个,在 renderbuffer 可以被呈现之前,必须调用 为之分配存储空间。在前面设置 drawable 属性时,我们设置 为FALSE,表示不想保持呈现的内容,因此在下一次呈现时,应用程序必须完全重绘一次。将该设置为 TRUE 对性能和资源影像较大,因此只有当renderbuffer需要保持其内容不变时,我们才设置 为 TRUE。
三,进行渲染
1,有了前面的准备工作,我们来看看我们的成果吧。首先在 AppDelegate中使用 OpenGLView作为 window 的view,修改 AppDelegate.h为:
#import#import "OpenGLView.h"@interface AppDelegate : UIResponder { OpenGLView* _glView;}@property (strong, nonatomic) IBOutlet UIWindow *window;@property (strong, retain) IBOutlet OpenGLView *glView;@end
2,在 AppDelegate.m 中实现如下代码:
@implementation AppDelegate@synthesize window = _window;@synthesize glView = _glView;- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{ self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; CGRect screenBounds = [[UIScreen mainScreen] bounds]; self.glView = [[OpenGLView alloc] initWithFrame:screenBounds]; [self.window addSubview:self.glView]; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES;}
由于我们使用 ARC,所以不必担心资源的释放。
3,返回 OpenGLView.m,在其中添加函数:
- (void)layoutSubviews { [self setupLayer]; [self setupContext]; [self destoryRenderAndFrameBuffer]; [self setupRenderBuffer]; [self setupFrameBuffer]; [self render];}
4,编译运行,小功告成:
5,如果你还没有保存你的代码,选择 File-Source Control->Commit, 提交你的代码到 git 中吧,时常提交代码是个好习惯。后续文章我们还将使用到在这里编写的代码。本文源代码可以在这里查看与下载:
四,Refference