OpenGL ES 编程指南(下)

931 阅读19分钟

在上一篇 OpenGL ES 编程指南(上)我们了解了OpenGL ES 的基本概念和 配置 OpenGL ES 上下文 的相关内容,今天继续完成 OpenGL ES编程指南的接下来的内容。

使用OpenGL ES和GLKit绘图

GLKit框架提供了视图和视图控制器类,消除了绘制OpenGL ES内容和动画所需的设置和维护代码。GLKView类管理OpenGL ES基础设施,为您的绘图代码提供位置,GLKViewController类提供了一个渲染循环,用于在GLKit视图中流畅地动画OpenGL ES内容。这些类扩展了用于绘制视图内容和管理视图呈现的标准UIKit设计模式。因此,您可以主要专注于OpenGL ES渲染代码,并快速启动和运行应用程序。GLKit框架还提供了其他功能,以简化OpenGL ES 2.0和3.0的开发。

GLKit视图按需绘制OpenGL ES内容

GLKView类提供了基于OpenGL ES的标准UIView绘图周期的等价物。UIView实例自动配置其图形上下文,以便您的drawRect:实现只需要执行Quartz 2D绘图命令,GLKView实例会自动配置自己,以便您的绘图方法只需要执行OpenGL ES绘图命令。GLKView类通过维护一个帧缓冲对象来提供此功能,该对象保存OpenGL ES绘图命令的结果,然后在绘图方法返回后自动将其呈现给Core Animation。

与标准UIKit视图一样,GLKit视图按需呈现其内容。当您的视图首次显示时,它会调用您的绘图方法——Core Animation缓存渲染的输出,并在显示视图时显示它。当您想更改视图的内容时,请调用其setNeedsDisplay方法,该视图再次调用您的绘图方法,缓存生成的图像并将其显示在屏幕上。当用于渲染图像的数据不频繁或仅根据用户操作更改时,这种方法非常有用。通过仅在需要时渲染新视图内容,您可以节省设备的电池电量,并为设备留出更多时间执行其他操作。

图3-1使用GLKit视图渲染OpenGL ES内容

三阶段渲染过程:GLKView准备OpenGL ES基础设施;您的代码绘制到帧缓冲区,GLKView显示渲染的图像以供显示

创建和配置GLKit视图

您可以以编程方式或使用Interface Builder创建和配置GLKView对象。在使用它进行绘图之前,您必须将其与EAGLContext对象相关联。

  • 以编程方式创建视图时,首先创建一个上下文,然后将其传递给视图的initWithFrame:context:方法。
  • 从故事板加载视图后,创建一个上下文,并将其设置为视图context属性的值。

GLKit视图会自动创建和配置自己的OpenGL ES帧缓冲对象和渲染缓冲区。您可以使用视图的可绘制属性控制这些对象的属性,如清单3-1所示。如果您更改GLKit视图的大小、缩放因子或可绘制属性,它会自动删除并重新创建适当的帧缓冲对象和渲染缓冲区,下次绘制其内容时。

清单3-1配置GLKit视图

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // 创建一个OpenGL ES上下文,并将其分配给从故事板加载的视图
    GLKView *view = (GLKView *)self.view;
    view.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
 
    // 配置视图创建的渲染缓冲区
    view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
    view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
    view.drawableStencilFormat = GLKViewDrawableStencilFormat8;
 
    // 启用多采样
    view.drawableMultisample = GLKViewDrawableMultisample4X;
}

您可以使用GLKView实例的drawableMultisample属性为多采样。多采样是一种抗锯齿形式,可以平滑锯齿状边缘,以使用更多内存和片段处理时间为代价来改善大多数3D应用程序的图像质量——如果您启用多采样,请始终测试应用程序的性能,以确保它仍然可以接受。

使用GLKit视图绘图

图3-1概述了绘制OpenGL ES内容的三个步骤:准备OpenGL ES基础设施,发出绘图命令,以及将渲染的内容呈现给Core Animation以供显示。GLKView类实现了第一步和第3步。在第二步中,您将实现一种绘图方法,如清单3-2中的示例。

列出3-2 GLKit视图的示例绘图方法

