OpenGL ES 设计指南

本章帮助设计应用的渲染引擎,提高性能。介绍渲染器设计的关键概念,后面章节使用特定的最佳实践和性能技术扩展此信息

可视化OpenGL ES

两种可视化OpenGL ES设计观点:作为客户端-服务器体系结构和作为管道。

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


app将状态更改、纹理和顶点数据以及渲染命令传达给OpenGL ES客户端。客户端将此数据转换为图形硬件可以理解的格式,并将其转发给GPU。这个过程会增加应用程序的图形性能开销。

为了出色的性能,要仔细管理开销。经过精心设计的app可以减少OpenGL ES的调用频率,使用适合硬件的数据格式来最大程度降低翻译成本,仔细管理其与OpenGL ES之间的数据流。

OpenGL ES作为图形管线

app配置pipeline,然后执行绘图命令将顶点数据向下发送到管线。顶点着色器处理顶点数据,将顶点组装为土元,将图元光栅化为片段(fragments),运行片元着色器计算每个片元的颜色和深度值,并将片源混合到帧缓冲区进行显示

渲染器设计包括编写着色器程序处理管道的顶点和片段阶段,组织输入到这些程序中的顶点和纹理数据以及配置驱动管道的固定阶段的OpenGL ES状态机

图形管道中的各个阶段可以同时计算其结果-例如,应用可能准备新的图元,而图形硬件的各个部x分对先前提交的的几何图形执行顶点和片段计算。但后面阶段取决于前面阶段的输出。如果任何管道阶段执行过多的工作或执行的太慢则其他极端会处于闲置阶段直到最慢的工作完成。因此,精心设计的应用app可以根据图形硬件功能平衡每个流水线阶段执行的工作

OpenGL ES版本和渲染器体系机构

OpenGL ES 3.0是iOS 7中的新功能。

3.0中添加了新功能,例如 uniform block、32-bit运算、或者额外的整数操作,用于在顶点着色器和片段着色器程序中执行跟你更多通用计算任务。

参阅Adopting OpenGL ES Shading Language version 3.0
OpenGL ES API Registry.

多渲染目标

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

可以创建多个渲染目标,然后调用glDrawBuffers指定在渲染中使用哪些帧缓冲区附件
示例: 设置多渲染目标

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

当app发出绘制命令,片元着色器确定为每个渲染目中的像素输出什么颜色。
示例:片元着色器,通过上面代码设置的片元输出变量渲染到多个目标

#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

图形硬件使用高度并行的体系结构优化矢量处理。可以通过Transform Feedback功能利用此硬件,允许你捕获顶点着色器的输出到GPU内存的缓冲区对象中。可以从一个渲染通道捕获数据到另一个渲染通道,或者禁用部分图形管线,或将transform feedback用于general-purpose。

例如:传统上,实现粒子系统的app在CPU上运行,并将模拟结果存储在顶点缓冲区,用于渲染粒子。但是,将顶点缓冲区内容发传输到GPI内存是很耗时的,因此变换反馈,通过现代GPU硬件中的并行体系结构功能,可以有效的解决问题。
变换反馈的解决此问题优化:由于OpenGL ES将每个粒子及其状态表示为一个顶点,因此GPU的顶点着色器阶段可以一次运行多个粒子的仿真。由于包含粒子状态数据的顶点缓冲区在帧之间被重用,因此将数据传输到GPU内存的昂贵过程仅在初始化时发生一次。

  1. 初始化时,创建一个顶点缓冲区,并用包含所有粒子的初始转哪个台数据填充
  2. 在GLSL顶点着色器程序中实现粒子,并通过绘制包含粒子位置数据的顶点顶点缓冲区的内容在每一帧中运行它
    • 在启用了transform feedback情况下进行渲染,调用glBeginTransformFeedBack函数(在恢复正常图形之前调用glEndTransformFeedback())
    • 使用glTransformFeedBackVarying函数指定应通过transform feedback捕获的着色器输出,使用glBindBufferBaseglBindBufferRange函数以及GL_TRANSFORM_FEEDBACK_BUFFER缓冲区类型指定将其捕获到缓冲区
    • 通过调用glEnable(GL_RASTERIZER_DISCARD)禁用光栅化(以及管道执行的后续阶段)
  3. 渲染模拟的粒子结果进行显示,使用包含粒子位置的顶点缓冲区作为第二次绘制过程的输入,并再次启用光栅化(以及管道的后续部分),并使用适合渲染app的顶点和片段着色器
  4. 在下一帧,将上一帧模拟步骤输出的顶点缓冲区用作下一模拟步骤的输入

