OpenGL ES for iOS - 7

调整您的OpenGL ES应用程序

iOS中OpenGL ES应用程序的性能与OS X或其他桌面操作系统中OpenGL的性能不同。虽然功能强大的计算设备,基于iOS设备的桌面或笔记本电脑不具备内存或CPU功能。使用与典型台式机或笔记本电脑GPU可能使用的算法不同的嵌入式GPU来优化较低的内存和功耗。低效渲染图形数据可能会导致较差的帧速率或极大地降低基于iOS设备的电池寿命。

后面的章节介绍了许多提高应用程序性能的技术;本章涵盖整体策略。除非另有说明,本章中的建议涉及OpenGL ES的所有版本。

使用Xcode和仪器调试和配置您的应用程序

在各种设备上的各种场景中测试其性能之前,请勿优化应用程序。 Xcode和Instruments包括帮助您识别应用程序中的性能和正确性问题的工具

  • 监视Xcode调试量表,了解性能的一般概述。当您从Xcode运行应用程序时,可以看到这些仪表,以便在开发应用程序时轻松发现性能变化。

  • 使用仪器中的OpenGL ES Analysis和OpenGL ES驱动程序工具,以更深入地了解运行时性能。获取关于您的应用程序的资源使用和符合OpenGL ES最佳做法的详细信息,并选择性地禁用部分图形管道,以便您可以确定哪个部分是应用程序中的重大瓶颈。有关详细信息,请参阅“仪器用户指南”。

  • 在Xcode中使用OpenGL ES Frame Debugger和性能分析器工具来精确定位性能和渲染问题。捕获用于渲染和呈现单个帧的所有OpenGL ES命令,然后通过这些命令查看每个OpenGL ES状态,绑定资源和输出帧缓冲区的影响。您还可以查看着色器源代码,编辑它,并查看更改如何影响渲染图像。在支持OpenGL ES 3.0的设备上,Frame Debugger还指出哪些绘制调用和着色器指令对渲染时间最有贡献。有关这些工具的更多信息,请参阅Xcode OpenGL ES工具概述。

注意 Xcode 和 Instruments 中的 OpenGL ES 错误

当您的应用程序错误地使用 OpenGL ES API(例如,通过请求底层硬件无法执行的操作)时,会发生 OpenGL ES 错误。即使您的内容呈现正确,这些错误也可能表明存在性能问题。检查 OpenGL ES 错误的传统方法是调用 glGetError 函数;但是,重复调用此函数会显着降低性能。相反,请使用上述工具来测试错误:

  • 在 Instruments 中分析您的应用程序时,请参阅 OpenGL ES 分析器工具的详细信息窗格以查看记录时报告的任何 OpenGL ES 错误。
  • 在 Xcode 中调试您的应用程序时,捕获一个帧以检查用于生成它的绘图命令,以及在执行这些命令时遇到的任何错误。

您还可以将 Xcode 配置为在遇到 OpenGL ES 错误时停止程序执行。 (请参阅添加 OpenGL ES 错误断点。)

注释您的 OpenGL ES 代码以进行信息性调试和分析

您可以通过将 OpenGL ES 命令组织成逻辑组并向 OpenGL ES 对象添加有意义的标签来提高调试和分析的效率。这些组和标签出现在 Xcode 中的 OpenGL ES 帧调试器中,如图 7-1 所示,以及仪器中的 OpenGL ES 分析器中。要添加组和标签,请使用 EXT_debug_marker 和 EXT_debug_label 扩展


Figure7-1

当您有一系列表示单个有意义操作的绘图命令时(例如,绘制游戏角色),您可以使用标记将它们分组以进行调试。清单 7-1 显示了如何对场景的单个元素的纹理、程序、顶点数组和绘制调用进行分组。首先,它调用 glPushGroupMarkerEXT 函数提供一个有意义的名称,然后它发出一组 OpenGL ES 命令。最后,它通过调用 glPopGroupMarkerEXT 函数关闭组。

Listing 7-1 使用调试标记来注释绘图命令

