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

Posted

技术标签:

【中文标题】如何提高自定义 OpenGL ES 2.0 深度纹理生成的性能?【英文标题】:How can I improve the performance of my custom OpenGL ES 2.0 depth texture generation? 【发布时间】:2011-08-28 10:01:57 【问题描述】:

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

不幸的是,OpenGL ES 2.0 does not let you write to gl_FragDepth,所以我需要将这些值输出到自定义深度纹理。我使用帧缓冲区对象 (FBO) 对场景进行传递,仅渲染出与深度值相对应的颜色,并将结果存储到纹理中。然后将此纹理加载到我的渲染过程的后半部分,生成实际的屏幕图像。如果该阶段的片段处于屏幕上该点的深度纹理中存储的深度级别,则会显示该片段。如果没有,它就被扔掉了。有关该过程的更多信息,包括图表,可以在我的帖子here 中找到。

这种深度纹理的生成是我渲染过程中的一个瓶颈,我正在寻找一种方法让它更快。它似乎比它应该的要慢,但我不知道为什么。为了正确生成此深度纹理​​,GL_DEPTH_TEST 被禁用,GL_BLEND 使用glBlendFunc(GL_ONE, GL_ONE) 启用,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 上,使用直通着色器渲染 DNA 空间填充模型的帧以进行显示需要 35 到 68 毫秒(在 iPhone 4 上为 18 到 35 毫秒)。根据 PowerVR PVRUniSCo 编译器(their SDK 的一部分),此着色器最多使用 11 个 GPU 周期,最差使用 16 个周期。我知道建议您不要在着色器中使用分支,但在这种情况下,这会带来更好的性能。

当我把它简化为

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 毫秒,但在 iPhone 4 上只需要 1.7 - 2.4 毫秒。此着色器的估计 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 之间第二个着色器的突然变化让我觉得我在这里缺少一些东西。如果您想自己尝试一下,可以从here 下载包含这三个着色器变体的完整源项目(查看 SphereDepth.fsh 文件并注释掉相应的部分)和测试模型。

如果您已经阅读到这里,我的问题是:基于此分析信息,我如何提高我的自定义深度着色器在 iOS 设备上的渲染性能?

【问题讨论】:

有关于着色器条件的帖子。您必须避免着色器中的条件 @Joe - 整体上很难进行基准测试,因为它比上述设备快得多,所以无论如何它都会以 60 FPS 的速度渲染测试模型。我扔给它的任何模型都很少遇到问题,因此我将精力集中在速度较慢的设备上。 我在阅读这个问答时学到了很多东西。感谢您跟进并彻底了解代码和图表。 (赞成) 【参考方案1】:

根据 Tommy、Pivo​​t 和 rotoglup 的建议,我实施了一些优化,从而使应用程序中的深度纹理生成和整体渲染管道的渲染速度提高了一倍。

首先,我重新启用了之前使用的预先计算的球体深度和光照纹理,但效果不大,只是现在我在处理来自该纹理的颜色和其他值时使用了正确的lowp 精度值。这种组合,加上适当的纹理贴图,似乎可以产生约 10% 的性能提升。

更重要的是,我现在在渲染我的深度纹理和最终的光线跟踪冒名顶替者之前进行一次传递,在其中我放置了一些不透明的几何图形来阻止永远不会被渲染的像素。为此,我启用了深度测试,然后使用简单的不透明着色器绘制出构成场景中对象的正方形,并按 sqrt(2) / 2 缩小。这将创建嵌入的正方形,覆盖已知球体中不透明的区域。

然后我使用glDepthMask(GL_FALSE) 禁用深度写入,并在距离用户一个半径的位置渲染方形球体冒名顶替。这使得 iOS 设备中基于 tile 的延迟渲染硬件能够有效地去除在任何条件下都不会出现在屏幕上的片段,但仍然基于每像素深度值在可见球体冒名顶替者之间提供平滑的交叉点。我在下面的粗略插图中对此进行了描述:

在此示例中,顶部两个冒名顶替者的不透明阻挡方块不会阻止来自这些可见对象的任何片段被渲染,但它们会阻止来自最低冒名顶替者的大部分片段。然后,最前面的冒名顶替者可以使用逐像素测试来生成平滑的交叉点,而来自后面的冒名顶替者的许多像素不会因为渲染而浪费 GPU 周期。

