集成并启动OpenGL ES应用程序

这些章节概述了OpenGL ES如何集成到iOS中,以及在iOS设备上启动并运行第一个OpenGL ES应用所需的所有详细信息。

配置OpenGL ES上下文

每个OpenGL ES实现都提供了一种创建渲染上下文的方法,来管理OpenGL ES规范要求的状态。通过将这个状态置于上下文中,多个app可以简单的共享图形硬件,而不会干扰其他状态

EAGL是OpenGL ES渲染上下文的iOS实现

在app调用任何OpenGL ES函数之前,必须初始化EAGLContext对象。其还提供了用于将OpenGL ESCore Animation集成的方法

OpenGL ES函数调用的目标即为 Current Context

iOS中的每个线程都有其上下文 当调用OpenGL ES函数时,这是其状态被调用更改的上下文

通过EAGLContext 的 setCurrentContext方法设置当前上下文[EAGLContext setCurrentContext: myContext];
而通过调用EAGLContextcurrentContext来检索线程的当前上下文

当应用程序在同一线程的两个或者多个上下文间主动切换时,请在将新的上下文设置为当前上下文前,调用glFlush函数,可以确保将先前提交的命名即使交付给图形硬件

OpenGL ES对当前上下文的EAGLContext对象具有强引用。当设置setCurrentContext时,就不在引用先前的context。因此,如果想context在不是当前上下文时不被dealloc,app就要保持对这些对象的强引用

针对特定版本的OpenGL ES有特定的上下文

一个EAGLContext仅支持OpenGL ES的一个版本。
应用会在创建和初始化EAGLContext时决定要支持的OpenGL ES版本。如果设备不支持该版本,则initWithAPI:方法会返回nil,因此应用必须进行测试来确保上下文在使用前已经初始化成功。因此,可以再app中支持多版本的OpenGL ES,先尝试最新版本的渲染上下文,如果返回为nil,则尝试初始化旧版本的上下文

EAGLContext* CreateBestEAGLContext()
{
   EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
   if (context == nil) {
      context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
   }
   return context;
}

contextAPI属性@property(readonly) EAGLRenderingAPI API;说明了上下文支持的OpenGL ES版本。app应该测试context的api属性,来选择正确的渲染路径,我们通常为每个渲染路径创建一个类,然后app会在初始化时测试context并创建一次渲染器。

EAGL Sharegroup 为上下文管理OpenGL ES对象

尽管Context保持了OpenGL ES状态,但是并不直接管理OpenGL ES对象。OpenGL ES对象是由EAGLSharegroup对象进行创建和维护。每个context都包含一个EAGLSharegroup对象,将对象的创建委托给该对象

当多个context连接到同一Sharegroup时(可见sharegroup的优势),任一context创建的OpenGL ES对象,在所有context中均是可用的。如果在创建的context与其它context绑定了相同的对象标识符,则就引用相同的OpenGL ES对象。

移动设备资源短缺,多个context创建相同内容的多个副本是浪费的,共享资源可以更好地利用设备上的可用图形资源

sharegroup是不透明对象,并没有app可以调用的属性和方法,使用sharegroup的context对其保持强引用

Sharegroups在两种特定情况下很有用

  • 当context之间共享的大多数资源都不变时
  • 当想要app能在渲染主线程外的其它线程创建新的OpenGL ES对象时。此时,第二个context在单独线程运行,并且用于获取数据和创建加载资源。加载资源后,第一个线程可以绑定到该对象并立即使用它。GLKTextureLoader就是使用这种方式实现异步纹理加载

如果要创建引用同一sharegroup的多个context,可以通过initWithAPI:来初始化第一个context,此时将会自动创建sharegroup。然后通过调用initWithAPI:sharegroup:将其他context初始化为第一个context的共享组。

同一sharegroup的所有context必须使用相同的OpenGL ES版本

EAGLContext* firstContext = CreateBestEAGLContext();
EAGLContext* secondContext = [[EAGLContext alloc] initWithAPI:[firstContext API] sharegroup: [firstContext sharegroup]];

当多个context共享sharegroup时,应用有责任管理OpenGL ES的状态更改,

  • 未修改对象时,app可同时在多个上下文中访问该对象
  • 通过发送到context的命令修改对象时,不能在其他context中读取或者修改对象
  • 当修改对象后,所有context必须重新绑定对象来查看其更改。在context对其绑定之前 引用对象,该对象内容是undefined的

应用更新OpenGL ES对象应遵循的步骤:

  1. 在可能正在使用该对象的每个context中调用glFlush
  2. 在想要修改对象的context中,调用一个或者多个OpenGL ES函数来更改对象
  3. 在收到状态修改命令的上下中调用glFlush
  4. 在所有的其它上下文中,重新绑定该对象标识符