- (void)drawRect:(CGRect)rect
{
    // 清除帧缓冲区
    glClearColor(0.0f, 0.0f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
    // 使用先前配置的纹理,着⾊器和顶点数组绘制
    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提示,任何现有的帧缓冲区内容都可以丢弃,从而避免了将之前的内容加载到内存中的昂贵内存操作。为了确保最佳性能,您应该始终在绘图前调用此函数。

GLKView类能够为OpenGL ES绘图提供一个简单的界面,因为它管理OpenGL ES渲染过程的标准部分:

  • 在调用绘图方法之前,视图:

    • 使其EAGLContext对象成为当前上下文
    • 根据当前大小、比例因子和可绘制属性(如果需要)创建帧缓冲区对象和渲染缓冲区
    • 将帧缓冲区对象绑定为绘制命令的当前目标
    • 将OpenGL ES视口设置为匹配帧缓冲区大小
  • 绘图方法返回后,视图:

    • 解决多采样缓冲区(如果启用了多采样)
    • 丢弃不再需要内容的渲染缓冲区
    • 向 Core Animation 呈现渲染缓冲区内容,以进行缓存和显示

使用委托对象进行渲染

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

GLKit非常适合这种方法——您可以将渲染器对象作为标准GLKView实例的委托。您的渲染器类不是子类GLKView和实现drawRect:方法,而是采用GLKViewDelegate协议并实现glkView:drawInRect:方法。清单3-3演示了在应用程序启动时根据硬件功能选择渲染器类。

清单3-3根据硬件功能选择渲染器类

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // 创建一个上下文,以便我们可以测试功能
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:context];
 
    // 根据设备功能选择渲染类
    GLint maxTextureSize;
    glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);
    if (maxTextureSize > 2048)
        self.renderer = [[MyBigTextureRenderer alloc] initWithContext:context];
    else
        self.renderer = [[MyRenderer alloc] initWithContext:context];
 
    // 使渲染器成为从主故事板加载的视图的委托
    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对象维护动画循环。这个循环遵循游戏和模拟中常见的设计模式,分为两个阶段:更新显示。图3-2显示了动画循环的简化示例。

图3-2动画循环

两阶段动画循环的带示例代码:更新增量角度变量,并用它来计算旋转矩阵,显示使用矩阵绘制内容的绘图命令

了解动画循环

对于更新阶段,视图控制器调用自己的update方法(或其委托的glkViewControllerUpdate:方法)。在这个方法中,您应该为绘制下一帧做好准备。例如,游戏可能会使用此方法根据自上一帧以来收到的输入事件来确定玩家和敌方角色的位置,而科学可视化可能会使用此方法运行其模拟步骤。如果您需要计时信息来确定应用程序下一帧的状态,请使用视图控制器的计时属性之一,例如timeSinceLastUpdate属性。在图3-2中,更新相位会增加一个angle变量,并用它来计算转换矩阵。

对于显示阶段,视图控制器调用其视图的display方法,进而调用您的绘图方法。在绘图方法中,您将OpenGL ES绘图命令提交到GPU以呈现您的内容。为了获得最佳性能,您的应用程序应在渲染新帧开始时修改OpenGL ES对象,然后提交绘图命令。在图3-2中,显示阶段在着色器程序中将统一变量设置为更新阶段计算的矩阵,然后提交绘图命令来呈现新内容。

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

重要信息: 为了获得最佳效果,请选择您的应用程序可以持续实现的帧速率。平稳、一致的帧速率比不规则变化的帧速率产生更愉快的用户体验。

使用GLKit视图控制器

清单3-4演示了使用GLKViewController子类和GLKView实例渲染动画OpenGL ES内容的典型策略。

清单3-4使用GLKit视图和视图控制器绘制OpenGL ES内容并为其制作动画

@implementation PlanetViewController // subclass of GLKViewController
 
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // 创建一个OpenGL ES上下文,并将其分配给从故事板加载的视图
    GLKView *view = (GLKView *)self.view;
    view.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
 
    // 设置动画帧速率
    self.preferredFramesPerSecond = 60;
 
    // 未显示:加载着色器、纹理和顶点数组,设置投影矩阵
    [self setupGL];
}
 