我没想过要禁用深度写入,但在最后一个渲染阶段进行深度测试。这是防止冒名顶替者简单地相互叠加,但仍在使用 PowerVR GPU 中的一些硬件优化的关键。

在我的基准测试中,渲染我上面使用的测试模型每帧产生 18 到 35 毫秒的时间,而我之前得到的时间是 35 到 68 毫秒,渲染速度几乎翻了一番。将这种相同的不透明几何体预渲染应用到光线跟踪通道可以使整体渲染性能提高一倍。

奇怪的是,当我尝试通过使用嵌入和外接八边形来进一步改进这一点时,在绘制时应该减少约 17% 的像素,并且更有效地阻止片段,性能实际上比使用简单正方形时更差。在最坏的情况下,Tiler 利用率仍然低于 60%,因此可能更大的几何图形会导致更多的缓存未命中。

编辑(2011 年 5 月 31 日):

根据 Pivot 的建议,我创建了内接和外接八边形来代替我的矩形,只是我遵循了 here 的建议来优化三角形以进行光栅化。在之前的测试中,八边形的性能比正方形更差,尽管它可以去除许多不必要的碎片并让您更有效地阻挡覆盖的碎片。通过调整三角形绘制如下:

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

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

我重新审视了 Tommy 使用 step 函数的想法,现在由于八边形,我可以丢弃的片段更少。这与球体的深度查找纹理相结合,现在在 iPad 1 上为我的测试模型生成深度纹理的平均渲染时间为 2 毫秒。在这个渲染案例中,我认为这与我希望的一样好,并且从我开始的地方有了巨大的改进。对于后代,这是我现在使用的深度着色器:

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

我已经更新了测试样本here,如果您希望看到这种新方法与我最初所做的相比正在发挥作用。

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

【讨论】:

【参考方案2】:

在桌面上,许多早期的可编程设备都是这样,虽然它们可以同时处理 8 个或 16 个或任何片段,但实际上它们只有一个程序计数器来处理其中的许多片段(因为这也意味着只有一个 fetch/解码单元和其他一切之一,只要它们以 8 或 16 像素为单位工作)。因此,最初禁止条件语句,并且在此之后的一段时间内,如果对将一起处理的像素的条件评估返回不同的值,则这些像素将以某种排列方式在更小的组中进行处理。

虽然 PowerVR 并不明确,但他们的 application development recommendations 有一个关于流控制的部分,并提出了很多关于动态分支的建议,通常只有在结果可以合理预测的情况下才是一个好主意,这让我觉得他们正在在同一件事上。因此,我建议速度差异可能是因为您包含了条件。

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

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


【讨论】:

我需要调整着色器代码以使其正确渲染(负平方根导致片段在圆外变黑),我稍微优化了排序,所以我编辑了你的着色器以反映我最终尝试的内容。这一步是一个非常好的主意,但不幸的是,在 iPad 1 上,它导致每帧 51 - 69 毫秒(在 iPhone 4 上为 18 - 35 毫秒),因此它在实际设备上表现中性或倒退了一步。根据编译器的说法,上面的代码在最好和最坏的情况下使用了大约 20 个 GPU 周期。不过值得一试。 我的想法不多了,但有没有什么办法可以将几何图形分成绝对不透明的部分和可能不透明的部分?您可以使用传统的 z 缓冲区从前向后绘制(ish)并希望节省一些片段成本,然后在禁用深度缓冲区的情况下以任何旧顺序绘制其他的,仍然可以获得正确的颜色结果稍后进入另一个深度缓冲区。 然后分两遍做? Pass 1:将绝对不透明的几何体放置在球体可以到达的最近位置,在启用颜色写入、启用深度读取但禁用深度写入的情况下绘制它。 Pass 2:将几何体推回到球体可以到达的最远位置,禁用颜色写入(如果片段成本仍然累积,则设置统一以移动到简化的着色器路径),启用深度读取和写入。 实际上,分两遍完成场景可能更聪明。 Pass 1:没有颜色写入,深度缓冲区读取和写入,绝对不透明的部分,每个 GL 几何深度是球体可以到达的最远。 Pass 2:颜色写入,深度缓冲区只读,所有部分,每个 GL 几何深度都是球体可以接近的。 另外,在实施了 Pivot 对此几何图形的光栅优化八边形的建议之后,我重新访问了您的阶跃函数着色器。结合深度纹理查找,使用 step 函数能够将我的渲染时间从大约 19 毫秒降低到每个深度纹理帧的 2.4 毫秒。通过删除分支减少开销而受益的片段数量现在必须大大超过可能提前中止处理的片段。【参考方案3】:

其中许多点已被其他发布答案的人所涵盖,但这里的首要主题是您的渲染做了很多将被丢弃的工作:

    着色器本身做了一些潜在的冗余工作。向量的长度很可能被计算为sqrt(dot(vector, vector))。无论如何,您不需要 sqrt 来拒绝圆外的片段,并且您正在平方长度来计算深度。此外,您是否查看过深度值的显式量化是否实际上是必要的,或者您是否可以仅使用硬件从浮点到整数的转换用于帧缓冲区(可能带有额外的偏差以确保您的准- 深度测试稍后会出来)?

    许多片段微不足道地位于圆圈之外。 您绘制的四边形区域中只有 π/4 会产生有用的深度值。在这一点上,我想您的应用程序严重偏向于片段处理,因此您可能需要考虑增加您绘制的顶点数量以换取您必须着色的区域的减少。由于您是通过正交投影绘制球体,因此任何外接正多边形都可以,但您可能需要根据缩放级别增加一点额外尺寸,以确保栅格化足够多的像素。

    许多片段被其他片段轻微遮挡。 正如其他人指出的那样,您没有使用硬件深度测试,因此没有充分利用 TBDR 消除阴影的能力早点工作。如果你已经为 2) 实现了一些东西,你需要做的就是在你可以生成的最大深度处绘制一个内接正多边形(一个穿过球体中间的平面),然后在最小深度处绘制你的真实多边形(球体的前面)。 Tommy 和 rotoglup 的帖子都已经包含状态向量细节。

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

【讨论】:

感谢您的提示。我最终给了汤米赏金,因为他对预渲染深度写入通道的明确描述正是我最终所做的(更多信息请参见我的答案)。此外,我确实尝试过为我的冒名顶替者使用插图和外接八边形,但它们导致整体性能低于简单的正方形。我仍在调查原因,因为通过消除约 17% 的要绘制的片段,这似乎是一个明显的胜利。 我还没有机会在实际设备上玩这个,但我从发布的消息来源看到你正在使用围绕中心点的风扇进行三角测量,即suboptimal for rasterization。我很想知道链接中描述的三角测量方案是否会更好,因为这相当于从你的内接正方形开始并添加 4 个额外的块。 很棒的收获。这似乎是这里的确切情况,因为当我将三角形绘制顺序(如我更新的答案所示)从居中的扇形更改为更“贪婪”的版本时,八边形从比正方形慢到快 14%。我不知道这会对光栅化产生如此大的影响,所以感谢您指出。 @Brad Larson:这是个好消息。用普通的八角形而不是现在的稍微不均匀的八角形,你还能剃掉更多的面积吗? 除非我搞砸了我的计算,否则我会在我的代码中使用正八边形。我的答案中的绘图只是我在 OmniGraffle 中拼凑起来的一个快速而肮脏的东西,所以不要按它的尺寸来衡量。【参考方案4】:

我根本不是移动平台专家,但我认为让你感到困扰的是:

你的深度着色器相当昂贵 在您禁用 GL_DEPTH 测试时在深度通道中体验到大量透支

在深度测试之前绘制一个额外的通道不是有帮助吗?

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

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

此解决方案将涉及发出另一组绘图调用,因此这可能无益。

【讨论】:

以上是关于如何提高自定义 OpenGL ES 2.0 深度纹理生成的性能?的主要内容,如果未能解决你的问题,请参考以下文章

Android Opengl ES 2.0 FrameBuffer 不工作

哪些 OpenGL ES 2.0 纹理格式可进行颜色、深度或模板渲染?

OpenGL ES 2.0 雾

OPENGL ES 2.0 知识串讲 ——GLSL 语法(IV)

OPENGL ES 2.0 知识串讲 ——GLSL 语法(IV)

在 iOS 上的 OpenGL ES 着色器中混合多个纹理会导致反向行为