glPushGroupMarkerEXT(0, "Draw Spaceship");
glBindTexture(GL_TEXTURE_2D, _spaceshipTexture);
glUseProgram(_diffuseShading);
glBindVertexArrayOES(_spaceshipMesh);
glDrawElements(GL_TRIANGLE_STRIP, 256, GL_UNSIGNED_SHORT, 0);
glPopGroupMarkerEXT();

您可以使用多个嵌套标记在复杂场景中创建有意义的组的层次结构。当您使用 GLKView 类绘制 OpenGL ES 内容时,它会自动创建一个“渲染”组,其中包含您绘制方法中的所有命令。您创建的任何标记都嵌套在该组中。

标签为 OpenGL ES 对象提供有意义的名称,例如纹理、着色器程序和顶点数组对象。调用 glLabelObjectEXT 函数为对象指定一个名称,以便在调试和分析时显示。清单 7-2 说明了使用这个函数来标记一个顶点数组对象。如果您使用 GLKTextureLoader 类加载纹理数据,它会自动用文件名标记它创建的 OpenGL ES 纹理对象。

glGenVertexArraysOES(1, &_spaceshipMesh);
glBindVertexArrayOES(_spaceshipMesh);
glLabelObjectEXT(GL_VERTEX_ARRAY_OBJECT_EXT, _spaceshipMesh, 0, "Spaceship");

一般性能建议

使用常识来指导您的性能调优工作。例如,如果您的应用程序每帧仅绘制几十个三角形,则更改它提交顶点数据的方式不太可能提高其性能。寻找为您的工作提供最大性能改进的优化。

仅在场景数据更改时重绘场景

在渲染新帧之前,您的应用程序应该等到场景中的某些内容发生变化。 Core Animation 缓存呈现给用户的最后一个图像并继续显示它,直到呈现新的帧。

即使您的数据发生变化,也没有必要以硬件处理命令的速度渲染帧。对于用户来说,较慢但固定的帧速率通常比快速但可变的帧速率更平滑。每秒 30 帧的固定帧速率足以满足大多数动画的需求,并有助于降低功耗。

禁用未使用的 OpenGL ES 功能

最好的计算是您的应用程序永远不会执行的计算。例如,如果可以预先计算结果并将其存储在模型数据中,则可以避免在运行时执行该计算。

如果您的应用程序是为 OpenGL ES 2.0 或更高版本编写的,请不要创建具有大量开关和条件的单个着色器来执行应用程序渲染场景所需的每项任务。相反,编译多个着色器程序,每个着色器程序执行特定的、重点任务。

如果您的应用程序使用 OpenGL ES 1.1,请禁用渲染场景所不需要的任何固定功能操作。例如,如果您的应用不需要照明或混合,请禁用这些功能。同样,如果您的应用仅绘制 2D 模型,则应禁用雾和深度测试。

简化您的照明模型

这些指南既适用于 OpenGL ES 1.1 中的固定功能照明,也适用于您在 OpenGL ES 2.0 或更高版本中的自定义着色器中使用的基于着色器的照明计算。

为您的应用程序使用尽可能少的灯光和最简单的灯光类型。考虑使用平行光而不是聚光灯,后者需要更多的计算。着色器应在模型空间中执行光照计算;考虑在着色器中使用更简单的光照方程而不是更复杂的光照算法。
预先计算您的照明并将颜色值存储在可以通过片段处理进行采样的纹理中。

有效地使用基于 Tile 的延迟渲染

iOS 设备中使用的所有 GPU 都使用基于图块的延迟渲染 (TBDR)。当您调用 OpenGL ES 函数向硬件提交渲染命令时,这些命令会被缓冲,直到积累了大量命令。在您呈现渲染缓冲区或刷新命令缓冲区之前,硬件不会开始处理顶点和着色像素。然后,它通过将帧缓冲区划分为瓦片,然后为每个瓦片绘制一次命令,将这些命令作为单个操作渲染,每个瓦片仅渲染其中可见的图元。 (GLKView 类在您的绘图方法返回后呈现渲染缓冲区。如果您使用 CAEAGLLayer 类创建自己的用于显示的渲染缓冲区,则可以使用 OpenGL ES 上下文的 presentRenderbuffer: 方法来呈现它。glFlush 或 glFinish 函数会刷新命令缓冲区。)

