OpenGL ES 框架详细解析(九) —— 调整您的OpenGL ES应用程序

96
刀客传奇
0.2 2017.10.02 11:37* 字数 6028

版本记录

版本号 时间
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功能
8. OpenGL ES 框架详细解析(八) —— OpenGL ES 设计指南

Tuning Your OpenGL ES App - 调整您的OpenGL ES应用程序

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

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


Debug and Profile Your App with Xcode and Instruments - 使用Xcode和仪器调试和配置您的应用程序

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

  • 监视Xcode调试量表,以了解性能的一般概述。 当您从Xcode运行应用程序时,可以看到这些仪表,以便在开发应用程序时轻松发现性能变化。
  • 使用仪器中的OpenGL ES分析和OpenGL ES驱动程序工具,以更深入地了解运行时性能。 获取关于您的应用程序的资源使用和符合OpenGL ES最佳做法的详细信息,并选择性地禁用部分图形管道,以便您可以确定哪个部分是您的应用程序中的重大瓶颈。 有关更多信息,请参阅 Instruments User Guide
  • 在Xcode中使用OpenGL ES Frame Debugger和性能分析器工具来精确定位性能和渲染问题。 捕获用于渲染和呈现单个帧的所有OpenGL ES命令,然后遍历这些命令,以查看每个对OpenGL ES状态,绑定的资源和输出帧缓冲区的影响。 您还可以查看着色器源代码,编辑它,并查看更改如何影响渲染的图像。 在支持OpenGL ES 3.0的设备上,Frame Debugger还指出哪些绘图调用和着色器指令对渲染时间最有贡献。 有关这些工具的更多信息,请参阅Xcode OpenGL ES Tools Overview

1. Watch for OpenGL ES Errors in Xcode and Instruments - 在Xcode和Instruments中观察OpenGL ES错误

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

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

当遇到OpenGL ES错误时,还可以配置Xcode以停止程序执行。 (请参阅Adding an OpenGL ES Error Breakpoint。)

2. Annotate Your OpenGL ES Code for Informative Debugging and Profiling - 注释您的OpenGL ES代码进行信息调试和分析

您可以通过将OpenGL ES命令组织到逻辑组中并为OpenGL ES对象添加有意义的标签来使调试和分析更加高效。 这些组和标签出现在Xcode中的OpenGL ES Frame Debugger中,如下图所示,在仪器中的OpenGL ES Analyzer中。 要添加组和标签,请使用EXT_debug_markerEXT_debug_label扩展。

Xcode Frame Debugger before and after adding debug marker groups

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

// Using a debug marker to annotate drawing commands

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函数为对象提供调试和分析时要显示的名称。 下面代码说明了使用这个函数来标记一个顶点数组对象。 如果您使用GLKTextureLoader类加载纹理数据,它会自动标记其使用其文件名创建的OpenGL ES纹理对象。

// Using a debug label to annotate an OpenGL ES object

glGenVertexArraysOES(1, &_spaceshipMesh);

glBindVertexArrayOES(_spaceshipMesh);

glLabelObjectEXT(GL_VERTEX_ARRAY_OBJECT_EXT, _spaceshipMesh, 0, "Spaceship");

General Performance Recommendations - 一般性能推荐

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

1. Redraw Scenes Only When the Scene Data Changes - 仅当场景数据更改时才重画场景

您的应用程序应该等待,直到场景中的某些内容发生变化才能渲染新的帧 核心动画缓存呈现给用户的最后一张图像,并继续显示,直到出现新的帧。

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

2. Disable Unused OpenGL ES Features - 禁用未使用的OpenGL ES功能

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

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

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

3. Simplify Your Lighting Models - 简化您的照明模型

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

为您的应用程序使用最少的灯和最简单的照明类型。 考虑使用定向灯而不是点光源,点光源这需要更多的计算。 着色器应在模型空间中执行照明计算; 在更复杂的照明算法中,考虑在着色器中使用更简单的照明方程。

预先计算您的照明,并将颜色值存储在可通过片段处理进行采样的纹理中。


Use Tile-Based Deferred Rendering Efficiently - 有效地使用基于平铺的延迟渲染

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

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

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

1. Avoid Logical Buffer Loads and Stores - 避免逻辑缓冲区负载和存储

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

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

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

2. Use Hidden Surface Removal Effectively - 有效地使用隐藏的表面去除

TBDR图形处理器自动使用深度缓冲区为整个场景执行隐藏的表面删除,确保每个像素只运行一个片段着色器。 用于减少片段处理的传统技术不是必需的。 例如,通过深度从前到后排序对象或原语有效地复制了GPU完成的工作,浪费了CPU时间。

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

如果您无法避免混合,Alpha测试或丢弃说明,请考虑以下策略来降低其性能影响:

  • 按不透明度排序对象。 先绘制不透明物体。 接下来,使用丢弃操作(或OpenGL ES 1.1中的Alpha测试)绘制需要着色器的对象。 最后,绘制alpha混合对象。
  • 修剪需要混合或丢弃指令的对象以减少处理的碎片数量。 例如,如下图所示,不是绘制一个正方形来渲染包含大部分空白空间的2D精灵纹理,而是绘制一个更贴近图像形状的多边形。 附加顶点处理的性能成本远低于运行片段着色器,运行片段着色器的结果将不被使用。
