OpenGL ES for iOS - 6

OpenGL ES 设计指南

现在您已经掌握了在 iOS 应用程序中使用 OpenGL ES 的基础知识,请使用本章中的信息来帮助您设计应用程序的渲染引擎以获得更好的性能。本章介绍渲染器设计的关键概念;后面的章节通过特定的最佳实践和性能技术对此信息进行了扩展。

如何可视化 OpenGL ES

本节描述了可视化 OpenGL ES 设计的两个视角:作为客户端-服务器架构和作为管道。这两种观点都有助于规划和评估应用程序的架构。

OpenGL ES 作为客户端-服务器架构

图 6-1 将 OpenGL ES 可视化为客户端-服务器架构。您的应用程序将状态更改、纹理和顶点数据以及渲染命令传达给 OpenGL ES 客户端。客户端将此数据转换为图形硬件可以理解的格式,并将它们转发给 GPU。这些进程会增加应用程序的图形性能开销。


Figure 6 - 1

实现出色的性能需要仔细管理这种开销。一个设计良好的应用程序会减少它对 OpenGL ES 的调用频率,使用适合硬件的数据格式来最小化转换成本,并仔细管理自身和 OpenGL ES 之间的数据流。

OpenGL ES 作为图形管道

图 6-2 将 OpenGL ES 可视化为图形管道。您的应用程序配置图形管道,然后执行绘图命令以沿管道发送顶点数据。管道的后续阶段运行顶点着色器来处理顶点数据,将顶点组装成图元,将图元光栅化成片段,运行片段着色器来计算每个片段的颜色和深度值,并将片段混合到帧缓冲区中以进行显示。


Figure 6 - 2

使用管道作为思维模型来确定您的应用程序执行哪些工作来生成新框架。您的渲染器设计包括编写着色器程序来处理管道的顶点和片段阶段,组织您提供给这些程序的顶点和纹理数据,以及配置驱动管道固定功能阶段的 OpenGL ES 状态机。

图形管道中的各个阶段可以同时计算它们的结果——例如,您的应用程序可能会准备新的图元,而图形硬件的不同部分对先前提交的几何体执行顶点和片段计算。但是,后期阶段取决于早期阶段的输出。如果任何流水线阶段执行太多工作或执行太慢,则其他流水线阶段将闲置,直到最慢的阶段完成其工作。精心设计的应用程序会根据图形硬件功能平衡每个流水线阶段执行的工作。

  • 重要提示:当您调整应用程序的性能时,第一步通常是确定它在哪个阶段遇到瓶颈,以及原因。

OpenGL ES 版本和渲染器架构

iOS 支持三个版本的 OpenGL ES。较新的版本提供了更大的灵活性,允许您在不影响性能的情况下实现包含高质量视觉效果的渲染算法。

OpenGL ES 3.0

OpenGL ES 3.0 是 iOS 7 中的新功能。您的应用程序可以使用 OpenGL ES 3.0 中引入的功能来实现高级图形编程技术(以前只能在桌面级硬件和游戏控制台上使用),以获得更快的图形性能和引人注目的视觉效果。

下面重点介绍了 OpenGL ES 3.0 的一些关键特性。有关完整概述,请参阅 OpenGL ES API 注册表中的 OpenGL ES 3.0 规范。

OpenGL ES 着色语言版本 3.0

GLSL ES 3.0 添加了新功能,例如统一块、32 位整数和附加整数运算,用于在顶点和片段着色器程序中执行更通用的计算任务。要在着色器程序中使用新语言,您的着色器源代码必须以 #version 330 es 指令开头。 OpenGL ES 3.0 上下文与为 OpenGL ES 2.0 编写的着色器保持兼容。

有关更多详细信息,请参阅 OpenGL ES API 注册表中的采用 OpenGL ES 着色语言 3.0 版和 OpenGL ES 着色语言 3.0 规范。

多个渲染目标

通过启用多个渲染目标,您可以创建同时写入多个帧缓冲区附件的片段着色器。