由于 tile 内存是 GPU 硬件的一部分,因此深度测试和混合等渲染过程的部分在时间和能源使用方面比传统的基于流的 GPU 架构更高效。由于这种架构一次处理整个场景的所有顶点,因此 GPU 可以在处理片段之前执行隐藏表面去除。不可见的像素在不采样纹理或执行片段处理的情况下被丢弃,显着减少了 GPU 必须执行以渲染图块的计算。

一些在传统的基于流的渲染器上有用的渲染策略在 iOS 图形硬件上有很高的性能成本。遵循以下指南可以帮助您的应用在 TBDR 硬件上表现良好。

避免逻辑缓冲区加载和存储

当 TBDR 图形处理器开始渲染图块时,它必须首先将帧缓冲区该部分的内容从共享内存传输到 GPU 上的图块内存。这种内存传输称为逻辑缓冲区加载,需要时间和精力。大多数情况下,绘制下一帧不需要帧缓冲区的先前内容。每当您开始渲染新帧时,都调用 glClear 来避免加载先前缓冲区内容的性能成本。

同样,当 GPU 完成渲染图块时,它必须将图块的像素数据写回共享内存。这种传输称为逻辑缓冲存储,也有性能成本。对于绘制的每一帧,至少需要一个这样的传输——屏幕上显示的颜色渲染缓冲区必须传输到共享内存,以便 Core Animation 呈现它。渲染算法中使用的其他帧缓冲区附件(例如,深度、模板和多重采样缓冲区)不需要保留,因为它们的内容将在绘制的下一帧中重新创建。 OpenGL ES 会自动将这些缓冲区存储到共享内存中——这会导致性能成本——除非您明确地使它们无效。要使缓冲区无效,请使用 OpenGL ES 3.0 中的 glInvalidateFramebuffer 命令或 OpenGL ES 1.1 或 2.0 中的 glDiscardFramebufferEXT 命令。 (有关更多详细信息,请参阅丢弃不需要的渲染缓冲区。)当您使用 GLKView 类提供的基本绘制周期时,它会自动使其创建的任何可绘制深度、模板或多重采样缓冲区无效。

如果切换渲染目标,也会发生逻辑缓冲区存储和加载操作。如果渲染到纹理,然后渲染到视图的帧缓冲区,然后再次渲染到相同的纹理,则必须在共享内存和 GPU 之间重复传输纹理的内容。批处理您的绘图操作,以便一起完成所有到渲染目标的绘图。当您切换帧缓冲区(使用 glBindFramebuffer 或 glFramebufferTexture2D 函数或 bindDrawable 方法)时,使不需要的帧缓冲区附件无效以避免导致逻辑缓冲区存储。

有效地使用隐藏表面去除

TBDR 图形处理器自动使用深度缓冲区为整个场景执行隐藏表面去除,确保每个像素只运行一个片段着色器。不需要传统的减少片段处理的技术。例如,从前到后按深度对对象或基元进行排序会有效地重复 GPU 完成的工作,从而浪费 CPU 时间。

当启用混合或 alpha 测试时,或者片段着色器使用丢弃指令或写入 gl_FragDepth 输出变量时,GPU 无法执行隐藏表面移除。在这些情况下,GPU 无法使用深度缓冲区来决定片段的可见性,因此它必须为覆盖每个像素的所有基元运行片段着色器,从而大大增加渲染帧所需的时间和能量。为避免这种性能成本,请尽量减少混合、丢弃指令和深度写入的使用。

