Directives de conception OpenGL ES (Partie 1)

Aujourd'hui, nous introduisons les concepts clés de la conception de moteur de rendu ; les chapitres suivants étendent ces informations avec des meilleures pratiques et des techniques de performance spécifiques.

Comment visualiser OpenGL ES

Visualisez deux perspectives de conception OpenGL ES : en tant qu'architecture client-serveur et en tant que pipeline. Les deux perspectives peuvent être utilisées pour planifier et évaluer l'architecture d'une application.

OpenGL ES en tant qu'architecture client-serveur

La figure 1 visualise OpenGL ES en tant qu'architecture client-serveur. Votre application communique les changements d'état, les données de texture et de sommet et les commandes de rendu aux clients OpenGL ES. Le client convertit ces données dans un format que le matériel graphique peut comprendre et les transmet au GPU. Ces processus ajoutent une surcharge de performances graphiques à l'application.

Figure 1    Architecture client-serveur OpenGL ES

L'obtention de bonnes performances nécessite une gestion prudente de ces frais généraux. Une application bien conçue effectuera ses appels à OpenGL ES moins fréquemment, utilisera des formats de données adaptés au matériel pour minimiser les coûts de traduction et gérera avec soin le flux de données entre elle-même et OpenGL ES.

OpenGL ES en tant que pipeline graphique

La figure 2 visualise OpenGL ES sous la forme d'un pipeline graphique. Votre application configure le pipeline graphique, puis exécute des commandes de dessin pour envoyer des données de sommet au pipeline. Les étapes successives du pipeline exécutent des shaders de vertex pour traiter les données de vertex, assembler des vertex en primitives, pixelliser les primitives en fragments, exécuter des shaders de fragment pour calculer les valeurs de couleur et de profondeur pour chaque fragment et mélanger les fragments dans le framebuffer pour l'affichage.

Figure 2    Pipeline graphique OpenGL ES

OpenGL effectue des opérations complexes lorsque les données circulent dans le programme

Utilisez les pipelines comme modèle mental pour déterminer le travail effectué par votre application pour générer de nouveaux frameworks. Votre conception de moteur de rendu consiste à écrire des programmes de shader pour gérer les étapes de sommet et de fragment du pipeline, à organiser les données de sommet et de texture que vous alimentez dans ces programmes et à configurer les machines d'état OpenGL ES qui pilotent les étapes à fonction fixe du pipeline.

Différentes étapes du pipeline graphique peuvent calculer leurs résultats simultanément. Par exemple, votre application peut préparer de nouvelles primitives, tandis que des parties distinctes du matériel graphique effectuent des calculs de sommets et de fragments sur une géométrie précédemment soumise. Cependant, les étapes ultérieures dépendent des sorties des étapes précédentes. Si une étape du pipeline effectue trop de travail ou s'exécute trop lentement, les autres étapes du pipeline resteront inactives jusqu'à ce que l'étape la plus lente termine son travail. Une application bien conçue équilibre le travail effectué par chaque étape du pipeline en fonction des capacités du matériel graphique.

Version OpenGL ES et architecture de rendu

iOS prend en charge trois versions d'OpenGL ES. Les versions plus récentes offrent une plus grande flexibilité, vous permettant d'implémenter des algorithmes de rendu qui incluent des effets visuels de haute qualité sans compromettre les performances.

OpenGL ES 3.0

OpenGL ES 3.0 est nouveau dans iOS 7. Vos applications peuvent utiliser les fonctionnalités introduites dans OpenGL ES 3.0 pour implémenter des techniques de programmation graphique avancées (auparavant uniquement disponibles sur le matériel de bureau et les consoles de jeu) pour des performances graphiques plus rapides et des visuels attrayants.

Ce qui suit met en évidence certaines des fonctionnalités clés d'OpenGL ES 3.0. Pour un aperçu complet, consultez la spécification OpenGL ES 3.0 dans le registre de l'API OpenGL ES .

Langage d'ombrage OpenGL ES Version 3.0