- (void)update
{
    _rotation += self.timeSinceLastUpdate * M_PI_2; // 每秒四分之一轮换
 
    // 为旋转行星设置变换矩阵
    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
{
    // 清除帧缓冲区
    glClearColor(0.0f, 0.0f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
    // 将着色器制服设置为在-update中计算的值
    glUseProgram(_diffuseShading);
    glUniformMatrix4fv(_uniformModelViewProjectionMatrix, 1, 0, _modelViewProjectionMatrix.m);
    glUniformMatrix3fv(_uniformNormalMatrix, 1, 0, _normalMatrix.m);
 
    // 使用之前配置的纹理和顶点数组绘制
    glBindTexture(GL_TEXTURE_2D, _planetTexture);
    glBindVertexArrayOES(_planetMesh);
    glDrawElements(GL_TRIANGLE_STRIP, 256, GL_UNSIGNED_SHORT, 0);
}
 
@end

在本例中,从故事板加载PlanetViewController类(自定义GLKViewController子类)的实例,以及标准GLKView实例及其可绘制属性。viewDidLoad方法创建一个OpenGL ES上下文并将其提供给视图,并设置动画循环的帧速率。

视图控制器自动是其视图的委托,因此它实现了动画循环的更新和显示阶段。在update方法中,它计算了显示旋转行星所需的变换矩阵。在glkView:drawInRect:方法中,它将这些矩阵提供给着色器程序,并提交绘图命令来渲染行星几何形状。

使用GLKit开发您的渲染器

除了查看和查看控制器基础设施外,GLKit框架还提供了其他几项功能,以方便在iOS上开发OpenGL ES。

处理矢量和矩阵数学

OpenGL ES 2.0及更高版本不提供用于创建或指定转换矩阵的内置功能。相反,可编程着色器提供顶点转换,并使用通用均匀变量指定着色器输入。GLKit 框架包含一个全面的矢量和矩阵类型和功能库,针对 iOS 硬件的高性能进行了优化。(请参阅*GLKit框架参考*。)

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

OpenGL ES 2.0及更高版本删除了与OpenGL ES 1.1固定功能图形管道相关的所有功能。GLKBaseEffect类为OpenGL ES 1.1管道的转换、照明和着色阶段提供了Objective-C模拟,GLKSkyboxEffectGLKReflectionMapEffect类增加了对常见视觉效果的支持。有关详细信息,请参阅这些课程的参考文档。

加载纹理数据

GLKTextureLoader类提供了一种简单的方法,可以同步或异步地将iOS支持的任何图像格式的纹理数据加载到OpenGL ES上下文中。


绘制到其他渲染目的地

Framebuffer对象是渲染命令的目的地。当您创建帧缓冲对象时,您可以精确控制其对颜色、深度和模板数据的存储。您通过将图像附加到帧缓冲区来提供此存储,如图4-1所示。最常见的图像附件是渲染缓冲对象。您还可以将OpenGL ES纹理附加到帧缓冲区的颜色连接点,这意味着任何绘图命令都会呈现到纹理中。稍后,纹理可以作为未来渲染命令的输入。您还可以在单个渲染上下文中创建多个帧缓冲对象。您可以这样做,以便在多个帧缓冲区之间共享相同的渲染管道和OpenGL ES资源。

图4-1带颜色和深度渲染缓冲区的框架缓冲区

带附件的帧缓冲区。

所有这些方法都需要手动创建帧缓冲区和渲染缓冲区对象来存储OpenGL ES上下文的渲染结果,以及编写额外的代码将其内容呈现到屏幕上,并在需要时运行动画循环。

创建帧缓冲对象

根据您的应用程序打算执行的任务,您的应用程序会配置不同的对象以附加到帧缓冲区对象。在大多数情况下,配置帧缓冲区的区别在于将什么对象附加到帧缓冲区对象的颜色附加点:

创建屏幕外帧缓冲区对象

用于屏幕外渲染的帧缓冲区将其所有附件分配为OpenGL ES渲染缓冲区。以下代码分配一个带有颜色和深度附件的帧缓冲对象。

  1. 创建框架缓冲区并将其绑定。
GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
  1. 创建一个颜色渲染缓冲区,为其分配存储空间,并将其附加到框架缓冲区的颜色连接点。
GLuint colorRenderbuffer;
glGenRenderbuffers(1, &colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRenderbuffer);
  1. 创建一个深度或深度/模板渲染缓冲区,为其分配存储空间,并将其附加到帧缓冲区的深度连接点。
GLuint depthRenderbuffer;
glGenRenderbuffers(1, &depthRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer);
  1. 测试帧缓冲区的完整性。只有当帧缓冲区的配置发生变化时,才需要执行此测试。
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER) ;
if(status != GL_FRAMEBUFFER_COMPLETE) {
    NSLog(@"failed to make complete framebuffer object %x", status);
}

绘制到屏幕外渲染缓冲区后,您可以使用glReadPixels函数将其内容返回CPU进行进一步处理。

使用帧缓冲对象渲染到纹理

创建此帧缓冲区的代码几乎与屏幕外示例相同,但现在分配了一个纹理并将其附加到颜色附件点。

  1. 创建帧缓冲对象(使用与 创建屏幕外帧缓冲对象 相同的过程)。
  2. 创建目标纹理,并将其附加到帧缓冲区的颜色连接点。
// 创建纹理
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8,  width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
  1. 分配并附加深度缓冲区(和以前一样)。
  2. 测试帧缓冲区的完整性(和以前一样)。

虽然此示例假设您正在渲染到颜色纹理,但其他选项是可能的。例如,使用OES_depth_texture扩展,您可以将纹理附加到深度附件点,以将场景的深度信息存储到纹理中。您可以使用此深度信息来计算最终渲染场景中的阴影。

渲染到核心动画层

Core Animation 是 iOS 上图形渲染和动画的核心基础设施。您可以使用承载使用不同 iOS 子系统(例如 UIKit、Quartz 2D 和 OpenGL ES)呈现的内容的层来组成应用程序的用户界面或其他视觉显示。OpenGL ES 通过CAEAGLLayer类连接到 Core Animation,这是一种特殊类型的 Core Animation 层,其内容来自 OpenGL ES 渲染缓冲区。Core Animation 将渲染缓冲区的内容与其他层合成并在屏幕上显示生成的图像。

图4-2 Core Animation与OpenGL ES共享渲染缓冲区

基于核心动画的渲染缓冲区

CAEAGLLayer通过提供两个关键功能为OpenGL ES提供支持。首先,它为渲染缓冲区分配共享存储空间。其次,它向Core Animation显示渲染缓冲区,用渲染缓冲区的数据替换图层之前的内容。该模型的一个优点是,只有当渲染的图像发生变化时,才需要在每个帧中绘制核心动画层的内容。

注意: GLKView类会自动执行以下步骤,因此当您想在视图的内容层中使用OpenGL ES绘图时,您应该使用它。

要使用核心动画层进行OpenGL ES渲染:

  1. 创建一个CAEAGLLayer对象并配置其属性

    为了获得最佳性能,请将图层opaque属性的值设置为YES。请参阅“了解核心动画合成性能”。

    或者,通过为CAEAGLLayer对象的drawableProperties属性分配新的值字典来配置渲染表面的表面属性。您可以指定渲染缓冲区的像素格式,并指定渲染缓冲区的内容在发送到Core Animation后是否被丢弃。有关允许密钥的列表,请参阅*EAGLDrawable协议参考*。

  2. 分配OpenGL ES上下文,使其成为当前上下文。请参阅配置OpenGL ES上下文

  3. 创建帧缓冲对象(如上面的创建屏幕外帧缓冲对象)。

  4. 创建一个颜色渲染缓冲区,通过调用上下文的renderbufferStorage:fromDrawable:方法分配其存储空间,并将图层对象作为参数传递。宽度、高度和像素格式取自图层,用于为渲染缓冲区分配存储空间。

GLuint colorRenderbuffer;
glGenRenderbuffers(1, &colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
[myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:myEAGLLayer];
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRenderbuffer);

注意: 当Core Animation层的边界或属性发生变化时,您的应用程序应该重新分配渲染缓冲区的存储空间。如果您不重新分配渲染缓冲区,渲染缓冲区大小将与图层的大小不匹配;在这种情况下,Core Animation可能会缩放图像的内容以适合图层。

  1. 检索颜色渲染缓冲区的高度和宽度。
GLint width;
GLint height;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);

