OpenGL ES設計ガイドライン(パート1)

今日は、レンダラーデザインの主要な概念を紹介します。後の章では、この情報を特定のベストプラクティスとパフォーマンス手法で拡張します。

OpenGLESを視覚化する方法

OpenGL ES設計の2つの視点を視覚化します。クライアントサーバーアーキテクチャとして、およびパイプラインとしてです。どちらの観点も、アプリケーションのアーキテクチャを計画および評価するために使用できます。

クライアント/サーバーアーキテクチャとしてのOpenGLES

図1は、OpenGLESをクライアントサーバーアーキテクチャとして視覚化したものです。アプリケーションは、状態の変化、テクスチャと頂点のデータ、およびレンダリングコマンドをOpenGLESクライアントに伝達します。クライアントは、このデータをグラフィックハードウェアが理解できる形式に変換し、GPUに転送します。これらのプロセスは、アプリケーションにグラフィックパフォーマンスのオーバーヘッドを追加します。

   1OpenGLESクライアントサーバーアーキテクチャ

良好なパフォーマンスを実現するには、このオーバーヘッドを注意深く管理する必要があります。適切に設計されたアプリケーションは、OpenGL ESへの呼び出し頻度を減らし、ハードウェアに適したデータ形式を使用して変換コストを最小限に抑え、アプリケーションとOpenGLES間のデータフローを注意深く管理します。

グラフィックパイプラインとしてのOpenGLES

図2は、OpenGLESをグラフィックスパイプラインとして視覚化したものです。アプリケーションはグラフィックスパイプラインを構成してから、描画コマンドを実行して頂点データをパイプラインに送信します。パイプラインの連続するステージでは、頂点シェーダーを実行して頂点データを処理し、頂点をプリミティブにアセンブルし、プリミティブをフラグメントにラスタライズし、フラグメントシェーダーを実行して各フラグメントの色と深度の値を計算し、フラグメントをフレームバッファーにブレンドして表示します。

   2OpenGLESグラフィックパイプライン

OpenGLは、データがプログラムを流れるときに複雑な操作を実行します

メンタルモデルとしてパイプラインを使用して、アプリケーションが新しいフレームワークを生成するためにどのような作業を行うかを決定します。レンダラーの設計は、パイプラインの頂点ステージとフラグメントステージを処理するシェーダープログラムの作成、これらのプログラムにフィードする頂点とテクスチャデータの整理、パイプラインの固定機能ステージを駆動するOpenGLESステートマシンの構成で構成されます。

グラフィックスパイプラインのさまざまなステージで結果を同時に計算できます。たとえば、アプリケーションで新しいプリミティブを準備し、グラフィックスハードウェアの個別の部分で、以前に送信されたジオメトリで頂点とフラグメントの計算を実行できます。ただし、後のステージは前のステージの出力に依存します。いずれかのパイプラインステージの実行が多すぎるか、実行速度が遅すぎる場合、他のパイプラインステージは、最も遅いステージがその作業を完了するまでアイドル状態になります。適切に設計されたアプリケーションは、グラフィックハードウェアの機能に応じて、各パイプラインステージによって実行される作業のバランスを取ります。

OpenGLESバージョンとレンダラーアーキテクチャ

iOSは、OpenGLESの3つのバージョンをサポートしています。新しいバージョンは柔軟性が高く、パフォーマンスを損なうことなく高品質の視覚効果を含むレンダリングアルゴリズムを実装できます。

OpenGL ES 3.0

OpenGLES3.0はiOS7の新機能です。アプリケーションは、OpenGL ES 3.0で導入された機能を使用して、高度なグラフィックプログラミング技術(以前はデスクトップクラスのハードウェアおよびゲームコンソールでのみ利用可能)を実装して、より高速なグラフィックパフォーマンスと魅力的なビジュアルを実現できます。

以下は、OpenGLES3.0の主な機能の一部を示しています。完全な概要については、OpenGLESAPIレジストリのOpenGLES3.0仕様を参照してください。

OpenGLESシェーディング言語バージョン3.0

GLSL ES 3.0は、統合ブロック、32ビット整数、頂点およびフラグメントシェーダープログラムでのより一般的なコンピューティングタスクのための追加の整数演算などの新機能を追加します。#version 330 esシェーダープログラムで新しい言語を使用するには、シェーダーのソースコードが命令で始まる必要があります。OpenGL ES 3.0コンテキストは、OpenGLES2.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 可能不会检查是否已经设置了相同的统一状态。即使该值与当前值相同,它也会简单地更新状态值。

このような呼び出しを描画ループに入れる代わりに、専用のセットアップまたはシャットダウンルーチンを使用して、不要な状態を設定しないようにします。セットアップとシャットダウンのルーチンは、特定の視覚効果を実現する機能のオンとオフを切り替える場合にも役立ちます。たとえば、テクスチャポリゴンの周囲にワイヤーフレームの輪郭を描画する場合などです。

OpenGLESオブジェクトで状態をカプセル化する

状態の変化を減らすには、複数のOpenGLES状態の変化を単一の関数呼び出しに関連付けることができるオブジェクトに収集するオブジェクトを作成します。たとえば、頂点配列オブジェクトは、複数の頂点属性の構成を1つのオブジェクトに格納します。頂点配列オブジェクトを使用した頂点配列の状態変更のマージを参照してください

状態の変化を最小限に抑えるために描画呼び出しを整理します

OpenGL ESの状態を変更しても、すぐには効果がありません。代わりに、描画コマンドを発行すると、OpenGLESは一連の状態値を使用して描画に必要な作業を実行します。状態の変化を最小限に抑えることで、グラフィックスパイプラインの再構成にかかるCPU時間を削減できます。たとえば、アプリケーションに状態ベクトルを保持し、描画呼び出し間で状態が変化した場合にのみ、対応するOpenGLES状態を設定します。もう1つの便利なアルゴリズムは、状態の順序付けです。実行する必要のある描画操作と各操作に必要な状態変更の量を追跡し、同じ状態を使用して操作を連続して実行するように順序付けます。

OpenGL ESのiOS実装は、状態を効率的に切り替えるために必要な構成データの一部をキャッシュできますが、各固有の状態セットの初期構成には時間がかかります。一貫したパフォーマンスを得るには、セットアップルーチンで使用する予定の各状態セットを「ウォームアップ」できます。

  1. 使用する予定の状態構成またはシェーダーを有効にします。
  2. この状態構成を使用して、少数の頂点を描画します。
  3. OpenGL ESコンテキストをフラッシュして、ウォームアップフェーズ中に図面が表示されないようにします。

おすすめ

転載: juejin.im/post/7120058470744719390
おすすめ