如何提高自定义OpenGL ES 2.0深度纹理生成的性能?

我有一个开源的iOS应用程序,它使用自定义的OpenGL ES 2.0着色器来显示分子结构的三维表示。 它通过使用在矩形上绘制的程序生成的球体和圆柱体冒名顶替,而不是使用大量顶点构build的这些相同的形状。 这种方法的缺点是这些冒充者对象的每个片段的深度值需要在片段着色器中计算,以便在对象重叠时使用。

不幸的是,OpenGL ES 2.0 不会让你写入gl_FragDepth ,所以我需要输出这些值到一个自定义的深度纹理。 我使用framebuffer对象(FBO)对场景进行遍历,只渲染与深度值对应的颜色,并将结果存储到纹理中。 然后这个纹理加载到我的渲染过程的后半部分,在那里生成实际的屏幕图像。 如果该阶段的某个片段处于屏幕上该点的深度纹理中存储的深度级别,则会显示该片段。 如果不是,就扔掉。 有关过程的更多信息,包括图表,可以在我的文章中find 。

这种深度纹理的产生是我渲染过程中的一个瓶颈,我正在寻找一种使其更快的方法。 它似乎比应该慢,但我不明白为什么。 为了实现这种深度纹理的正确生成,禁用GL_DEPTH_TEST ,使用glBlendFunc(GL_ONE, GL_ONE)启用glBlendEquation() ,并将glBlendEquation()设置为GL_MIN_EXT 。 我知道以这种方式输出的场景并不是像iOS设备中的PowerVR系列那样基于瓦片的延迟渲染器上最快的,但我想不出一个更好的方法来做到这一点。