在早期的例子中,显式提供了渲染缓冲区的宽度和高度,以分配缓冲区的存储空间。在这里,代码在分配存储后从颜色渲染缓冲区检索宽度和高度。您的应用程序这样做是因为颜色渲染缓冲区的实际尺寸是根据图层的边界和缩放因子计算的。附加到框架缓冲区的其他渲染缓冲区必须具有相同的尺寸。除了使用高度和宽度来分配深度缓冲区外,还使用它们来分配OpenGL ES视口,并帮助确定应用程序纹理和模型所需的细节水平。请参阅支持高分辨率显示器

  1. 分配并附加深度缓冲区(和以前一样)。

  2. 测试帧缓冲区的完整性(和以前一样)。

  3. 通过将CAEAGLLayer对象传递给可见层的addSublayer:方法将其添加到核心动画层层次结构中。

绘制帧缓冲区对象

现在您有一个帧缓冲区对象,您需要填充它。本节介绍渲染新帧并将其呈现给用户所需的步骤。渲染到纹理或屏幕外帧缓冲区的作用类似,仅在应用程序使用最终帧的方式上有所不同。

按需渲染或使用动画循环

在渲染到 Core Animation 图层时,您必须选择何时绘制 OpenGL ES 内容,就像使用 GLKit 视图和视图控制器绘图时一样。如果渲染到屏幕外帧缓冲区或纹理,请根据使用这些类型的帧缓冲区的情况进行绘图。