Trim transparent objects to reduce fragment processing
  • 在片段着色器中尽早使用丢弃指令,以避免执行结果未使用的计算。
  • 不使用alpha测试或丢弃指令来杀死像素,而是将alpha混合与Alpha设置为零。 彩色帧缓冲区未被修改,但图形硬件仍然可以使用它执行的任何Z缓冲区优化。 这样做会改变存储在深度缓冲区中的值,因此可能需要对透明基元进行前后排序。
  • 如果您的表现受到不可避免的丢弃操作限制,请考虑“Z-Prepass”渲染策略。 使用简单的片段着色器渲染场景,只包含丢弃逻辑(避免昂贵的照明计算)来填充深度缓冲区。 然后,使用GL_EQUAL深度测试功能和您的照明着色器再次渲染您的场景。 虽然多次渲染通常会导致性能损失,但是这种方法可以产生比涉及大量丢弃操作的单遍渲染更好的性能。

3. Group OpenGL ES Commands for Efficient Resource Management - OpenGL ES命令用于高效的资源管理

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

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


Minimize the Number of Drawing Commands - 最小化绘图命令的数量

每当您的应用程序提交要由OpenGL ES处理的图元时,CPU将准备图形硬件的命令。 如果您的应用程序使用许多glDrawArraysglDrawElements调用来渲染场景,则其性能可能受到CPU资源的限制,而不会完全利用GPU。

为了减少这种开销,寻找将渲染整合到较少绘图调用中的方法。 有用的策略包括:

  • 将多个基元合并成单个三角形条,如Use Triangle Strips to Batch Vertex Data中所述。 为获得最佳效果,请合并在紧密的空间附近绘制的图元。 大量,蔓延的模型更难以有效地剔除当他们在帧中不可见时。
  • 创建纹理地图集以使用相同纹理图像的不同部分绘制多个图元,如Combine Textures into Texture Atlases中所述。
  • 使用实例绘制来渲染许多类似的对象,如下所述。

1. Use Instanced Drawing to Minimize Draw Calls - 使用实例化绘图来最小化绘制调用

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

重复使用的顶点数据是实例绘制的主要候选者。 例如,下面代码中的代码在场景中的多个位置绘制一个对象。 然而,许多glUniformglDrawArrays调用增加了CPU开销,从而降低了性能。

// Drawing many similar objects without instancing

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

采用实例化绘图需要两个步骤:首先,如上所述替换循环,单次调用glDrawArraysInstancedglDrawElementsInstanced。 这些调用与glDrawArraysglDrawElements相同,但附加参数指示要绘制的实例数(上面代码中的示例为100)。 其次,选择和实现OpenGL ES为您的顶点着色器使用每个实例信息提供的两个策略之一。

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

// OpenGL ES 3.0 vertex shader using gl_InstanceID to compute per-instance information

#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绘制每个实例时该属性的进度。 下面两段代码的第一段演示了为实例绘制设置一个顶点数组,第二段显示了相应的着色器。

// Using a vertex attribute for per-instance information

#define kMyInstanceDataAttrib 5

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);
// OpenGL ES 3.0 vertex shader using instanced arrays

#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 APIOpenGL ES 2.0中通过EXT_draw_instancedEXT_instanced_arrays扩展提供。


Minimize OpenGL ES Memory Usage - 最小化OpenGL ES内存使用

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

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

iOS中的虚拟内存系统不使用交换文件。 当检测到低内存条件时,虚拟内存不会将易失性页面写入磁盘,而是释放非易失性内存,为运行中的应用程序提供所需的内存。 您的应用程序应尽可能少地使用内存,并准备处理对应用程序不是必需的对象。 针对低内存条件的响应在iOS的App Programming Guide for iOS中有详细介绍。


Be Aware of Core Animation Compositing Performance - 要注意Core Animation合成性能

Core Animation将renderbuffers的内容与视图层次结构中的任何其他图层相结合,无论这些图层是用OpenGL ES,Quartz还是其他图形库绘制。 这很有帮助,因为这意味着OpenGL ES是核心动画的first - class citizen。 然而,将OpenGL ES内容与其他内容混合需要时间; 当使用不当时,您的应用程序可能执行得太慢,无法达到交互式帧速率。

为了获得最佳性能,您的应用程序应该仅依靠OpenGL ES来呈现您的内容。 将保存OpenGL ES内容的视图设置为与屏幕匹配,确保其opaque属性设置为YES(GLKView对象的默认值),并且不显示其他视图或Core Animation图层。

如果将其渲染为在其他图层之上合成的Core Animation图层,则使CAEAGLLayer对象不透明减少但不会消除性能成本。 如果您的CAEAGLLayer对象在图层层次结构中的层之下混合在一起,则renderbuffer的颜色数据必须是由Core Animation正确合成的预乘法alpha格式。 将OpenGL ES内容混合在其他内容之上具有严重的性能损失。

后记

未完,待续~~~

OC
Web note ad 1