共享对象的另一种方法是使用单个渲染上下文,但使用多个目标帧缓冲区。在渲染时,您的应用绑定了适当的帧缓冲区并根据需要渲染其帧。由于所有OpenGL ES对象都是从单个上下文中引用的,因此它们将看到相同的OpenGL ES数据。此模式使用的资源较少,但是仅对单线程应用有用,在该应用中您可以仔细控制上下文的状态

使用GLKit绘图

GLKit framework提供view和viewController,消除了对OpenGL内容绘制和动画必须设置和维护的代码。
GLKView类管理OpenGL ES基础结构为绘图代码提供位置
GLKViewController类提供了渲染循环 来使GLKit中的OpenGL内容可以平滑动画。
GLKit帮助我们主要将精力集中在OpenGL ES渲染代码上,并使app可以快读启动并运行。
GLKit还提供了其它功能来简化OpenGL2.0和3.0的开发

GLKit View按需绘制OpenGL内容

GLKView提供了基于OpenGL的等效于标准的UIView的绘图周期。类似UIView自动配置其图形上下文,方便我们实现drawRect:方法只需要执行Quartz 2D命令,而GLKView实例自动进行自身配置,以便于我们绘制方法仅需要执行OpenGL绘制命令。GLKView类通过维护了保存OpenGLES绘制命令结果的帧缓冲区对象,然后在绘制方法return后自动将其呈现给Core Animation

像标准的UIKit UIView一样,GLKit View按需呈现其内容。首次显示View时,它会调用您的绘制方法--Core Animation缓存渲染的输出,并在无论何时显示View时将其展示。当您想要更改View的内容时,请调用其setNeedsDisplay方法,然后该视图再次调用您的绘图方法,缓存生成的图像,并将其显示在屏幕上。当用于渲染图像的数据不经常更改或仅响应用户操作而更改时,此方法很有用。通过仅在需要时渲染新的视图内容,可以节省设备的电池电量,并为设备留出更多时间执行其他操作

创建和配置GLKit View

可以通过代码或者XIB创建GLKView对象,但是在试用其之前必须将其与EAGLContext对象相关联

  • 当以代码方式创建时,请先创建一个context然后将其传递给view的initWithFrame:context:方法
  • 如果容storyboard中加载视图后,创建一个context并将其设置为视图的context属性值

GLKView会自动创建和配置它自己的OpenGL的framebuffer对象和renderbuffer。可以使用drawable属性控制这些对象的属性,如果更改GLKit Viewsizescale factor或者drawable属性,则会在下次绘制其内容时自动删除并重新创建适当的framebufferrenderbuffer

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // Create an OpenGL ES context and assign it to the view loaded from storyboard
    GLKView *view = (GLKView *)self.view;
    view.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
 
    // Configure renderbuffers created by the view
    view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
    view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
    view.drawableStencilFormat = GLKViewDrawableStencilFormat8;
 
    // Enable multisampling
    view.drawableMultisample = GLKViewDrawableMultisample4X;
}

可以使用drawableMultisample属性为GLKView启用多重采样。多重采样是一种抗锯齿形式,可以平滑锯齿边缘,以占用更多内存和和片段处理为代价,提高大多数3Dapp的图像质量,因此,启用多重采样,需要始终测试应用性能是可接受的

使用GLKit绘制

绘制OpenGL ES内容的3个步骤:

  • 准备OpenGL ES基础结构
  • 发出绘制命令
  • 将渲染的内容呈现给CoreAnimation进行显示
    GLKView实现了第一和第三步
//第二步 绘制示例
- (void)drawRect:(CGRect)rect
{
    // Clear the framebuffer
    glClearColor(0.0f, 0.0f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
    // Draw using previously configured texture, shader, uniforms, and 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);
}

glClear函数向OpenGL ES提示可以丢弃任何现有帧缓冲区内容,避免将先前内容加载到内存中的昂贵内存操作。因此,为了确保最佳性能,应始终在绘制之前调用此函数。

GLKitView类能为OpenGL ES绘图提供一个简单的接口,因为其管理渲染进程的标准部分:

  • 在调用绘图方法之前:
    • 将其EAGLContext设置为当前上下文
    • 根据当前size、scale factor和drawable属性创建framebuffer对象和renderbuffer
    • framebuffer对象绑定为当前绘图命令的目标
    • 设置OpenGL ES的Viewport来匹配framebuffer size
  • 在绘制方法返回后:
    • 解析多重采样缓冲区(如果启用了多重采样)
    • 丢弃不再需要内容的renderbuffers
    • 将renderbuffer内容呈现给Core Animation进行缓存和显示

使用Delegate对象渲染

许多OpenGL ES app在自定义类中实现渲染代码。这种方法的优点是,通过为每个渲染算法定义一个不同的渲染器类,可以轻松支持多种渲染算法。共享通用功能的渲染算法可以从super class继承它。例如,您可能使用不同的渲染器类来支持OpenGL ES 2.0和3.0。或者,您可以使用它们来自定义渲染,以在具有更强大硬件的设备上获得更好的图像质量。

GLKit非常适合上面所说的方法,可以使你的渲染对象称为GLKView实例的代理。render class继承GLKViewDelegate协议并且实现glkView:drawRect:方法,而不是子类化GLKView实现drawRect:方法。

//在app启动时,基于硬件功能选择渲染器类:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Create a context so we can test for features
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:context];
 
    // Choose a rendering class based on device features
    GLint maxTextureSize;
    glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);
    if (maxTextureSize > 2048)
        self.renderer = [[MyBigTextureRenderer alloc] initWithContext:context];
    else
        self.renderer = [[MyRenderer alloc] initWithContext:context];
 
    // Make the renderer the delegate for the view loaded from the main storyboard
    GLKView *view = (GLKView *)self.window.rootViewController.view;
    view.delegate = self.renderer;
 
    // Give the OpenGL ES context to the view so it can draw
    view.context = context;
    return YES;
}