对于按需绘图,请实现您自己的方法来绘制和呈现渲染缓冲区,并在您想要显示新内容时调用它。

要使用动画循环绘图,请使用CADisplayLink对象。显示链接是 Core Animation 提供的一种计时器,可让您将绘图同步到屏幕的刷新率。清单4-1 展示了如何检索显示视图的屏幕,使用该屏幕创建新的显示链接对象,并将显示链接对象添加到运行循环中。

注意: GLKViewController类自动使用CADisplayLink对象为GLKView内容制作动画。仅当您需要超出GLKit框架范围的行为时,才直接使用CADisplayLink类。

清单4-1创建和启动显示链接

displayLink = [myView.window.screen displayLinkWithTarget:self selector:@selector(drawFrame)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

drawFrame方法的实现中,读取显示链接timestamp属性,以获取下一个渲染帧的时间戳。它可以使用该值来计算对象在下一帧中的位置。

通常,每次屏幕刷新时都会触发显示链接对象;该值通常为60 Hz,但在不同的设备上可能会有所不同。大多数应用程序不需要每秒更新屏幕60次。您可以将显示链接的frameInterval属性设置为调用方法之前经过的实际帧数。例如,如果帧间隔设置为3,则每第三帧调用一次您的应用程序,或大约每秒20帧。

重要信息: 为了获得最佳效果,请选择您的应用程序可以持续实现的帧速率。平稳、一致的帧速率比不规则变化的帧速率产生更愉快的用户体验。

渲染帧

图4-3显示了OpenGL ES应用程序在iOS上渲染和呈现框架时应采取的步骤。这些步骤包括许多提高应用程序性能的提示。

图4-3 iOS OpenGL渲染步骤

清除缓冲区

在每个帧的开头,抹掉所有帧缓冲区附件的内容,这些附件不需要以前帧的内容来绘制下一帧。调用glClear函数,传递一个带有所有缓冲区的位掩码,如清单4-2所示。

列出4-2个透明框架缓冲器附件

glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

使用glClear“提示”OpenGL ES,可以丢弃渲染缓冲区或纹理的现有内容,从而避免将之前的内容加载到内存中的昂贵操作。

准备资源并执行绘图命令

这两个步骤包含您在设计应用程序架构时做出的大部分关键决策。首先,您决定要向用户显示什么,并配置相应的OpenGL ES对象,如顶点缓冲对象、纹理、着色器程序及其输入变量,以便上传到GPU。接下来,您将提交绘图逗号,告诉GPU如何使用这些资源渲染帧。

OpenGL ES设计指南更详细地介绍了渲染器设计。目前,最重要的性能优化是,如果您的应用程序仅在渲染新帧开始时修改OpenGL ES对象,则运行速度会更快。虽然您的应用程序可以在修改对象和提交绘图命令之间交替使用(如图4-3中的虚线所示),但如果每帧只执行每个步骤一次,它运行速度会更快。

执行绘图命令

此步骤将您上一步准备的对象提交绘图命令以使用它们。OpenGL ES设计指南详细介绍了设计渲染代码的这一部分以高效运行。目前,需要注意的最重要的性能优化是,如果您的应用程序仅在渲染新帧时修改OpenGL ES对象,则运行速度会更快。虽然您的应用程序可以在修改对象和提交绘图命令之间交替使用(如虚线所示),但如果只执行每个步骤一次,它运行速度会更快。

解决多采样问题

如果您的应用程序使用多采样来提高图像质量,则您的应用程序必须在向用户显示像素之前解析像素。使用多采样提高图像质量详细介绍了多采样

丢弃不必要的渲染缓冲器

丢弃操作是一种性能提示,告诉OpenGL ES不再需要一个或多个渲染缓冲区的内容。通过向OpenGL ES暗示您不需要渲染缓冲区的内容,可以丢弃缓冲区中的数据,并避免更新这些缓冲区内容的昂贵任务。

在渲染循环的这个阶段,您的应用程序已经提交了框架的所有绘图命令。虽然您的应用程序需要颜色渲染缓冲区显示到屏幕上,但它可能不需要深度缓冲区的内容。清单4-3丢弃了深度缓冲区的内容。

清单4-3丢弃深度帧缓冲器

const GLenum discards[]  = {GL_DEPTH_ATTACHMENT};
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glDiscardFramebufferEXT(GL_FRAMEBUFFER,1,discards);

注意: glDiscardFramebufferEXT功能由OpenGL ES 1.1和2.0的EXT_discard_framebuffer扩展提供。在OpenGL ES 3.0上下文中,请使用glInvalidateFramebuffer函数。

向 Core Animation 展示结果

在此步骤中,颜色渲染缓冲区将保留完成的帧,因此您只需将其呈现给用户。清单4-4将渲染缓冲区绑定到上下文并呈现它。这会导致完成的帧交给Core Animation。

清单4-4呈现成品框架

glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER];