此功能支持使用高级渲染算法,例如延迟着色,其中您的应用程序首先渲染到一组纹理以存储几何数据,然后执行一个或多个从这些纹理读取的着色传递并执行照明计算以输出最终图片。由于这种方法预先计算了光照计算的输入,因此向场景中添加大量光照的增量性能成本要小得多。延迟着色算法需要多个渲染目标支持,如图 6-3 所示,以实现合理的性能。否则,渲染到多个纹理需要对每个纹理进行单独的绘制过程。


Figure 6-3

除了创建帧缓冲区对象中描述的过程之外,您还可以设置多个渲染目标。您不是为帧缓冲区创建单一颜色的附件,而是创建多个附件。然后,调用 glDrawBuffers 函数来指定在渲染中使用哪些帧缓冲区附件,如清单 6-1 所示。

// Attach (previously created) textures to the framebuffer.
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _colorTexture, 0);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, _positionTexture, 0);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, _normalTexture, 0);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, _depthTexture, 0);
 
// Specify the framebuffer attachments for rendering.
GLenum targets[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2};
glDrawBuffers(3, targets);

当您的应用程序发出绘图命令时,您的片段着色器会确定为每个渲染目标中的每个像素输出什么颜色(或非颜色数据)。清单 6-2 显示了一个基本的片段着色器,它通过分配位置与清单 6-1 中设置的位置匹配的片段输出变量来渲染多个目标。

#version 300 es
 
uniform lowp sampler2D myTexture;
in mediump vec2 texCoord;
in mediump vec4 position;
in mediump vec3 normal;
 
layout(location = 0) out lowp vec4 colorData;
layout(location = 1) out mediump vec4 positionData;
layout(location = 2) out mediump vec4 normalData;
 
void main()
{
    colorData = texture(myTexture, texCoord);
    positionData = position;
    normalData = vec4(normalize(normal), 1.0);
}

多个渲染目标也可用于其他高级图形技术,例如实时反射、屏幕空间环境遮挡和体积照明。

转换反馈

图形硬件使用针对矢量处理优化的高度并行化架构。您可以通过新的变换反馈功能更好地利用此硬件,该功能可让您将顶点着色器的输出捕获到 GPU 内存中的缓冲区对象中。您可以从一个渲染通道捕获数据以用于另一个渲染通道,或者禁用部分图形管道并使用变换反馈进行通用计算。

一种受益于变换反馈的技术是动画粒子效果。渲染粒子系统的一般架构如图 6-4 所示。首先,应用程序设置粒子模拟的初始状态。然后,对于渲染的每一帧,应用程序都会运行一个模拟步骤,更新每个模拟粒子的位置、方向和速度,然后绘制代表粒子当前状态的视觉资产。


Figure 6-4

传统上,实现粒子系统的应用程序在 CPU 上运行其模拟,将模拟结果存储在顶点缓冲区中以用于渲染粒子艺术。但是,将顶点缓冲区的内容传输到 GPU 内存非常耗时。通过优化现代 GPU 硬件中可用的并行架构的能力,转换反馈可以更有效地解决问题。

借助变换反馈,您可以设计渲染引擎以更有效地解决此问题。图 6-5 显示了您的应用程序如何配置 OpenGL ES 图形管道以实现粒子系统动画的概述。由于 OpenGL ES 将每个粒子及其状态表示为一个顶点,因此 GPU 的顶点着色器阶段可以同时运行多个粒子的模拟。由于包含粒子状态数据的顶点缓冲区在帧之间重复使用,因此将数据传输到 GPU 内存的昂贵过程仅在初始化时发生一次。


Figure 6-5
  • 在初始化时,创建一个顶点缓冲区并用包含模拟中所有粒子初始状态的数据填充它。

  • 在 GLSL 顶点着色器程序中实现您的粒子模拟,并通过绘制包含粒子位置数据的顶点缓冲区的内容来每帧运行它。

    • 要在启用变换反馈的情况下进行渲染,请调用 glBeginTransformFeedback 函数。 (在恢复正常绘图之前调用 glEndTransformFeedback()。)
    • 使用 glTransformFeedbackVaryings 函数指定变换反馈应捕获哪些着色器输出,并使用 glBindBufferBase 或 glBindBufferRange 函数和 GL_TRANSFORM_FEEDBACK_BUFFER 缓冲区类型指定它们将被捕获到的缓冲区。
    • 通过调用 glEnable(GL_RASTERIZER_DISCARD) 禁用光栅化(以及管道的后续阶段)。
  • 要渲染模拟结果以供显示,请使用包含粒子位置的顶点缓冲区作为第二次绘制过程的输入,再次启用光栅化(和管道的其余部分)并使用适合渲染应用程序视觉内容的顶点和片段着色器。

  • 在下一帧上,使用上一帧模拟步骤输出的顶点缓冲区作为下一个模拟步骤的输入。