我的深度片段着色器(最常见的显示元素)看起来是这个瓶颈的核心(仪器中的渲染器利用率固定在99%,表明我受限于片段处理)。 目前看起来如下所示:

 precision mediump float; varying mediump vec2 impostorSpaceCoordinate; varying mediump float normalizedDepth; varying mediump float adjustedSphereRadius; const vec3 stepValues = vec3(2.0, 1.0, 0.0); const float scaleDownFactor = 1.0 / 255.0; void main() { float distanceFromCenter = length(impostorSpaceCoordinate); if (distanceFromCenter > 1.0) { gl_FragColor = vec4(1.0); } else { float calculatedDepth = sqrt(1.0 - distanceFromCenter * distanceFromCenter); mediump float currentDepthValue = normalizedDepth - adjustedSphereRadius * calculatedDepth; // Inlined color encoding for the depth values float ceiledValue = ceil(currentDepthValue * 765.0); vec3 intDepthValue = (vec3(ceiledValue) * scaleDownFactor) - stepValues; gl_FragColor = vec4(intDepthValue, 1.0); } } 

在iPad 1上,这需要35 – 68毫秒的时间才能使用直通着色器显示DNA空间填充模型(iPhone 4上的18到35毫秒)。 根据PowerVR PVRUniSCo编译器( 其SDK的一部分),该着色器最多使用11个GPU周期,最坏16个周期。 我知道build议您不要在着色器中使用分支,但是在这种情况下,导致更好的性能。

当我简化到

 precision mediump float; varying mediump vec2 impostorSpaceCoordinate; varying mediump float normalizedDepth; varying mediump float adjustedSphereRadius; void main() { gl_FragColor = vec4(adjustedSphereRadius * normalizedDepth * (impostorSpaceCoordinate + 1.0) / 2.0, normalizedDepth, 1.0); } 

在iPad 1上需要18 – 35 ms,但iPhone 4上只需要1.7 – 2.4 ms。该着色器估计的GPU周期数为8个周期。 渲染时间基于循环次数的变化似乎不是线性的。

最后,如果我只输出一个常量的颜色:

 precision mediump float; void main() { gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0); } 

渲染时间在iPad 1上下降到1.1 – 2.3毫秒(iPhone 4上为1.3毫秒)。

渲染时间的非线性缩放以及iPad和iPhone 4之间对于第二个着色器的突然变化使得我认为这里有一些我不在这里的东西。 如果您想自己尝试,可以从这里下载包含这三种着色器变体(查看SphereDepth.fsh文件并注释掉相应部分)和testing模型的完整源代码项目。

如果你已经阅读了这个,我的问题是:基于这个分析信息,我怎样才能提高iOS设备上自定义深度着色器的渲染性能?

根据Tommy,Pivot和rotoglup的build议,我已经实现了一些优化,使得应用程序中深度纹理生成和整个渲染pipe道的渲染速度提高了一倍。

首先,我重新启用了之前使用过的预先计算的球体深度和光照纹理,但是现在只有在处理来自该纹理的颜色和其他值时才使用适当的lowp精度值。 这种组合,以及纹理适当的mipmapping,似乎产生了〜10%的性能提升。

更重要的是,我现在在渲染我的深度纹理和最终的光线追踪的仿冒者之前做了一个通行证,在那里我放下了一些不透明的几何graphics来阻止永远不会渲染的像素。 为此,我启用深度testing,然后绘制出构成场景中对象的正方形,用sqrt(2)/ 2缩小,并使用一个简单的不透明着色器。 这将创build覆盖区域中的插图广场,已知在所代表的球体中是不透明的。

然后,我使用glDepthMask(GL_FALSE)禁用深度写入,并将方形球体冒名顶替者放置在距离用户更近的位置。 这允许iOS设备中基于瓦片的延迟渲染硬件有效地去除在任何情况下永远不会出现在屏幕上的碎片,但仍基于每像素深度值给出可见球体冒充者之间的平滑交集。 这在我的粗略例子中描述如下:

分层球体和不透明度测试

在这个例子中,顶端两个冒名顶尖的不透明阻塞方块不会阻止来自这些可见对象的任何片段被渲染,但是它们会阻塞来自最低冒名顶替者的一个片段。 然后,最前面的骗子可以使用每像素testing来生成平滑的交叉点,而来自后端冒充者的许多像素不会通过渲染浪费GPU周期。

我没有想过要禁用深度写入,但在进行最后一个渲染阶段时,请不要进行深度testing。 这是防止盗版者简单堆叠在一起的关键,但仍然使用PowerVR GPU中的一些硬件优化。

在我的基准testing中,渲染我上面使用的testing模型,每帧的渲染时间为18 – 35毫秒,而我之前得到的35 – 68毫秒,渲染速度几乎翻了一番。 将这种不透明的几何预渲染应用于光线追踪通道会使整体渲染性能提高一倍。

奇怪的是,当我试图通过使用插入和外接的八边形来进一步优化这个时,它应该在绘制时减less约17%的像素,并且在阻塞碎片时效率更高,性能实际上比使用简单方块更差。 在最坏的情况下,砖瓦的利用率还不到60%,所以也许更大的几何尺寸会导致更多的caching未命中。

编辑(5/31/2011):

根据Pivot的build议,我创build了刻写和限定的八angular形来代替我的矩形,只有我按照这里的build议来优化用于光栅化的三angular形。 在之前的testing中,尽pipe删除了许多不必要的片段,并且让您更有效地封闭了覆盖的片段,但八angular形比正方形产生的性能更差。 通过调整三angular形图如下:

栅格化优化八角形

在上述优化的基础上,通过从正方形切换成八angular形,我能够将整体渲染时间平均减less14%。 现在生成深度纹理的时间为19 ms,偶尔降低到2 ms,尖峰到35 ms。

编辑2(2011年5月31日):

我重新考虑了Tommy使用step函数的想法,现在由于八angular形的原因我放弃了更less的碎片。 这与球体的深度查找纹理相结合,现在可以在iPad 1上产生2 ms的平均渲染时间,用于testing模型的深度纹理生成。 我认为在这个渲染案例中,我希望能达到我所希望的那么好,而且从我开始的地方来看,这是一个巨大的进步。 对于后人,这里是我正在使用的深度着色器:

 precision mediump float; varying mediump vec2 impostorSpaceCoordinate; varying mediump float normalizedDepth; varying mediump float adjustedSphereRadius; varying mediump vec2 depthLookupCoordinate; uniform lowp sampler2D sphereDepthMap; const lowp vec3 stepValues = vec3(2.0, 1.0, 0.0); void main() { lowp vec2 precalculatedDepthAndAlpha = texture2D(sphereDepthMap, depthLookupCoordinate).ra; float inCircleMultiplier = step(0.5, precalculatedDepthAndAlpha.g); float currentDepthValue = normalizedDepth + adjustedSphereRadius - adjustedSphereRadius * precalculatedDepthAndAlpha.r; // Inlined color encoding for the depth values currentDepthValue = currentDepthValue * 3.0; lowp vec3 intDepthValue = vec3(currentDepthValue) - stepValues; gl_FragColor = vec4(1.0 - inCircleMultiplier) + vec4(intDepthValue, inCircleMultiplier); } 

我已经在这里更新了testing样例,如果您希望看到这个新的方法在行动中,与我最初的做法相比。

我仍然接受其他build议,但是这对于这个应用程序来说是一个巨大的进步。

在桌面上,许多早期的可编程设备就是这样,虽然他们可以同时处理8个或16个或其他任何碎片,但是它们实际上只有一个程序计数器(因为这也意味着只有一个读取/解码单元和其他任何东西,只要他们在8或16像素的单位工作)。 因此,对条件的最初禁止以及之后的一段时间,如果对将被一起处理的像素的条件评估返回不同的值,那么这些像素将在一些布置中以较小的组进行处理。

尽pipePowerVR并不是显式的,但是他们的应用程序开发build议有一个关于stream程控制的章节,并且提出了关于dynamic分支的许多build议,只有在结果合理可预测的情况下通常才是一个好主意,这使我认为他们得到了相同的结果那类的东西。 因此,我build议速度差异可能是因为你已经包含了条件。

作为第一个testing,如果您尝试以下操作会发生什么?

 void main() { float distanceFromCenter = length(impostorSpaceCoordinate); // the step function doesn't count as a conditional float inCircleMultiplier = step(distanceFromCenter, 1.0); float calculatedDepth = sqrt(1.0 - distanceFromCenter * distanceFromCenter * inCircleMultiplier); mediump float currentDepthValue = normalizedDepth - adjustedSphereRadius * calculatedDepth; // Inlined color encoding for the depth values float ceiledValue = ceil(currentDepthValue * 765.0) * inCircleMultiplier; vec3 intDepthValue = (vec3(ceiledValue) * scaleDownFactor) - (stepValues * inCircleMultiplier); // use the result of the step to combine results gl_FragColor = vec4(1.0 - inCircleMultiplier) + vec4(intDepthValue, inCircleMultiplier); } 

其中许多点已经被其他已经发布答案的人所覆盖,但是这里的主题是你的渲染会做很多工作,而这些工作将会被抛弃:

  1. 着色器本身做了一些潜在的重复工作。 vector的长度很可能按sqrt(dot(vector, vector)) 。 无论如何,您不需要sqrt就可以拒绝圆圈外的碎片,而且您还可以计算深度。 另外,你是否研究了深度值的显式量化是否真的是必要的,或者你是否可以避免使用硬件从帧缓冲到浮点数的转换(可能带来额外的偏差,以确保你的准深度testing出来以后)?

  2. 在圈子外面,很多碎片是平凡的。 你正在绘制的四边形区域只有π/ 4产生有用的深度值。 在这一点上,我认为你的应用程序严重偏向碎片处理,所以你可能要考虑增加你绘制的顶点的数量,以换取减less面积的阴影。 由于您是通过正交投影绘制球体,所以任何外接正多边形都可以,尽pipe您可能需要一些额外的大小,具体取决于缩放级别,以确保栅格化足够的像素。

  3. 许多碎片被其他碎片平凡地遮挡住。 正如其他人所指出的那样,你并没有使用硬件深度testing,因此也没有充分利用TBDR的能力来尽早地完成阴影工作。 如果你已经实现了2)的东西,你只需要在可以生成的最大深度(通过球体中间的一个平面)上绘制一个规则的多边形,然后在最小深度绘制你的实际多边形(球体的前面)。 Tommy和rotoglup的post都已经包含状态向量细节。

请注意,2)和3)也适用于您的光线追踪着色器。

我根本就不是移动平台的专家,但是我觉得有什么可以咬你的是:

  • 你的深度着色器是相当昂贵的
  • 当你禁用GL_DEPTHtesting时,在深度传递中遇到大量透支

在深度testing之前不会有额外的通行证有帮助吗?

这个过程可以进行GL_DEPTH填充,例如通过画出表示为面向四面体的相机(或者可能更容易设置的立方体)的每个球体并且包含在相关联的球体中。 可以在没有颜色遮罩或片段着色器的情况下绘制该通道,只需启用GL_DEPTH_TESTglDepthMask 。 在桌面平台上,这些通行证的绘制速度比颜色+深度通过要快。

然后在你的深度计算过程中,你可以启用GL_DEPTH_TEST并禁用glDepthMask ,这样你的着色器就不会在靠近几何graphics的像素上被执行。

这个解决scheme将涉及发出另一组平局呼叫,所以这可能没有好处。