设计高性能OpenGL ES应用程序

启动应用程序时,第一件事就是初始化在应用程序的生命周期内不打算更改的资源。理想,将这些资源封装到OpenGL ES对象中,目标是创建在app运行期间保持不变的任何对象,以增加初始化时间为代价来获得更好的渲染性能。复杂的命令或者状态的更改应被替换为通过单个函数调用使用OpenGL ES对象。可选的,在初始化时编译图形着色器,然后在运行时通过单个函数调用切换到它。OpenGL ES对象的创建或修改操作始终成本高昂,因此应时钟作为静态对象创建

在渲染循环中,每帧都会更新一些数组,如上图的内部循环所示,应用程序在更新渲染资源和提交资源的绘图命令中进行交替。其目的是平衡工作服在,使CPU和GPU并行工作,防止app和OpenGLES同时访问相同的资源。在iOS上,如果未在帧的开始或结尾进行修改,则修改OpenGL ES对象的成本可能很高。

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

在app提交帧所需的全部绘图命令后,将结果显示在屏幕上。在非交互的app会将最终图像复制到app内存中进行进一步处理。

最后,当您的应用程序准备退出时或完成一项主要任务时,它会释放OpenGL ES对象以使其自身或其他应用程序有更多资源可用。

总结:

  • 尽可能创建静态资源
  • 内部渲染循环在修改动态资源和提交渲染命令之间交替,避免修改动态资源,除非在帧的开始或结尾
  • 避免将中间循环结果读回app

避免Synchronizing和Flushing操作

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

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

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

高效使用glFlush

通常,只有两种情况下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框架调试器或Instruments的OpenGL ES分析器中。您应该使用这些工具代替glGetError函数,如果频繁调用它会降低性能。其他查询,例如glCheckFramebufferStatus(),glGetProgramInfoLog()glValidateProgram()通常也仅在开发和调试时有用。您应该在应用的内部版本中忽略对这些功能的调用。

使用OpenGLES管理资源

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

使用双缓冲以避免资源冲突

当app和OpenGL ES同时访问OpenGL ES对象时 就会发生资源冲突,此时当尝试修改另一个参与者正在使用的OpenGL ES对象时,他们可能会阻塞,直到不再使用该对象为止。一旦他们开始修改对象,其他参与者就可能无法访问对象,直到修改完成。或者,OpenGL ES可以隐式复制对象,以便两个参与者都可以继续执行命令。两种方法都是安全的,但每种方法最终都会成为应用程序的瓶颈。(如下图所示)

下图示例:有一个纹理对象,OpenGL ES和您的应用程序都希望使用该对象。当应用尝试更改纹理时,它必须等待,直到先前提交的绘图命令完成为止-CPU同步到GPU。

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


对于大多数应用程序来说,双缓冲就足够了

注意OpenGL ES状态

OpenGL ES实现维护一组复杂的状态数据,硬件具有一种当前状态,该状态被延迟编译和缓存。切换状态非常昂贵,因此最好设计您的应用程序以最小化状态切换。

不要设置已经设置的状态。启用功能后,无需再次启用它。

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

使用OpenGL ES对象封装状态

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

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

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

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

  1. 启用您打算使用的状态配置或着色器。
  2. 使用该状态配置绘制少量的顶点。
  3. 刷新OpenGL ES上下文,以免显示此预热阶段中的图形。