默认情况下,您必须假设渲染缓冲区的内容在应用程序显示渲染缓冲区后被丢弃。这意味着,每次您的应用程序显示帧时,它都必须在渲染新帧时完全重新创建帧的内容。因此,上面的代码总是会擦除颜色缓冲区。

如果您的应用程序希望在帧之间保留颜色渲染缓冲区的内容,请将kEAGLDrawablePropertyRetainedBacking键添加到存储在CAEAGLLayer对象的drawableProperties属性中的字典中,并从早期的glClear函数调用中删除GL_COLOR_BUFFER_BIT常量。保留备份可能需要iOS分配额外的内存来保存缓冲区的内容,这可能会降低应用程序的性能。

使用多采样来提高图像质量

多采样是一种抗锯齿形式,可以平滑锯齿状边缘,并提高大多数3D应用程序的图像质量。OpenGL ES 3.0包括多采样作为核心规范的一部分,iOS通过APPLE_framebuffer_multisample扩展在OpenGL ES 1.1和2.0中提供。多采样使用更多的内存和片段处理时间来渲染图像,但它可能会以低于其他方法的性能成本提高图像质量。

图4-4显示了多采样的工作原理。您的应用程序不会创建一个帧缓冲区对象,而是创建两个。多采样缓冲区包含渲染内容所需的所有附件(通常是颜色和深度缓冲区)。解析缓冲区仅包含使用创建帧缓冲区对象中的适当过程向用户显示渲染图像所需的附件(通常是颜色渲染缓冲区,但可能是纹理)。多样本渲染缓冲区使用与解析帧缓冲区相同的维度分配,但每个参数都包含一个额外的参数,该参数指定要存储每个像素的样本数量。您的应用程序执行对多采样缓冲区的所有渲染,然后通过将这些样本解析到解析缓冲区来生成最终的抗锯齿图像。

图4-4多采样的工作原理

清单4-5显示了创建多采样缓冲区的代码。此代码使用之前创建的缓冲区的宽度和高度。它调用glRenderbufferStorageMultisampleAPPLE函数,为渲染缓冲区创建多采样存储。

清单4-5创建多样本缓冲区

glGenFramebuffers(1, &sampleFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, sampleFramebuffer);
 
glGenRenderbuffers(1, &sampleColorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, sampleColorRenderbuffer);
glRenderbufferStorageMultisampleAPPLE(GL_RENDERBUFFER, 4, GL_RGBA8_OES, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, sampleColorRenderbuffer);
 
glGenRenderbuffers(1, &sampleDepthRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, sampleDepthRenderbuffer);
glRenderbufferStorageMultisampleAPPLE(GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT16, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, sampleDepthRenderbuffer);
 
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    NSLog(@"Failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));

以下是修改渲染代码以支持多采样的步骤:

  1. 在清除缓冲区步骤中,您可以清除多采样帧缓冲区的内容。

    glBindFramebuffer(GL_FRAMEBUFFER, sampleFramebuffer);
    glViewport(0, 0, framebufferWidth, framebufferHeight);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
  2. 提交绘图命令后,您将内容从多采样缓冲区解析为解析缓冲区。为每个像素存储的样本在解析缓冲区中合并为单个样本。

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER_APPLE, resolveFrameBuffer);
    glBindFramebuffer(GL_READ_FRAMEBUFFER_APPLE, sampleFramebuffer);
    glResolveMultisampleFramebufferAPPLE();
    
  3. 在丢弃步骤中,您可以丢弃附加到多样本帧缓冲区的两个渲染缓冲区。这是因为您计划呈现的内容存储在解析帧缓冲区中。

    const GLenum discards[]  = {GL_COLOR_ATTACHMENT0,GL_DEPTH_ATTACHMENT};
    glDiscardFramebufferEXT(GL_READ_FRAMEBUFFER_APPLE,2,discards);
    
  4. 在“当前结果”步骤中,您将显示附加到解析帧缓冲区的颜色渲染缓冲区。

    glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
    [context presentRenderbuffer:GL_RENDERBUFFER];
    

多采样不是免费的;存储其他样本需要额外的内存,将样本解析到解析帧缓冲区需要时间。如果您向应用程序添加多采样,请务必测试应用程序的性能,以确保它仍然可以接受。


多任务处理、高分辨率和其他 iOS 功能