GLKit View Controller使OpenGL ES动画内容

GLKit提供了一个视图控制器,该类维护其管理的GLKView对象的一个动画循环。这个循环遵循在游戏和模拟中常见的设计模式,分为两个阶段:更新和展示

动画循环的理解

在更新阶段,视图控制器调用自己的update方法(或者其delegate的glkViewControllerUpdate:方法)。在这个方法中,应该准备绘画下一帧。也可以使用ViewController的timing属性例如timeSinceLastUpdate来决定下一帧的状态。

在显示阶段,viewController调用view的display方法,其又会调用你的绘制方法。在绘制方法中,将OpenGL ES命令提交给GPU来渲染内容。为了获得最佳性能,应用应该在渲染新帧开始时修改OpenGL ES对象,然后提交绘图命令。

animation loopViewControllerframePerSecond属性指示的速率在两个阶段之间交替。可以使用preferredFramesPerSecond属性设置所需的帧速率,为了优化当前显示的硬件性能,控制器会自动选择接近首选值的最佳帧速率

使用GLKit View Controller

使用GLKViewControllerGLKView实例渲染动态OpenGL ES内容

@implementation PlanetViewController // subclass of GLKViewController
 
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // 加载PlantViewController类 以及标准的GLKView实例 及其可绘制属性
    GLKView *view = (GLKView *)self.view;
    //创建和设置context
    view.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
 
    // 设置动画循环的帧率
    self.preferredFramesPerSecond = 60;
 
    // Not shown: load shaders, textures and vertex arrays, set up projection matrix
    [self setupGL];
}
 
//动画循环的更新阶段
- (void)update
{
    _rotation += self.timeSinceLastUpdate * M_PI_2; // one quarter rotation per second
 
    // 计算旋转的行星所需要变换矩阵
    GLKMatrix4 modelViewMatrix = GLKMatrix4MakeRotation(_rotation, 0.0f, 1.0f, 0.0f);
    _normalMatrix = GLKMatrix3InvertAndTranspose(GLKMatrix4GetMatrix3(modelViewMatrix), NULL);
    _modelViewProjectionMatrix = GLKMatrix4Multiply(_projectionMatrix, modelViewMatrix);
}
 
 //视图控制器 自动称为其View的delegate  实现动画循环的显示阶段
 //将计算出来的矩阵提供给着色器程序 并提交绘制
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    // Clear the framebuffer
    glClearColor(0.0f, 0.0f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
    // Set shader uniforms to values calculated in -update
    glUseProgram(_diffuseShading);
    glUniformMatrix4fv(_uniformModelViewProjectionMatrix, 1, 0, _modelViewProjectionMatrix.m);
    glUniformMatrix3fv(_uniformNormalMatrix, 1, 0, _normalMatrix.m);
 
    // Draw using previously configured texture and vertex array
    glBindTexture(GL_TEXTURE_2D, _planetTexture);
    glBindVertexArrayOES(_planetMesh);
    glDrawElements(GL_TRIANGLE_STRIP, 256, GL_UNSIGNED_SHORT, 0);
}
 
@end

使用GLKit开发渲染器

除了view和viewController结构,GLKit 框架还提供了一些其他特性来简化OpenGL ES的开发

处理向量和数学矩阵

OpenGL ES 2.0及更高版本不提供用于创建或指定转换矩阵的内置函数。而是,可编程着色器提供顶点转换,您可以使用通用的uniform variables指定着色器输入。 GLKit框架包括一个全面的矢量,矩阵类型和函数库,针对iOS硬件上的高性能进行了优化。参考GLKit Framework Reference.

从OpenGL ES 1.1固定功能管道迁移

加载纹理数据

GLKTextureLoader类提供了一种简单的方法,可以将纹理数据从iOS支持的任何图像格式同步或异步加载到OpenGL ES上下文中。 (请参阅使用GLKit框架加载纹理数据