可以从变换反馈中受益的其他图形编程技术包括骨架动画(也称为蒙皮)和光线行进。

OpenGL ES 2.0

OpenGL ES 2.0 提供了具有可编程着色器的灵活图形管道,可用于所有当前的 iOS 设备。 OpenGL ES 3.0 规范中正式引入的许多功能可通过 OpenGL ES 2.0 扩展提供给 iOS 设备,因此您可以实现许多高级图形编程技术,同时与大多数设备保持兼容。

OpenGL ES 1.1

OpenGL ES 1.1 仅提供基本的固定功能图形管道。 iOS 支持 OpenGL ES 1.1 主要是为了向后兼容。如果您正在维护 OpenGL ES 1.1 应用程序,请考虑为较新的 OpenGL ES 版本更新您的代码。

设计高性能 OpenGL ES 应用程序

总而言之,一个设计良好的 OpenGL ES 应用程序需要:

  • 利用 OpenGL ES 管道中的并行性。
  • 管理应用程序和图形硬件之间的数据流。

图 6-6 建议了使用 OpenGL ES 为显示器执行动画的应用程序的流程。


Figure6-6

当应用程序启动时,它所做的第一件事就是初始化在应用程序的生命周期内不打算更改的资源。理想情况下,应用程序将这些资源封装到 OpenGL ES 对象中。目标是创建可以在应用程序运行时(甚至是应用程序生命周期的一部分,例如游戏关卡的持续时间)保持不变的任何对象,以增加初始化时间来换取更好的渲染性能。复杂的命令或状态更改应替换为可与单个函数调用一起使用的 OpenGL ES 对象。例如,配置固定功能管道可能需要进行数十次函数调用。相反,在初始化时编译图形着色器,并在运行时通过单个函数调用切换到它。创建或修改成本高昂的 OpenGL ES 对象几乎总是应创建为静态对象。

渲染循环处理您打算渲染到 OpenGL ES 上下文的所有项目,然后将结果呈现给显示器。在动画场景中,每帧都会更新一些数据。在图 6-6 所示的内部渲染循环中,应用程序在更新渲染资源(在此过程中创建或修改 OpenGL ES 对象)和提交使用这些资源的绘图命令之间交替进行。这个内循环的目标是平衡工作负载,使 CPU 和 GPU 并行工作,防止应用程序和 OpenGL ES 同时访问相同的资源。在 iOS 上,当修改不是在帧的开始或结束时执行时,修改 OpenGL ES 对象可能会很昂贵。

此内部循环的一个重要目标是避免将数据从 OpenGL ES 复制回应用程序。将结果从 GPU 复制到 CPU 可能非常慢。如果复制的数据稍后也用作渲染当前帧的过程的一部分,如中间渲染循环所示,您的应用程序会阻塞,直到完成所有先前提交的绘图命令。

在应用程序提交框架中所需的所有绘图命令后,它会将结果呈现到屏幕上。非交互式应用程序会将最终图像复制到应用程序内存中以进行进一步处理。

最后,当您的应用程序准备退出时,或完成一项主要任务时,它会释放 OpenGL ES 对象,以便为自身或其他应用程序提供额外的资源。
总结一下这个设计的重要特征:

  • 在可行时创建静态资源。
  • 内部渲染循环在修改动态资源和提交渲染命令之间交替。尽量避免修改动态资源,除非在帧的开头或结尾。
  • 避免将中间渲染结果读回您的应用程序。
  • 本章的其余部分提供了有用的 OpenGL ES 编程技术来实现此渲染循环的功能。后面的章节将演示如何将这些通用技术应用于 OpenGL ES 编程的特定领域。

