OpenGL ES第一篇上下文配置及基本使用
配置 OpenGL ES 上下文
每一个OpenGL ES的实现都提供了一种创建上下文的方式去管理OpenGL ES规范所必须的状态。在一个上下文中处理这些状态,多个应用共享图形硬件而不会干扰其它的状态。我们将会详细介绍如何在iOS中创建和配置上下文
EAGL是iOS中OpenGL ES渲染上下文的实现
在你的应用调用OpenGL ES函数前,必须初始化一个
EAGLContext
对象。EAGLContext
类同时也提供了用于将OpenGL ES内容与核心动画集成的方法。
当前上下文是OpenGL ES函数调用的目标
iOS应用中的每一个线程都有一个当前上下文,当你调用OpenGL ES函数,通过这个调用上下文的状态会被改变。要设置线程的当前上下文,使用
[EAGLContext setCurrentContext:myContext]
,获取当前上下文的[EAGLContext currentContext]
。
* 如果在同一个线程中两个或更多上下文中切换,在设置一个新的上下文为当前上下文前要调用glFlush
函数。这样确保之前提交的命令及时交付图形硬件。
OpenGL ES对EAGLContext对象拥有一个强引用,对应于当前上下文。(如果使用手动引用计数器,OpenGL ES持有该对象)当调用
setCurrentContext:
方法改变当前上下文,OpenGL ES不再引用之前的上下文。为了防止EAGLContext对象从不是当前上下文中被销毁,你的应用必须保持强引用(or retain)这些对象。
每个上下文面向一个指定的OpenGL ES版本
一个EAGLContext对象仅一个OpenGL ES版本。为OpenGL ES 1.1版本写的代码不兼容2.0或3.0的上下文。使用OpenGL ES 2.0核心功能代码是与3。0上下文兼容,为OpenGL ES 2.0扩展设计的代码通常可以使用在3.0的上下文中,只需很小的修改。3.0的很多新特性和增强硬件的能力必须要3.0的上下文。当你创建和初始化EAGLContext对象时,你的应用将会决定支持哪个版本的OpenGL ES。如果你的设备支持要求版本的OpenGL ES,
initWithAPI:
方法会返回nil
。应用在使用前必须要测试确保上下文初始化成功。为了支持多个版本的OpenGL ES在你的应用中作为渲染选项,你应该首先尝试初始化一个最新版本的上下文。如果返回nil
,再初始化一个较老的版本。示例如下:
EAGLContext *createBestEAGLContext() {
EAGLContext *ctx = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES3];
if (ctx == nil) {
ctx = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
}
return ctx;
}
一个 EAGL ShareGroup 为上下文管理OpenGL ES对象组
尽管上下文持有OpenGL ES的状态,但不直接管理OpenGL ES对象组。相反,OpenGL ES对象组由EAGLSharegroup对象创建和维护的。每个上下文包含一个EAGLSharegroup对象,它将对象的创建委托给这个对象。
在两个或多个上下文引用同一个sharegroup时,它的优势是显而易见的。在多个上下文被连接到共同的sharegroup,在任一上下文中创建OpenGL ES对象可以用在所有的上下文中。如果绑定到另一个上下文中与创建它的上下文相同对象标识符,则引用相同的OpenGL ES对象。在移动设备上资源是很稀缺的,在不同上下文中同一份内容创建多个副本是很浪费的。共享公用的资源可以更好的利用设备上图形资源
sharegroup是一个抽象对象,没有方法和属性可以直接调用。使用sharegroup对象的上下文对它保持一个强引用。
- sharegroups在以下两种场景中非常有用:
- 大部分资源在上下文之间共享且不改变
- 当你的应用想在另一个线程而非主线程为渲染器创建新的OpenGL ES对象组时。在这种情况下,第二个上下文运行在独立的线程,专心的获取数据和创建资源。在资源加载完成,第一个上下文可以绑定到这个对象上且可以立即使用。
GLKTextureLoader
类使用此种模式提供纹理异步加载。
// 为了创建多个上下文引用相同的sharegroup,第一个上下文通过调用 initWithAPI: 来初始化,sharegroup会自动被这个上下文创建
// 第二个或后面的上下文通过第一个上下文的sharegroup调用 initWithAPI: sharegroup:来初始化。
EAGLContext *firstCtx = createBestEAGLContext()
EAGLContext *secondCtx = [[EAGLContext alloc] initWithAPI: [firstCtx API] sharegroup: [firstCtx sharegroup]];
-
当sharegroup被共享在多个上下文中时,应用要负责管理OpenGL ES对象组的状态改变。规则如下:
- 如果对象没有被修改,你的应用可能会同时跨多个上下文访问该对象
- 当对象通过传送到上下文的命令正在被修改,该对象不准被任何其它上下文读或修改
- 在该对象修改完成,所有的上下文必须重新绑定该对象以查看更改。如果上下文在绑定对象之前引用它,则对象的内容是未定义的。
-
你的应用应当按照如下步骤更新OpenGL ES对象
-
- 在可能使用该对象的每一个上下文中调用 glFlush
-
- 在想修改该对象的上下文中,调用一个或多个OpenGL ES 函数更改该对象
-
- 在接收状态修改命令的上下文中调用 glFlush
-
- 在其它上下文中,重新绑定该对象的标识符
-
* 另一种方式共享对象是使用单一渲染上下文,但多个目标帧缓存。在渲染时,你的应用绑定合适的帧缓存,并根据需要渲染帧。因为所有的OpenGL ES对象都是从一个上下文中引用的,所以它们查看的是相同的OpenGL ES数据。此种模式使用更少的资源,但仅对单线程应用程序有用,在单线程中可以详细的控制上下文的状态。
用OpenGL ES和GLKit绘图
GLKit提供了视图和视图控制器的类,用来消除绘图和动画OpenGL ES内容所需的创建和维护代码。GLKView类管理OpenGL ES基础结构为绘制代码提供地方,GLKViewController类为GLKit视图中OpenGL ES内容动画平滑提供了一个呈现循环。这些类扩展了标准的UIKit的设计模式来绘制视图内容和管理视图呈现。这样可以把精力主要放在OpenGL ES渲染代码和快速启动和运行你的应用。GLKit还提供了其它特性来简化OpenGL ES2.0和3.0的开发。
根据需要用GLKit视图绘制OpenGL ES内容
GLKView类提供一个基于OpenGL ES与UiView相同的绘制循环。一个UIView实例会自动配置自己的图形上下文,因此在drawRect:方法实现中仅仅需要执行Quartz 2D绘制命令即可,所以GLKView的实例会自动配置你绘制的方法只需执行OpenGL ES绘图命令。GLKView类是通过维护一个 framebuffer 对象(此对象持有OpenGL ES的绘制命令)来提供此功能。最后一旦绘制方法返回会自动呈现到 Core Animation。像标准UIKit的视图一样, GLKit的视图也会根据需要去渲染自身内容。如果你的视图是首次展现,会调用你的绘制方法(Core Animation 会缓存渲染输出结果和当视图要出现时会展示)。当你想改变视图的内容时,要调用
setNeedsDisplay
方法,这样视图将会再次调用绘制方法,缓存生成的图像,将其显示在屏幕上。这个方法对渲染图像不经常或仅只在用户操作做出响应时很有用。通过只在需要时才呈现新的视图的内容,你可以节省设备电池电量,并为设备执行其它操作留有更多的时间。
创建和配置一个GLKit视图
当然创建和配置一个GLKView对象都可以通代码或 Xib。在使用它绘制前,你必须要关联一个
EAGLContext
对象。
代码创建视图,首先要创建一个上下文,然后将它传递给视图的initWithFrame:context:
方法
从storyboard加载一个视图,创建一个上下文并将其赋值绘视图的context
属性
GLKit视图会自动创建和配置自己的OpenGL ES 的framebuffer对象和renderbuffers。你使用视图的可绘制的属性来控制这些对象的属性(下面有示例)。如若你要改变GLKit视图尺寸、绅缩因子或可绘制属性,它会在下次内容重绘时自动删除和重新创建合适的framebuffer对象和renderbuffers。
- (void)viewDidLoad{
[super viewDidLoad];
// 创建OpenGL ES上下文并赋值给storyboard加载的视图
GLKView *view = (GLKView *)self.view;
view.context = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
// 配置视图的renderbuffers
view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
view.drawableStencilFormat - GLKViewDrawableStencilFormat8;
// 可多重采样
view.drawableMultisample = GLKViewDrawableMultisample4X;
}
用GLKit视图绘制
上面的示例代码通过三步绘制OpenGL ES内容:准备OpenGL ES的基础部分、分配绘制命令、将呈现内容显示到Core Animation中。 GLKView类实现了第一步和第三步,第二步你的实现可以参考下面
- (void)drawRect:(CGRect)rect {
// 清除 framebuffer
glClearColor(0.0f, 0.0f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 使用之前配置好的 texture, shader, uniforms, vertex array 绘制
glBindTexture(GL_TEXTURE_2D, _planetTexture);
glUseProgram(_diffuseShading);
glUniformMatrix4fv(_uniformModelViewProjectionMatrix, 1, 0, _modelViewProjectionMatrix.m);
glBindVertexArrayOES(_planetMesh);
glDrawElements(GL_TRIANGLE_STRIP, 256, GL_UNSIGNED_SHORT);
}
GLKView类能够为OpenGL ES绘制提供简单的接口,因为它管理标准OpenGL ES渲染的过程:
- 在调用绘制方法前,视图
+ 会让EAGLContext对象是当前上下文
+ 根据当前尺寸、绅缩因子、可绘制属性(如果需要)创建framebuffer对象和renderbuffers
+ 绑定framebuffer对象作为当前绘制命令的目标
+ 设置OpenGL ES视图端口以匹配framebuffer的大小
- 绘制方法返回后,视图
+ 解析多采样缓冲区(如果启用了)
+ 丢弃不再需要的renderbuffer内容
+ 呈现renderbuffer内容到CoreAniamtion中进行缓存和显示
使用代理对象渲染
很多OpenGL ES的应用实现渲染的代码在自定义的类中。这种做法的有利之处在于可以通过定义不同的渲染类,很容易支持多个渲染算法。渲染算法可以共享从父类继承的功能。例如你可能使用不同的渲染类支持OpenGL ES 2.0和3.0。或者你可能自定义渲染算法只为更好的图片质量。GLKit非常适合这种方法,你可以使你的render对象成为一个标准GLKView实例的代理。取代子类化一个GLKView并实现drawRect:方法,你的render类遵循GLKViewDelegate协议和实现glkView:drawInRect:方法。如下示例
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 创建上下文来测试特性
EAGLContext *context = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext: context];
// 根据设备特性选择一个render类
GLint maxTextureSize;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);
if (maxTextureSize > 2048) {
self.renderer = [MyBigTextureRender alloc] initWithContext: context];
} else {
self.renderer = [MyRenderer alloc] initWithContext: context];
}
// 使renderer成为视图的代理
GLKView *view = (GLKView * )self.window.rootViewController.view;
view.delegate = self.renderer;
// 将OpenGL ES上下文给这个视图就可以绘制
view.context = context;
return YES;
}
GLKit视图控制器可使OpenGL ES内容动画
通常,一个GLKView对象根据要求渲染内容。也就是说使用OpenGL ES绘图的一个关键优势是它能够使用图形硬件对复杂场景进行连续动画——游戏和模拟等应用很少呈现静态图像。对这些情况下,GLKit框架提供一个视图控制器类,用于维护它所管理的GLKView对象的动画循环。这个循环遵循游戏和模拟中常见的设计模式,分为两个阶段:更新和显示。
理解动画循环:在update阶段,控制器调用自己的update方法(或自身代理的glkViewControllerUpdate:方法)。在这个方法中你要准备下一帧的绘制。例如,一个游戏可能会使用这种方法来根据上一帧之后接收到的输入事件来确定玩家和敌人角色的位置,而一个科学的可视化可能会使用这种方法来运行模拟一个步骤。如果你需要时间信息来决定你应用下一帧的状态,可使用控制器的时间属性如timeSinceLastUpdate属性。在display阶段:视图控制器调用视图的display方法,视图的显示方法会调用视图的绘图方法。在绘图方法中,你提交OpenGL ES绘图命令给GPU去渲染你的内容。为了获得最佳性能,你的应用应该在开始新的帧渲染时修改OpenGL ES对象,并随后提交绘图命令。
动画循环以视图控制器的framesPerSecond属性所指示的速率在这两个阶段之间交替。你可以使用preferredFramesPerSecond属性来设置所需的帧速率———为了优化当前显示硬件的性能,视图控制器会自动选择一个接近你的首选值的最优帧速率。
使用GLKit视图控制器
下面是使用GLKViewController子类和GLKView实例呈现动画OpenGL ES内容的典型策略
@implementation PlanetViewController // subclass of GLKViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建上下文并赋值给视图
GLKView *view= (GLKView *)self.view;
view.context = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
// 设置动画帧率
self.preferredFramesPerSecond = 60;
// 加载shaders, texttures, vertex arrays, 设置工程矩阵
[self setupGL];
}
- (void)update{
_rotation += self.timeSinceLastUpdate * M_PI_2; // 1/4 旋转/s
// 设置变换矩阵
GLKMatrix4 modelViewMatrix = GLKMatrix4MakeRotation(_rotation, 0.0f, 1.0f, 0.0f);
_normalMatrix = GLKMatrix3InvertAndTranspose(GLKMatrix4GetMatrix3(modelViewMatrix), NULL);
_modelViewProjectionMatrix = GLKMatrix4Multiply(_projectionMatrix, modelViewMatrix);
}
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect{
// 清除framebuffer
glClearColor(0.0f, 0.0f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 设置uniforms shader
glUseProgram(_diffuseShading);
glUniformMatrix4fv(_uniformModelViewProjectionMatrix, 1, 0, _modelViewProjectionMatrix.m);
glUniformMatrix3fv(_uniformNormalMatrix, 1, 0, _normalMatrix.m);
// 使用先前配置的texture, vertex array绘图
glBindTexture(GL_TEXTURE_2D, _planetTexture);
glBindVertexArrayOES(_planetMesh);
glDrawElements(GL_TRIANGLE_STRIP, 256, GL_UNSIGNED_SHORT, 0);
}
@end
使用GLKit开发渲染器
除了视图和视图控制器基础结构之外,GLKit框架还提供了其它几个特性,以简化iOS上的OpenGL ES的开发
处理向量和矩阵数学OpenGL ES2.0及以后的版本不提供用于创建或指定转换矩阵的内置函数。相反,可编程着色器提供了顶点变换,你可以使用通用的统一变量来指定着色器的输入。GLKit框架包含一个全面向量和矩阵类型和函数库,针对iOS硬件的高性能进行了优化。
OpenGL ES2.0及以后的版本删除了所有与OpenGL ES1.1固定函数图形管道相关的功能。GLKBaseEffect类为OpenGL ES1.1管道的转换、点亮、阴影阶段提供了OC模拟,而GLKSkyboxEffect和GLKReflectionMapEffect类增加了对常见视觉效果的支持。
GLKTextureLoader类提供了一种简单的方法,可以同步或异步地将来自iOS支持的任何图像格式的纹理数据加载到OpenGL ES上下文中。