使用 OpenGL ES 的许多方面与平台无关,但在 iOS 上使用 OpenGL ES 的一些细节需要特别考虑。特别是,使用 OpenGL ES 的 iOS 应用程序必须正确处理多任务处理,否则在移动到后台时可能会被终止。在为 iOS 设备开发 OpenGL ES 内容时,您还应该考虑显示分辨率和其他设备功能。

实现多任务感知 OpenGL ES 应用程序

当用户切换到另一个应用程序时,您的应用程序可以继续运行。

OpenGL ES 应用程序在移动到后台时必须执行额外的工作。如果应用程序不正确地处理这些任务,它可能会被 iOS 终止。此外,应用程序可能希望释放 OpenGL ES 资源,以便这些资源可供前台应用程序使用。

后台应用程序可能无法在图形硬件上执行命令

如果 OpenGL ES 应用程序试图在图形硬件上执行 OpenGL ES 命令,它就会被终止。iOS 会阻止后台应用程序访问图形处理器,以便最前面的应用程序始终能够为用户提供出色的体验。您的应用不仅可以在后台调用 OpenGL ES 时终止,还可以在之前提交的命令在后台刷新到 GPU 时终止。您的应用程序必须确保所有先前提交的命令都已完成执行,然后才能进入后台。

如果您使用 GLKit 视图和视图控制器,并且仅在您的绘图方法期间提交 OpenGL ES 命令,则您的应用程序在移动到后台时会自动正确运行。默认情况下,当您的应用程序处于非活动状态时,GLKViewController该类会暂停其动画计时器,以确保不会调用您的绘图方法。

如果您不使用 GLKit 视图或视图控制器,或者您在GLKView绘图方法之外提交 OpenGL ES 命令,则必须采取以下步骤来确保您的应用程序不会在后台终止:

  1. 在您的应用委托的applicationWillResignActive:方法中,您的应用应停止其动画计时器(如果有),将自身置于已知的良好状态,然后调用该glFinish函数。
  2. 在您的应用程序委托的applicationDidEnterBackground:方法中,您的应用程序可能希望删除它的一些 OpenGL ES 对象,以便为前台应用程序提供内存和资源。调用该glFinish函数以确保立即删除资源。
  3. 在您的应用退出其applicationDidEnterBackground:方法后,它不得进行任何新的 OpenGL ES 调用。如果它进行 OpenGL ES 调用,它会被 iOS 终止。
  4. 在您应用的applicationWillEnterForeground:方法中,重新创建任何对象并重新启动动画计时器。

总而言之,您的应用程序需要调用该glFinish函数以确保所有先前提交的命令都从命令缓冲区中排出并由 OpenGL ES 执行。在它移入后台后,您必须避免使用 OpenGL ES,直到它移回前台。

在移动到后台之前轻松删除重新创建的资源

当您的应用移入后台时,永远不需要释放 OpenGL ES 对象。通常,您的应用应避免处置其内容。考虑两种情况:

  • 用户正在玩您的游戏并短暂退出以查看他们的日历。当玩家回到你的游戏时,游戏的资源还在内存中,游戏可以立即恢复。
  • 当用户启动另一个 OpenGL ES 应用程序时,您的 OpenGL ES 应用程序处于后台。如果该应用程序需要的内存比设备上的可用内存多,系统会静默自动终止您的应用程序,而不需要它执行任何额外的工作。

您的目标应该是将您的应用程序设计为一个好公民:这意味着尽可能短地移动到前台所需的时间,同时还要减少其在后台时的内存占用。

以下是您应该如何处理这两种情况:

  • 你的应用应该在内存中保存纹理、模型和其他资源;当您的应用程序进入后台时,永远不应丢弃需要很长时间才能重新创建的资源。
  • 您的应用程序应该处理可以快速轻松地重新创建的对象。寻找消耗大量内存的对象。

简单目标是您的应用程序分配用于保存渲染结果的帧缓冲区。当您的应用程序在后台时,它对用户不可见,并且可能不会使用 OpenGL ES 呈现任何新内容。这意味着您的应用程序的帧缓冲区消耗的内存已分配,但没有用处。此外,帧缓冲区的内容是暂时的;大多数应用程序在每次渲染新帧时都会重新创建帧缓冲区的内容。这使得渲染缓冲区成为可以轻松重新创建的内存密集型资源,成为移动到后台时可以处理的对象的良好候选者。

如果您使用 GLKit 视图和视图控制器,GLKViewController当您的应用程序移入后台时,该类会自动处理其关联视图的帧缓冲区。如果您为其他用途手动创建帧缓冲区,则应在您的应用程序移至后台时处理它们。无论哪种情况,您还应该考虑您的应用程序当时可以处理哪些其他临时资源。

