OpenGL ES 设计指南(上)

今天我们来介绍渲染器设计的关键概念;后面的章节通过特定的最佳实践和性能技术扩展了这些信息。

如何可视化 OpenGL ES

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

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

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

图 1   OpenGL ES 客户端-服务器架构

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

OpenGL ES 作为图形管道

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

扫描二维码关注公众号,回复: 14374801 查看本文章

图 2   OpenGL ES 图形管线

OpenGL 在数据流经程序时执行复杂的操作

使用管道作为心智模型来确定您的应用程序执行哪些工作来生成新框架。您的渲染器设计包括编写着色器程序来处理管线的顶点和片段阶段,组织您输入这些程序的顶点和纹理数据,以及配置驱动管线的固定功能阶段的 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 Registry中的OpenGL ES 3.0 规范

OpenGL ES 着色语言版本 3.0

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

多个渲染目标

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

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

图 3  片段着色器输出到多个渲染目标的示例 MultipleRenderTargets_2x.png

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

清单 1  设置多个渲染目标

// 将(先前创建的)纹理附加到帧缓冲区。
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);
 
// 指定用于渲染的帧缓冲附件。
GLenum targets[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2};
glDrawBuffers(3, targets);
复制代码

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

清单 2  输出到多个渲染目标的片段着色器

#版本 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 内存中的缓冲区对象中。您可以从一个渲染通道捕获数据以在另一个渲染通道中使用,或者禁用部分图形管道并使用变换反馈进行通用计算。

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

图 4  粒子系统动画概览

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

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

图 5  使用变换反馈的图形管线配置示例

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

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

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

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

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

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 版本更新您的代码。

GLKit 框架可以帮助您从 OpenGL ES 1.1 固定功能管道过渡到更高版本。

设计高性能 OpenGL ES 应用程序

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

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

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

图 6  管理资源的 App 模型

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

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

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

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

最后,当您的应用程序准备好退出时,或者当它完成一项主要任务时,它会释放 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 应用程序应该调用glFlushorglFinish函数。

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

避免查询 OpenGL ES 状态

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

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

使用 OpenGL ES 管理您的资源

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

使用双缓冲避免资源冲突

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

图 7  单缓冲纹理数据

单缓冲顶点数组数据

为了解决这个问题,您的应用程序可以在更改对象和使用它绘图之间执行额外的工作。但是,如果您的应用程序没有它可以执行的额外工作,它应该显式创建两个相同大小的对象;当一个参与者读取一个对象时,另一个参与者修改另一个。图 8说明了双缓冲方法。当 GPU 在一个纹理上运行时,CPU 会修改另一个纹理。初始启动后,CPU 或 GPU 都不会处于空闲状态。尽管显示的是纹理,但此解决方案几乎适用于任何类型的 OpenGL ES 对象。

图 8  双缓冲纹理数据

双缓冲顶点数组数据

对于大多数应用程序来说,双缓冲就足够了,但它要求两个参与者在大致相同的时间内完成处理命令。为避免阻塞,可以添加更多缓冲区;这实现了传统的生产者-消费者模型。如果生产者在消费者完成命令处理之前完成,它会占用一个空闲缓冲区并继续处理命令。在这种情况下,生产者只有在消费者严重落后时才会空闲。

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

注意 OpenGL ES 状态

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

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

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

用 OpenGL ES 对象封装状态

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

组织绘制调用以最小化状态变化

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

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

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

猜你喜欢

转载自juejin.im/post/7120058470744719390