OpenGL ES 框架详细解析(八) —— OpenGL ES 设计指南

96
刀客传奇
0.1 2017.10.01 12:43* 字数 6451

版本记录

版本号 时间
V1.0 2017.10.01

前言

OpenGL ES是一个强大的图形库,是跨平台的图形API,属于OpenGL的一个简化版本。iOS系统可以利用OpenGL ES将图像数据直接送入到GPU进行渲染,这样避免了从CPU进行计算再送到显卡渲染带来的性能的高消耗,能带来来更好的视频效果和用户体验。接下来几篇就介绍下iOS 系统的 OpenGL ES框架。感兴趣的可以看上面几篇。
1. OpenGL ES 框架详细解析(一) —— 基本概览
2. OpenGL ES 框架详细解析(二) —— 关于OpenGL ES
3. OpenGL ES 框架详细解析(三) —— 构建用于iOS的OpenGL ES应用程序的清单
4. OpenGL ES 框架详细解析(四) —— 配置OpenGL ES的上下文
5. OpenGL ES 框架详细解析(五) —— 使用OpenGL ES和GLKit进行绘制
6. OpenGL ES 框架详细解析(六) —— 绘制到其他渲染目的地
7. OpenGL ES 框架详细解析(七) —— 多任务,高分辨率和其他iOS功能

OpenGL ES Design Guidelines - OpenGL ES 设计指南

现在您已经掌握了在iOS应用程序中使用OpenGL ES的基础知识,请使用本章中的信息来帮助您设计应用程序的渲染引擎以获得更好的性能。 本章介绍了渲染器设计的关键概念; 随后的章节将根据具体的最佳做法和性能技术扩展这些信息。


How to Visualize OpenGL ES - 如何可视化OpenGL ES

本节介绍了可视化OpenGL ES设计的两个视角:作为客户端 - 服务器架构和管道。 这两个视角在规划和评估应用程序的体系结构方面都非常有用。

1. OpenGL ES as a Client-Server Architecture - OpenGL ES作为客户端 - 服务器架构

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

OpenGL ES client-server architecture

实现卓越性能需要仔细管理这种开销。 精心设计的应用程序可以降低OpenGL ES调用的频率,使用适合硬件的数据格式来最大限度地降低翻译成本,并仔细管理其本身与OpenGL ES之间的数据流。

2. OpenGL ES as a Graphics Pipeline - OpenGL ES作为图形流水线

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

OpenGL ES graphics pipeline

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

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

重要提示:当您调整应用程序的性能时,第一步通常是确定哪个阶段是瓶颈,以及为什么。


OpenGL ES Versions and Renderer Architecture - OpenGL ES版本和渲染器架构

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

1. 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 Shading Language Version 3.0 - OpenGL ES着色语言版本3.0

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

有关更多详细信息,请参阅 Adopting OpenGL ES Shading Language version 3.0OpenGL ES API Registry中的OpenGL ES着色语言3.0规范。

Multiple Render Targets - 多个渲染目标

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

此功能可以使用先进的渲染算法,例如延迟着色,您的应用程序首先渲染一组纹理来存储几何数据,然后执行从这些纹理读取的一个或多个阴影遍历,并执行照明计算以输出最终 图片。 因为这种方法预先计算照明计算的输入,所以增加更多数量的灯到场景的增量性能成本要小得多。 延迟着色算法需要多个渲染目标支持,如下图所示,以实现合理的性能。 否则,渲染到多个纹理需要为每个纹理单独的绘图通道。

Example of fragment shader output to multiple render targets

您可以使用“创建帧缓冲区对象”中描述的过程添加多个渲染目标。 您不必为帧缓冲区创建单个颜色附件,而是创建几个。 然后,调用glDrawBuffers函数来指定在渲染中使用哪个framebuffer附件,如下面代码所示。

// Setting up multiple render targets

// 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);

当您的应用程序发出绘图命令时,片段着色器决定为每个渲染目标中的每个像素输出的颜色(或非彩色数据)。 下面代码显示了一个基本的片段着色器,通过分配到与上面Setting up multiple render targets代码中设置的位置匹配的片段输出变量,呈现给多个目标。

// Fragment shader with output to multiple render targets

#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);
}

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

Transform Feedback - 转换反馈

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

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

Overview of a particle system animation

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

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

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

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

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

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

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

2. OpenGL ES 2.0

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

3. OpenGL ES 1.1

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

GLKit框架可以帮助您从OpenGL ES 1.1固定功能管道转换到更高版本。 有关详细信息,请参阅Using GLKit to Develop Your Renderer