支持高分辨率显示器

默认情况下,GLKit 视图的contentScaleFactor属性值与包含它的屏幕的比例相匹配,因此其关联的帧缓冲区被配置为以显示器的全分辨率呈现。

如果您使用核心动画层呈现 OpenGL ES 内容,则其比例因子1.0默认设置为。要以 Retina 显示器的全分辨率绘图,您应该更改CAEAGLLayer对象的比例因子以匹配屏幕的比例因子。

当支持具有高分辨率显示器的设备时,您应该相应地调整应用程序的模型和纹理资源。在高分辨率设备上运行时,您可能希望选择更详细的模型和纹理来渲染更好的图像。相反,在标准分辨率设备上,您可以使用更小的模型和纹理。

重要提示: 许多 OpenGL ES API 调用以屏幕像素表示尺寸。如果使用大于 的比例因子1.0,则应在使用glScissorglBlitFramebufferglLineWidthglPointSize函数或gl_PointSize着色器变量时相应地调整尺寸。

决定如何支持高分辨率显示器的一个重要因素是性能。Retina 显示屏上的比例因子翻倍,像素数量翻了两番,导致 GPU 处理的片段数量增加了四倍。如果您的应用程序对每个片段执行许多计算,则像素的增加可能会降低帧速率。如果您发现您的应用程序在更高的比例因子下运行速度明显变慢,请考虑以下选项之一:

  • 使用本文档中的性能调整指南优化片段着色器的性能。

  • 在片段着色器中实现更简单的算法。通过这样做,您正在降低单个像素的质量,从而以更高的分辨率渲染整个图像。

  • 使用介于 1.0 和 和屏幕的比例因子之间的小数比例因子。比例因子 1.5 比比例因子 1.0 提供更好的质量,但需要填充的像素比缩放到 2.0 的图像要少。

  • GLKView为您的对象drawableColorFormatdrawableDepthFormat属性使用较低精度的格式。通过这样做,您可以减少对底层渲染缓冲区进行操作所需的内存带宽。

  • 使用较低的比例因子并启用多重采样。另一个优点是多重采样还可以在不支持高分辨率显示的设备上提供更高的质量。

    要为GLKView对象启用多重采样,请更改其drawableMultisample属性的值。如果您不渲染到 GLKit 视图,则必须手动设置多重采样缓冲区并在呈现最终图像之前解析它们。

    多重采样不是免费的;需要额外的内存来存储额外的样本,并且将样本解析到解析帧缓冲区需要时间。如果您将多重采样添加到您的应用程序,请始终测试您的应用程序的性能以确保它仍然可以接受。

支持多种接口方向

与任何应用程序一样,OpenGL ES 应用程序应该支持适合其内容的用户界面方向。您可以在其信息属性列表中为您的应用声明支持的界面方向,或者为使用其supportedInterfaceOrientations方法托管您的 OpenGL ES 内容的视图控制器声明。

默认情况下,GLKViewControllerGLKView类会自动处理方向变化:当用户将设备旋转到支持的方向时,系统会为方向变化设置动画并更改视图控制器视图的大小。当它的大小发生变化时,GLKView对象会相应地调整其帧缓冲区和视口的大小。如果您需要响应此更改,请在您的子类中实现viewWillLayoutSubviewsorviewDidLayoutSubviews方法,或者如果您正在使用自定义子类,请实现该方法。GLKViewController``layoutSubviews``GLKView

如果您使用 Core Animation 层绘制 OpenGL ES 内容,您的应用程序仍应包含一个视图控制器来管理用户界面方向。

在外部显示器上呈现 OpenGL ES 内容

iOS 设备可以连接到外部显示器。外接显示器的分辨率及其内容比例因子可能与主屏幕的分辨率和比例因子不同;您呈现帧的代码应调整以匹配。

在外部显示器上绘图的过程几乎与在主屏幕上运行的过程相同。

  1. 在外部显示器上创建一个窗口。

  2. 为您的渲染策略添加适当的视图或视图控制器对象到窗口。

    • 如果使用 GLKit 进行渲染,请设置GLKViewController和(或您的自定义子类)的实例并使用其属性GLKView将它们添加到窗口中。rootViewController
    • 如果渲染到核心动画层,请将包含您的层的视图添加为窗口的子视图。要使用动画循环进行渲染,请通过检索screen窗口的属性并调用其displayLinkWithTarget:selector:方法来创建针对外部显示器优化的显示链接对象。