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

41
我有一个开源的iOS应用程序,使用自定义的OpenGL ES 2.0着色器显示分子结构的3D表示。它通过在矩形上绘制过程生成的球体和圆柱体冒充物来实现,而不是使用大量顶点构建这些相同的形状。这种方法的缺点是需要在片段着色器中计算这些冒充物对象的每个片段的深度值,以便在对象重叠时使用。
不幸的是,OpenGL ES 2.0 不允许你写入gl_FragDepth,因此我需要将这些值输出到自定义深度纹理中。我使用帧缓冲对象(FBO)对场景进行一次遍历,仅渲染与深度值相对应的颜色,并将结果存储到纹理中。然后将该纹理加载到我的渲染过程的第二部分中,生成实际的屏幕图像。如果该阶段的片段处于存储在该点屏幕上的深度纹理中的深度级别,则会显示它。否则,就会被丢弃。更多关于该过程的信息,包括图表,可以在我的帖子这里中找到。
这个深度纹理的生成过程是我渲染流程中的瓶颈,我正在寻找一种方法使它更快。似乎比它应该的要慢,但我无法想出原因。为了实现深度纹理的正确生成,禁用了GL_DEPTH_TEST,启用了GL_BLENDglBlendFunc(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编译器(他们的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设备上自定义深度着色器的渲染性能?


有关着色器中的条件问题已经发布了帖子。您必须避免在着色器中使用条件。 - Yuriy Vikulov
@Joe - 总体来说很难进行基准测试,因为它比上述设备要快得多,所以无论如何都能以60 FPS渲染测试模型。它很少遇到我投入其中的任何模型的问题,因此我正在将精力集中在速度较慢的设备上。 - Brad Larson
我在阅读这个问答过程中学到了不少。感谢您的跟进,并且对代码和图表进行了详细的说明。(已点赞) - mattorb
4个回答

21

根据Tommy、Pivot和rotoglup的建议,我实现了一些优化,使应用程序中深度纹理生成和整体渲染管线的渲染速度翻倍。

首先,我重新启用了之前使用过但效果不佳的预计算球体深度和光照纹理,现在我使用适当的lowp精度值来处理该纹理中的颜色和其他值。这种组合,再加上纹理的适当mipmapping,似乎可以提高约10%的性能。

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

然后,我使用glDepthMask(GL_FALSE)禁用深度写入,并在靠近用户一个半径的位置渲染正方形球体拟像。这使得iOS设备中的基于平铺的延迟渲染硬件能够有效地剥离永远不会在任何情况下出现在屏幕上的片段,但仍然根据每个像素的深度值为可见的球体拟像之间提供平滑的交集。这在我的粗略插图中显示如下:

Layered spheres and opacity testing

在这个例子中,最上面两个impostors的不透明阻塞方块并没有防止那些可见物体的任何片段被渲染,但是它们却阻塞了最低impostor的一部分片段。然后,最前面的impostors可以使用每像素测试来生成平滑的交集,而许多来自后面impostor的像素则不会浪费GPU周期而被渲染。
我之前没有想到禁用深度写入,但在进行最后的渲染阶段时保留深度测试。这是防止impostors简单堆叠在一起,同时仍然使用PowerVR GPU内部某些硬件优化的关键。
在我的基准测试中,渲染上面使用的测试模型每帧需要18-35毫秒的时间,而之前我得到的是35-68毫秒,近乎翻倍的渲染速度。将相同的不透明几何图形预先渲染到光线追踪通道中,整体渲染性能翻倍。 奇怪的是,当我尝试通过使用嵌入和外接八边形进一步改进时,这些八边形在绘制时应该覆盖约17%的像素,并且在阻塞片段方面更有效,但性能实际上比使用简单的正方形更差。在最坏的情况下,平铺器利用率仍然不到60%,因此可能是较大的几何图形导致了更多的缓存未命中。 编辑(2011年5月31日):

根据Pivot的建议,我创建了内切和外接八边形来替代我的矩形,只是我遵循了这里的三角形光栅化优化建议。在之前的测试中,尽管去除了许多不必要的片段并能更有效地阻止覆盖的片段,但八边形的性能仍不如正方形。通过以下方式调整三角形绘制:

Rasterization optimizing octagons

我通过从正方形转换为八边形,成功将总渲染时间平均缩短了14%。深度纹理现在可以在19毫秒内生成,偶尔会下降至2毫秒,峰值为35毫秒。
编辑2(2011年5月31日):
我重新考虑了Tommy的使用步进函数的想法,由于八边形导致要丢弃的片段更少,这与球体的深度查找纹理相结合,现在在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的更新版本,请查看此处。

我仍然乐意听取其他建议,但对于该应用程序来说,这是一个巨大的进步。


9
在桌面上,许多早期可编程设备可以同时处理8个、16个或其他数量的片段,但实际上它们只有一个程序计数器(因为这也意味着只有一个获取/解码单元和一个其他所有单元,只要它们以8或16像素为单位工作)。因此最初禁止使用条件语句,在此之后一段时间内,如果用于将一起处理的像素的条件评估返回不同值,则以某种方式将这些像素分组处理。

虽然PowerVR没有明确说明,但他们的 应用开发建议 中有一个关于流程控制的部分,并且提出了动态分支通常仅在结果相当可预测时才是一个好主意的建议,这使我想到他们所说的同样事情。因此,我建议速度差异可能是因为您包含了条件语句。

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

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

}

我猜你也已经使用了Instruments的GLES分析器?它可以提供一些非显而易见的提示,尽管有时并不是很有用(例如,在变量限制影响下,持续警告我关于依赖纹理采样)。 - Tommy
1
我已经没有更多的想法了,但是你能否将你的几何体分成明确不透明和可能不透明的部分?你可以使用传统的z缓冲器从前到后(大致)绘制前者,并希望节省一些片段成本,然后禁用深度缓冲器以任意顺序绘制其他部分,仍然可以获得正确的颜色结果,然后将其转换为另一个深度缓冲器。 - Tommy
1
那么分两步来做吧?第一步:将绝对不透明的几何体定位到最接近球体的位置,启用颜色写入,启用深度读取但禁用深度写入。第二步:将几何体推回到球体可以到达的最远位置,禁用颜色写入(如果片段成本仍然累积,则设置一个统一的移动到简化着色器路径的统一变量),启用深度读取和写入。 - Tommy
1
实际上,最好分两步完成场景。第一步:不进行颜色写入,深度缓冲区只读和写入,明确不透明的部分,每个GL几何深度都是球体可能存在的最远距离。第二步:进行颜色写入,仅进行深度缓冲区读取,所有部分,每个GL几何深度都是球体可能存在的最近距离。 - Tommy
1
此外,在实施了Pivot关于此几何图形栅格优化八边形的建议后,我重新审视了您的阶梯函数着色器。结合深度纹理查找,使用阶梯函数能够将我的渲染时间从没有使用时的约19毫秒降至每个深度纹理帧的2.4毫秒。通过消除分支的减少开销所受益的片段数量现在一定远远超过了可以提前终止处理的片段数量。 - Brad Larson
显示剩余5条评论

9
许多人已经回答了这些问题,但总体主题是,您的渲染做了很多无用功:
  1. 着色器本身可能会执行一些冗余工作。矢量的长度可能会被计算为sqrt(dot(vector, vector))。在拒绝圆外片段时,您不需要sqrt,并且您正在平方长度以计算深度。此外,您是否已经考虑过明确量化深度值是否真正必要,或者只能使用硬件将浮点转换为整数(可能还需要附加偏差以确保稍微深度测试后才能正确)?

  2. 许多片段显然在圆外部分。您绘制的四边形仅产生π/4的有用深度值区域。此时,我想您的应用程序可能严重倾向于片段处理,因此您可能需要考虑增加绘制的顶点数量,以换取您必须着色的面积的减少。由于您通过正交投影绘制球体,因此任何包围正多边形都可以,尽管根据缩放级别可能需要更多的额外大小来确保您光栅化足够的像素。

  3. 许多片段显然被其他片段遮挡。正如其他人指出的那样,您没有使用硬件深度测试,因此没有充分利用TBDR尽早杀死着色工作的能力。如果您已经为点2)实现了某些东西,则只需在可以生成的最大深度处(通过球体中间的平面)绘制一个内接的正多边形,然后在最小深度(球体的前面)处绘制真正的多边形。Tommy和rotoglup的帖子已经包含了状态向量的详细信息。

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


