OpenGL 优化 - 重复顶点流或重复调用 glDrawElements?

Posted

技术标签:

【中文标题】OpenGL 优化 - 重复顶点流或重复调用 glDrawElements?【英文标题】:OpenGL Optimization - Duplicate Vertex Stream or Call glDrawElements Repeatedly? 【发布时间】:2013-08-07 20:26:47 【问题描述】:

这是针对 android 上的 OpenGL ES 2.0 游戏,但我怀疑正确答案适用于任何 opengl 情况。

TL;DR - 将 N 个数据发送到 GPU 一次然后用它进行 K 个绘图调用是否更好;还是将 K*N 数据发送到 gpu 一次,然后进行 1 次绘制调用?

更多详情 我想知道适合我的情况的最佳做法。我有一个动态网格,我会重新计算每一帧的顶点——把它想象成一个水面——我需要在我的游戏中将这些顶点投影到 K 个不同的四边形上。 (在每种情况下,投影都略有不同;保留细节,您可以将它们想象为围绕网格的 K 个不同的镜子。)K 大约为 10-25;我还在想办法。

我可以想到两种广泛的选择:

    按原样绑定网格,并调用 draw K 不同时间,要么 更改着色器的制服或弄乱固定功能 状态渲染到正确的四边形(在屏幕上)或不同的 纹理的片段(我稍后可以在渲染四边形时使用它来实现 效果一样)。

    将网格中的所有顶点复制 K 次,本质上是制作一个 包含 K 个网格的单个顶点流,并添加一个属性(或 少数)指示每个网格克隆应该投影哪个四边形 到(以及如何到达那里),并使用顶点着色器进行投影。一世 会调用一次绘图,但发送 K 倍的数据。

问题:在这两个选项中,通常哪个更好?

(另外:有没有更好的方法来做到这一点?

我考虑了第三种选择,我将网格细节渲染到纹理,并将我的 K 克隆几何体创建为一种虚拟流,我可以一劳永逸地绑定它,在顶点着色器中查找进入每个顶点的纹理,找出它真正代表的顶点;但有人告诉我,顶点着色器中的纹理支持很差或在 OpenGL ES 2.0 中被禁止,因此我宁愿避免使用这种方法。)

【问题讨论】:

【参考方案1】:

这个问题没有完美的答案,但我建议您考虑一下实时计算机图形和 OpenGL 管道的本质。尽管需要“GL”来产生与按顺序执行一致的结果,但现实情况是 GPU 是高度并行的野兽。如果您实际上同时进行许多不相关的任务(有些甚至将整个管道分成离散的图块),它们会采用许多最有效的技巧。例如,GDDR 内存的延迟非常高,因此为了提高效率,GPU 需要能够调度其他作业,以在为刚刚开始的作业获取内存时保持流处理器(着色器单元)忙碌。

如果您在每帧重新计算网格的某些部分,那么您几乎肯定会希望更多的绘制调用而不是大量的 CPU->GPU 数据每帧传输。用不必要的数据传输使总线饱和甚至会困扰 PCI Express 硬件(它比几个额外的绘制调用所增加的开销要慢得多),它只会在嵌入式 OpenGL ES 系统上变得更糟。话虽如此,您没有理由不能简单地使用glBufferSubData (...) 仅在网格中受影响的部分进行流式传输,并在一次绘制调用中继续绘制整个网格。

如果您拆分(或分区内的数据)缓冲区和/或绘制调用,您可能会获得更好的缓存一致性,具体取决于您的实际用例场景。确定哪种方法更适合您的情况的唯一方法是在目标硬件上分析您的软件。但所有这些都没有从大局出发,即:“我为什么要在 CPU 上这样做?!”


听起来您真正想要的只是顶点实例化。如果您可以通过传递实例 ID 重新设计您的算法以在顶点着色器中完全工作,那么您应该会看到对我迄今为止提出的所有解决方案的巨大改进(真正的实例化实际上介于您在解决方案 1 和 2 中描述的内容):)