如果您无法避免混合、alpha 测试或丢弃指令,请考虑以下策略来减少它们的性能影响

  • 按不透明度对对象进行排序。先画不透明的物体。接下来使用丢弃操作(或 OpenGL ES 1.1 中的 alpha 测试)绘制需要着色器的对象。最后,绘制 alpha 混合对象。
  • 修剪需要混合或丢弃指令的对象以减少处理的片段数量。例如,不是绘制一个正方形来渲染包含大部分空白空间的 2D 精灵纹理,而是绘制一个更接近图像形状的多边形,如图 7-2 所示。额外顶点处理的性能成本远低于运行其结果将不被使用的片段着色器的性能成本。


    Figure7-2
  • 尽早在片段着色器中使用丢弃指令,以避免执行结果未使用的计算。
  • 不要使用 alpha 测试或丢弃指令来杀死像素,而是使用 alpha 混合并将 alpha 设置为零。颜色帧缓冲区没有被修改,但图形硬件仍然可以使用它执行的任何 Z 缓冲区优化。这确实会更改存储在深度缓冲区中的值,因此可能需要对透明图元进行从后到前的排序。
  • 如果您的性能受到不可避免的丢弃操作的限制,请考虑“Z-Prepass”渲染策略。使用仅包含丢弃逻辑(避免昂贵的光照计算)的简单片段着色器渲染一次场景以填充深度缓冲区。然后,使用 GL_EQUAL 深度测试功能和照明着色器再次渲染您的场景。尽管多通道渲染通常会导致性能损失,但与涉及大量丢弃操作的单通道渲染相比,这种方法可以产生更好的性能。

用于高效资源管理的 OpenGL ES 命令组

上述内存带宽和计算节省在处理大型场景时表现最佳。但是当硬件接收到需要它渲染较小场景的 OpenGL ES 命令时,渲染器的效率就会大大降低。例如,如果您的应用程序使用纹理渲染成批三角形,然后修改纹理,则 OpenGL ES 实现必须立即清除这些命令或复制纹理——这两种选择都不能有效地使用硬件。类似地,任何从帧缓冲区读取像素数据的尝试都需要处理前面的命令,如果它们会改变该帧缓冲区。

为避免这些性能损失,请组织 OpenGL ES 调用序列,以便同时执行每个渲染目标的所有绘图命令

尽量减少绘图命令的数量

每次您的应用程序提交要由 OpenGL ES 处理的图元时,CPU 都会为图形硬件准备命令。如果您的应用程序使用许多 glDrawArrays 或 glDrawElements 调用来渲染场景,则其性能可能会受到 CPU 资源的限制,而没有充分利用 GPU。

为了减少这种开销,请寻找将渲染合并为更少绘制调用的方法。有用的策略包括:

  • 将多个图元合并为一个三角形带,如使用三角形带批处理顶点数据中所述。为获得最佳效果,请合并在空间接近的情况下绘制的图元。当大型、庞大的模型在框架中不可见时,您的应用程序更难以有效地剔除它们。
  • 创建纹理图集以使用同一纹理图像的不同部分绘制多个图元,如将纹理组合到纹理图集中所述。
  • 使用实例化绘图来渲染许多类似的对象,如下所述。

使用实例化绘图来最小化绘图调用

实例化绘图命令允许您使用单个绘图调用多次绘制相同的顶点数据。与使用 CPU 时间来设置网格不同实例之间的变化(例如位置偏移、变换矩阵、颜色或纹理坐标)并为每个实例发出绘制命令不同,实例化绘制将实例变化的处理移动到着色器代码中在 GPU 上运行。

多次重复使用的顶点数据是实例化绘图的主要候选对象。例如,清单 7-3 中的代码在场景中的多个位置绘制一个对象。但是,许多 glUniform 和 glDrawArrays 调用会增加 CPU 开销,从而降低性能。

for (x = 0; x < 10; x++) {
    for (y = 0; y < 10; y++) {
        glUniform4fv(uniformPositionOffset, 1, positionOffsets[x][y]);
        glDrawArrays(GL_TRIANGLES, 0, numVertices);
    }
}

采用实例化绘图需要两个步骤:首先,用对 glDrawArraysInstanced 或 glDrawElementsInstanced 的单个调用替换上述循环。这些调用在其他方面与 glDrawArrays 或 glDrawElements 相同,但有一个额外的参数指示要绘制的实例数(示例 7-3 中的示例为 100)。其次,选择并实施 OpenGL ES 提供的两种策略之一,用于在您的顶点着色器中使用每个实例的信息。

使用着色器实例 ID 策略,您的顶点着色器会派生或查找每个实例的信息。每次顶点着色器运行时,它的 gl_InstanceID 内置变量包含一个标识当前正在绘制的实例的数字。使用此数字计算着色器代码中的位置偏移、颜色或其他每个实例的变化,或者在统一数组或其他大容量存储中查找每个实例的信息。例如,清单 7-4 使用这种技术绘制了位于 10 x 10 网格中的网格的 100 个实例。