GLSL ES 3.0 ajoute de nouvelles fonctionnalités telles que des blocs unifiés, des entiers 32 bits et une arithmétique entière supplémentaire pour des tâches informatiques plus générales dans les programmes de nuanceur de sommets et de fragments. #version 330 esPour utiliser un nouveau langage dans un programme de shader, votre code source de shader doit commencer par une instruction. Les contextes OpenGL ES 3.0 restent compatibles avec les shaders écrits pour OpenGL ES 2.0.

Plusieurs cibles de rendu

En activant plusieurs cibles de rendu, vous pouvez créer des shaders de fragment qui écrivent simultanément dans plusieurs pièces jointes de framebuffer.

此功能支持使用高级渲染算法,例如延迟着色,您的应用首先渲染到一组纹理以存储几何数据,然后执行一个或多个从这些纹理读取的着色通道并执行光照计算以输出最终结果图片。因为这种方法预先计算了光照计算的输入,所以向场景添加大量光照的增量性能成本要小得多。延迟着色算法需要多个渲染目标支持,如图 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  单缓冲纹理数据

données de tableau de sommets à tampon unique

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

图 8  双缓冲纹理数据

Données de tableau de sommets à double tampon

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

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

注意 OpenGL ES 状态

OpenGL ES 实现维护一组复杂的状态数据,包括您使用glEnableglDisable函数设置的开关、当前着色器程序及其统一变量、当前绑定的纹理单元以及当前绑定的顶点缓冲区及其启用的顶点属性。硬件有一个当前状态,该状态被延迟编译和缓存。切换状态的成本很高,因此最好设计您的应用程序以尽量减少状态切换。

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

Évitez de définir un état inutile en utilisant une routine de configuration ou d'arrêt dédiée au lieu de placer de tels appels dans la boucle de dessin. Les routines de configuration et d'arrêt sont également utiles pour activer et désactiver des fonctionnalités qui permettent d'obtenir des effets visuels spécifiques - par exemple, lors du dessin de contours filaires autour de polygones texturés.

Encapsuler l'état avec des objets OpenGL ES

Pour réduire les changements d'état, créez des objets qui collectent plusieurs changements d'état OpenGL ES dans des objets pouvant être liés à un seul appel de fonction. Par exemple, un objet tableau de vertex stocke la configuration de plusieurs attributs de vertex dans un seul objet. Reportez-vous à la section Fusion des changements d'état de Vertex Array à l'aide d'objets Vertex Array .

Organisez des tirages au sort pour minimiser les changements d'état

Changer l'état d'OpenGL ES n'a pas d'effet immédiat. Au lieu de cela, lorsque vous émettez une commande de dessin, OpenGL ES effectue le travail requis pour dessiner en utilisant un ensemble de valeurs d'état. Vous pouvez réduire le temps CPU consacré à la reconfiguration du pipeline graphique en minimisant les changements d'état. Par exemple, conservez un vecteur d'état dans votre application et ne définissez l'état OpenGL ES correspondant que lorsque votre état change entre les appels de dessin. Un autre algorithme utile est l'ordre d'état - gardez une trace des opérations de dessin que vous devez effectuer et de la quantité de changement d'état requise par chaque opération, puis ordonnez-leur d'effectuer des opérations consécutives en utilisant le même état.

L'implémentation iOS d'OpenGL ES peut mettre en cache certaines des données de configuration dont elle a besoin pour une commutation efficace entre les états, mais la configuration initiale de chaque ensemble unique d'états prend plus de temps. Pour des performances constantes, vous pouvez "préchauffer" chaque ensemble d'états que vous prévoyez d'utiliser dans votre routine de configuration :

  1. Activez la configuration d'état ou le shader que vous prévoyez d'utiliser.
  2. Utilisez cette configuration d'état pour dessiner un petit nombre de sommets.
  3. Vide le contexte OpenGL ES afin qu'aucun dessin ne soit affiché pendant la phase de préchauffage.

Guess you like

Origin juejin.im/post/7120058470744719390