避免同步和刷新操作

OpenGL ES 规范不需要立即执行命令的实现。通常,命令排队到命令缓冲区并在稍后由硬件执行。通常,OpenGL ES 会等待应用程序将许多命令排入队列,然后再将命令发送到硬件——批处理通常更有效。但是,某些 OpenGL ES 函数必须立即刷新命令缓冲区。其他函数不仅刷新命令缓冲区,而且在返回对应用程序的控制之前阻塞直到先前提交的命令完成。仅在需要该行为时才使用刷新和同步命令。过度使用刷新或同步命令可能会导致您的应用程序在等待硬件完成渲染时停顿。

这些情况需要 OpenGL ES 将命令缓冲区提交给硬件执行。

  • 函数 glFlush 将命令缓冲区发送到图形硬件。它阻塞直到命令提交给硬件,但不等待命令完成执行。
  • 函数 glFinish 刷新命令缓冲区,然后等待所有先前提交的命令在图形硬件上完成执行。
  • 检索帧缓冲区内容的函数(例如 glReadPixels)也会等待提交的命令完成。
  • 命令缓冲区已满。

有效地使用 glFlush

在某些桌面 OpenGL 实现中,定期调用 glFlush 函数来有效地平衡 CPU 和 GPU 工作可能很有用,但在 iOS 中并非如此。 iOS 图形硬件实现的 Tile-Based Deferred Rendering 算法依赖于一次缓冲场景中的所有顶点数据,因此可以对其进行优化处理以去除隐藏表面。通常,只有两种情况下 OpenGL ES 应用程序应该调用 glFlush 或 glFinish 函数。

  • 当您的应用程序移至后台时,您应该刷新命令缓冲区,因为当您的应用程序在后台时在 GPU 上执行 OpenGL ES 命令会导致 iOS 终止您的应用程序。 (请参阅实现多任务感知 OpenGL ES 应用程序。)
  • 如果您的应用程序在多个上下文之间共享 OpenGL ES 对象(例如顶点缓冲区或纹理),您应该调用 glFlush 函数来同步对这些资源的访问。例如,您应该在一个上下文中加载顶点数据后调用 glFlush 函数,以确保其内容已准备好被另一个上下文检索。当与其他 iOS API(例如 Core Image)共享 OpenGL ES 对象时,此建议也适用。

避免查询 OpenGL ES 状态

对 glGet*() 的调用,包括 glGetError(),可能需要 OpenGL ES 在检索任何状态变量之前执行先前的命令。这种同步迫使图形硬件与 CPU 同步运行,从而减少并行的机会。为避免这种情况,请维护您自己需要查询的任何状态的副本,并直接访问它,而不是调用 OpenGL ES。

发生错误时,OpenGL ES 会设置错误标志。这些和其他错误出现在 Xcode 中的 OpenGL ES 帧调试器或仪器中的 OpenGL ES 分析器中。您应该使用这些工具而不是 glGetError 函数,如果频繁调用会降低性能。其他查询,例如 glCheckFramebufferStatus()、glGetProgramInfoLog() 和 glValidateProgram() 通常也仅在开发和调试时有用。您应该在应用的发布版本中省略对这些函数的调用。

使用 OpenGL ES 管理您的资源

许多 OpenGL 数据可以直接存储在 OpenGL ES 渲染上下文及其关联的共享组对象中。 OpenGL ES 实现可以自由地将数据转换为最适合图形硬件的格式。这可以显着提高性能,尤其是对于不经常更改的数据。您的应用程序还可以向 OpenGL ES 提供有关它打算如何使用数据的提示。 OpenGL ES 实现可以使用这些提示来更有效地处理数据。例如,静态数据可能被放置在图形处理器可以轻松获取的内存中,甚至可以放入专用的图形内存中。

使用双缓冲避免资源冲突