实例化的实际概念非常简单,无论您的 OpenGL API 的特定版本是否在 API 级别支持它,都会给您带来好处(您始终可以使用顶点属性和额外的顶点缓冲区数据手动实现它)。问题是,如果您正确实施实例化,您根本不必复制数据。识别每个单独顶点所需的额外数据是静态的,您始终可以更改着色器统一并进行额外的绘制调用(这可能是您对 OpenGL ES 2.0 所做的,因为它不提供glDrawElementsInstanced)不接触任何顶点数据。

您当然不必复制顶点 K*N 次,您的缓冲区空间复杂度将更像 O (K + K*M),其中 M 是您必须添加的新组件的数量,以唯一标识每个顶点,以便您可以在 GPU 上计算所有内容。对于“实例”,您可能需要对四边形 1-4 中的每个顶点进行编号,并根据您正在处理的顶点在着色器中以不同方式处理顶点。在这种情况下,M 系数为 1,无论您需要动态计算每一帧的四边形实例有多少,它都不会改变; N 将确定 OpenGL ES 2.0 中的绘制调用次数,而不是数据的大小。如果 OpenGL ES 2.0 支持 gl_VertexID ,则不需要这些额外的存储空间:(

实例化是有效利用高度并行 GPU 并避免 CPU/GPU 同步和缓慢的总线传输的最佳方式。尽管 OpenGL ES 2.0 不支持 API 意义上的实例化,但使用相同顶点缓冲区的多个绘制调用(其中您在调用之间唯一更改的是几个着色器统一)通常比在 CPU 上计算顶点并上传新顶点更可取每帧数据或顶点缓冲区的大小直接取决于您打算绘制的实例数(yuck)。您必须尝试一下,看看您的硬件喜欢什么。

【讨论】:

【参考方案2】:

实例化将是您正在寻找的,但不幸的是它不适用于 OpenGL ES 2.0。如果您的所有资产都适合 GPU,我会赞成将所有顶点发送到 GPU 并进行一次绘制调用。我有将绘图调用从 100+ 减少到 1 的经验,性能从 15 fps 降低到 60 fps。

【讨论】:

对于像我一样思考的其他人:暂时我很兴奋,我可能只在索引缓冲区中使用 gl_VertexId 和复制(只能绑定一次)来派生一种被黑的实例 id在我的着色器中,但***.com/questions/10044185/… 澄清了 gl_VertixId 仅在 opengl-es-3.0+ 中可用:( 是的,我在回答中提到了这一点 :) 这确实很不幸,但好消息是,draw call 的费用并不总是你想的那样。在 Direct3D 中,最小化绘图调用很重要,因为每次进行绘图调用时,都必须从用户模式切换到内核模式,这会调用冗长的上下文切换。自然地,D3D 早在 OpenGL 之前就采用了实例化 :) 在 OpenGL 世界中,绘制调用的部分费用实际上是延迟状态设置——在最后一次调用和当前调用之间排队的所有状态和命令构成了大部分费用. 两个连续的绘图调用,使用相同的顶点和索引缓冲区,您更改的唯一状态是着色器状态实际上可能非常便宜。因此,在将绘图调用定型为性能瓶颈之前,您真的必须尝试一下。

以上是关于OpenGL 优化 - 重复顶点流或重复调用 glDrawElements?的主要内容,如果未能解决你的问题,请参考以下文章

在 OpenGL ES 2 中更新 VBO 的顶点

使用OpenGL和GLFW的简单三角形[重复]

OpenGL 4.5 直接状态访问渲染一个三角形 - GL_INVALID_VALUE [重复]

OpenGl,glUseProgram() 返回 GL_INVALID_VALUE 即使着色器的 ID 是正确的 [重复]

配置VAO和VBO时,顶点数据数组是不是应该使用相同的方法? (OpenGL)[重复]

如何频繁更新顶点缓冲区数据(每帧)opengl [重复]