谢谢你的建议。最终我选择了给Tommy发放奖金,因为他对于预渲染深度写入过程的明确描述恰好符合我的做法(请查看我的答案了解更多)。此外,我尝试使用插图和外接八边形来创建伪装器,但它们的性能比简单的正方形要差。我仍在研究原因,因为这似乎是通过消除约17%的要绘制的片段而获得的明显优势。 - Brad Larson
2
我还没有机会在实际设备上尝试这个,但是从发布的源代码中可以看出,您正在使用围绕中心点的扇形进行三角剖分,这对于光栅化来说是次优的。我很想知道像链接中描述的那种三角剖分方案是否更好,因为这将等同于从内接正方形开始并添加4个额外的阻挡块。 - Pivot
非常好的发现。这似乎正是情况所在,因为当我将三角形绘制顺序(如我的更新答案所示)从中心扇形更改为更“贪婪”的版本时,八边形的速度从比正方形慢变为快14%。我不知道这会对光栅化产生如此大的影响,所以感谢您指出这一点。 - Brad Larson
@Brad Larson:太好了。如果使用一个正八边形而不是现在略微不均匀的八边形,您还能进一步减小面积吗? - Pivot
除非我在计算上搞砸了,否则我的代码中使用的是一个普通八边形。我答案中的绘图只是我在OmniGraffle中随便拼凑的东西,所以不要按照那个尺寸来。 - Brad Larson

3

我并不是移动平台专家,但我认为问题出在:

  • 你的深度着色器比较耗费资源
  • 当你禁用GL_DEPTH测试时,在深度通道会出现过度绘制。

是否在深度测试之前增加一个额外的通道会有帮助?

这个通道可以进行GL_DEPTH预填充,例如通过将每个球面表示为面向相机的四边形(或立方体,可能更容易设置),并包含在相关球体内。这个通道可以不使用颜色掩码或片段着色器,只需启用GL_DEPTH_TESTglDepthMask即可。在桌面平台上,这种通道的绘制速度要快于颜色+深度通道。

然后在你的深度计算通道中,你可以启用GL_DEPTH_TEST并禁用glDepthMask,这样你的着色器就不会在被更近的几何体遮挡的像素上执行。

这个解决方案需要发出另一组绘制调用,因此可能并不是有益的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接