Designing a High-Performance OpenGL ES App - 设计高性能OpenGL ES应用程序

总而言之,精心设计的OpenGL ES应用程序需要:

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

下图提出了使用OpenGL ES对显示器执行动画的应用程序的流程。

App model for managing resources

应用启动时,首先要做的是初始化在应用程序生命周期内不会改变的资源。 理想情况下,应用程序将这些资源封装到OpenGL ES对象中。 目标是创建任何对应用程序的运行时间保持不变的对象(甚至应用程序生命周期的一部分,例如游戏中的级别持续时间),交易增加初始化时间以获得更好的呈现性能。 复杂的命令或状态更改应替换为可用于单个函数调用的OpenGL ES对象。 例如,配置固定功能管道可能需要几十个函数调用。 相反,在初始化时编译图形着色器,并在运行时通过单个函数调用切换到图形着色器。 创建或修改昂贵的OpenGL ES对象几乎总是被创建为静态对象。

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

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

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

最后,当您的应用程序准备退出或完成主要任务时,它可以释放OpenGL ES对象,为自己或其他应用程序提供额外的资源。

总结本设计的重要特点:

  • 创建静态资源。
  • 内部渲染循环在修改动态资源和提交呈现命令之间交替。 尝试避免修改动态资源,除了帧的开头或结尾。
  • 避免将中间渲染结果读回您的应用程序。

本章的其余部分提供了有用的OpenGL ES编程技术来实现此渲染循环的功能。 稍后章节将演示如何将这些一般技术应用于OpenGL ES编程的特定领域。


Avoid Synchronizing and Flushing Operations - 避免同步和刷新操作

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

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

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

1. Using glFlush Effectively - 有效使用glFlush

在一些桌面OpenGL实现中,周期性地调用glFlush函数可以有效地平衡CPU和GPU的工作,但在iOS中并非如此。 由iOS图形硬件实现的基于平铺的延迟呈现算法取决于立即缓冲场景中的所有顶点数据,因此可以对隐藏的表面删除进行最佳处理。 通常情况下,OpenGL ES应用程序只能调用glFlushglFinish函数。

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

2. Avoid Querying OpenGL ES State - 避免查询OpenGL ES状态

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

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


Use OpenGL ES to Manage Your Resources - 使用OpenGL ES管理你的资源

许多OpenGL数据可以直接存储在OpenGL ES渲染上下文及其关联的共享组对象中。 OpenGL ES实现可以将数据转换为图形硬件最佳的格式。 这可以显着提高性能,特别是对于不频繁更改的数据。 您的应用程序还可以向OpenGL ES提供有关如何使用数据的提示。 OpenGL ES实现可以使用这些提示更有效地处理数据。 例如,静态数据可能被放置在存储器中,图形处理器可以容易地获取,或者甚至进入专用图形存储器。


Use Double Buffering to Avoid Resource Conflicts - 使用双缓冲来避免资源冲突

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

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

Double-buffered texture data

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

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


Be Mindful of OpenGL ES State - 注意OpenGL ES状态

OpenGL ES实现保持一组复杂的状态数据,包括使用glEnable或glDisable函数设置的开关,当前着色器程序及其统一变量,当前绑定的纹理单元以及当前绑定的顶点缓冲区及其启用的顶点属性。 硬件有一个当前的状态,它被编译和缓存的很不及时。 开关状态很贵,所以最好设计你的应用程序以最小化状态开关。

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

避免使用专门的设置或关闭例程来设置超出必要的状态,而不是将这样的调用放在绘图循环中。 设置和关闭例程也可用于打开和关闭实现特定视觉效果的功能 - 例如,在纹理多边形周围绘制线框轮廓时。

1. Encapsulate State with OpenGL ES Objects - 使用OpenGL ES对象封装状态

为了减少状态变化,创建将多个OpenGL ES状态更改收集到可以通过单个函数调用绑定的对象的对象。 例如,顶点数组对象将多个顶点属性的配置存储到单个对象中。 请参阅Consolidate Vertex Array State Changes Using Vertex Array Objects

2. Organize Draw Calls to Minimize State Changes - 组织绘制调用以最小化状态更改

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

OpenGL ES的iOS实现可以缓存在状态之间有效切换所需的一些配置数据,但每个独特状态集的初始配置需要更长时间。 为了保持一致的性能,您可以在安装程序中“预温”您计划使用的每个状态集:

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

后记

未完,待续~~~

OC
Web note ad 1