#version 300 es
in vec4 position;
uniform mat4 modelViewProjectionMatrix;
void main()
{
    float xOffset = float(gl_InstanceID % 10) * 0.5 - 2.5;
    float yOffset = float(gl_InstanceID / 10) * 0.5 - 2.5;
    vec4 offset = vec4(xOffset, yOffset, 0, 0);
    gl_Position = modelViewProjectionMatrix * (position + offset);
}

使用实例化数组策略,您将每个实例的信息存储在顶点数组属性中。然后您的顶点着色器可以访问该属性以利用每个实例的信息。调用 glVertexAttribDivisor 函数来指定该属性在 OpenGL ES 绘制每个实例时如何前进。清单 7-5 演示了为实例绘制设置顶点数组,清单 7-6 显示了相应的着色器。

glGenBuffers(1, &_instBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _instBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(instData), instData, GL_STATIC_DRAW);
glEnableVertexAttribArray(kMyInstanceDataAttrib);
glVertexAttribPointer(kMyInstanceDataAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);
glVertexAttribDivisor(kMyInstanceDataAttrib, 1);
#version 300 es
layout(location = 0) in vec4 position;
layout(location = 5) in vec2 inOffset;
uniform mat4 modelViewProjectionMatrix;
void main()
{
    vec4 offset = vec4(inOffset, 0.0, 0.0)
    gl_Position = modelViewProjectionMatrix * (position + offset);
}

实例化绘图在核心 OpenGL ES 3.0 API 和 OpenGL ES 2.0 中通过 EXT_draw_instanced 和 EXT_instanced_arrays 扩展可用。

最小化 OpenGL ES 内存使用

您的 iOS 应用程序与系统和其他 iOS 应用程序共享主内存。为 OpenGL ES 分配的内存会减少应用程序中可用于其他用途的内存。考虑到这一点,只分配您需要的内存,并在您的应用不再需要它时立即释放它。以下是一些可以节省内存的方法:

  • 将图像加载到 OpenGL ES 纹理后,释放原始图像。
  • 仅在您的应用需要时分配深度缓冲区。
  • 如果您的应用程序不需要一次使用其所有资源,请仅加载项目的一个子集。例如,游戏可能分为多个级别;每个加载适合更严格资源限制的总资源的子集。

iOS 中的虚拟内存系统不使用交换文件(当内存不足时,将内存的数据copy到磁盘中,但内存空间回复时,将磁盘中的内存重新copy到内存中)。当检测到内存不足情况时,虚拟内存不会将易失性页面写入磁盘,而是释放非易失性内存,为正在运行的应用程序提供所需的内存。您的应用程序应努力使用尽可能少的内存,并准备好处理对您的应用程序不重要的对象。 iOS 应用程序编程指南中详细介绍了对低内存情况的响应。

注意核心动画合成性能

Core Animation 将渲染缓冲区的内容与视图层次结构中的任何其他层进行合成,无论这些层是使用 OpenGL ES、Quartz 还是其他图形库绘制的。这很有帮助,因为这意味着 OpenGL ES 是 Core Animation 的一等公民。但是,将 OpenGL ES 内容与其他内容混合需要时间;如果使用不当,您的应用可能会执行得太慢而无法达到交互式帧速率。

为获得绝对最佳性能,您的应用程序应完全依赖 OpenGL ES 来呈现您的内容。调整包含 OpenGL ES 内容的视图的大小以匹配屏幕,确保其 opaque 属性设置为 YES(GLKView 对象的默认值)并且没有其他视图或核心动画层可见。

如果您渲染到合成在其他层之上的核心动画层,使您的 CAEAGLLayer 对象不透明会降低(但不会消除)性能成本。如果您的 CAEAGLLayer 对象混合在层层次结构中它下面的层的顶部,则渲染缓冲区的颜色数据必须采用预乘 alpha 格式才能由 Core Animation 正确合成。在其他内容之上混合 OpenGL ES 内容会导致严重的性能损失。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容