当您的应用程序和 OpenGL ES 同时访问 OpenGL ES 对象时,就会发生资源冲突。当一个参与者尝试修改另一个正在使用的 OpenGL ES 对象时,他们可能会阻塞,直到该对象不再使用。一旦他们开始修改对象其他参与者在修改完成之前可能无法访问该对象。或者,OpenGL ES 可以隐式复制对象,以便两个参与者可以继续执行命令。任何一个选项都是安全的,但每个选项最终都可能成为您的应用程序的瓶颈。图 6-7 显示了这个问题。在此示例中,有一个纹理对象,OpenGL ES 和您的应用程序都希望使用它。当应用程序尝试更改纹理时,它必须等待之前提交的绘图命令完成——CPU 与 GPU 同步


Figure6-7

为了解决这个问题,您的应用程序可以在更改对象和使用它绘图之间执行额外的工作。但是,如果您的应用程序没有可以执行的额外工作,则应明确创建两个大小相同的对象;当一个参与者阅读一个对象时,另一个参与者修改另一个。图 6-8 说明了双缓冲方法。当 GPU 处理一个纹理时,CPU 修改另一个纹理。初始启动后,CPU 或 GPU 都没有闲置。尽管显示的是纹理,但该解决方案几乎适用于任何类型的 OpenGL ES 对象。
双缓冲对于大多数应用程序来说已经足够了,但它要求两个参与者大致在同一时间完成处理命令。为避免阻塞,您可以添加更多缓冲区;这实现了传统的生产者-消费者模型。如果生产者在消费者完成处理命令之前完成,它会占用一个空闲缓冲区并继续处理命令。在这种情况下,生产者只有在消费者严重落后时才会空闲。

双缓冲和三缓冲权衡消耗额外的内存以防止管道停顿。额外使用内存可能会对应用程序的其他部分造成压力。在 iOS 设备上,内存可能很稀缺;您的设计可能需要在使用更多内存与其他应用程序优化之间取得平衡。

注意 OpenGL ES 状态

OpenGL ES 实现维护一组复杂的状态数据,包括您使用 glEnable 或 glDisable 函数设置的开关、当前着色器程序及其统一变量、当前绑定的纹理单元、当前绑定的顶点缓冲区及其启用的顶点属性。硬件有一个当前状态,它被懒惰地编译和缓存。切换状态的成本很高,因此最好设计您的应用程序以尽量减少状态切换。

不要设置已经设置的状态。功能一旦启用,就不需要再次启用。例如,如果您多次调用具有相同参数的 glUniform 函数,OpenGL ES 可能不会检查是否已经设置了相同的统一状态。即使该值与当前值相同,它也会简单地更新状态值。

通过使用专用的设置或关闭例程而不是将此类调用置于绘图循环中,避免设置不必要的状态。设置和关闭例程对于打开和关闭实现特定视觉效果的功能也很有用,例如,在带纹理的多边形周围绘制线框轮廓时。

使用 OpenGL ES 对象封装状态

为了减少状态变化,创建将多个 OpenGL ES 状态变化收集到一个可以与单个函数调用绑定的对象的对象。例如,顶点数组对象将多个顶点属性的配置存储到单个对象中。请参阅使用顶点数组对象合并顶点数组状态更改。(VAO)

组织绘图调用以最小化状态更改

更改 OpenGL ES 状态不会立即生效。相反,当您发出绘图命令时,OpenGL ES 会执行使用一组状态值进行绘图所需的工作。您可以通过最小化状态更改来减少重新配置图形管道所花费的 CPU 时间。例如,在您的应用程序中保留一个状态向量,并仅当您的状态在绘制调用之间发生变化时才设置相应的 OpenGL ES 状态。另一个有用的算法是状态排序——跟踪您需要执行的绘图操作以及每个操作所需的状态更改量,然后对它们进行排序以连续使用相同的状态执行操作。

OpenGL ES 的 iOS 实现可以缓存它在状态之间高效切换所需的一些配置数据,但每个唯一状态集的初始配置需要更长的时间。为了获得一致的性能,您可以“预热”您计划在设置例程中使用的每个状态集:

  • 启用您计划使用的状态配置或着色器。
  • 使用该状态配置绘制少量顶点。
  • 刷新 OpenGL ES 上下文,以便不显示在此预热阶段期间的绘图。

你可能感兴趣的:(OpenGL ES for iOS - 6)