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

Bra*_*son 39 iphone opengl-es glsl ipad ios

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

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

这种深度纹理的生成是我渲染过程中的一个瓶颈,我正在寻找一种方法来加快它的速度.它似乎比它应该慢,但我无法弄清楚为什么.为了实现正确生成此深度纹理​​,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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在iPad 1上,这需要35 - 68 ms来渲染DNA空间填充模型的帧,使用直通着色器进行显示(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);
}
Run Code Online (Sandbox Code Playgroud)

在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);
}
Run Code Online (Sandbox Code Playgroud)

iPad 1上的渲染时间下降到1.1 - 2.3 ms(iPhone 4上为1.3 ms).

对于第二个着色器,渲染时间的非线性缩放和iPad与iPhone 4之间的突然变化让我觉得我在这里缺少一些东西.如果您希望自己尝试这个,可以从这里下载包含这三个着色器变体的完整源项目(查看SphereDepth.fsh文件并注释掉相应的部分)和测试模型.

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

Bra*_*son 19

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

首先,我重新启用了之前使用过的预先计算的球体深度和光照纹理效果很小,现在我只在使用lowp该纹理处理颜色和其他值时使用了适当的精度值.这种组合,以及纹理的适当mipmapping,似乎可以提高~10%的性能.

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

然后我禁用深度写入glDepthMask(GL_FALSE)并使用方形球体冒充者在靠近用户一个半径的位置渲染.这允许iOS设备中基于图块的延迟渲染硬件有效地剥离在任何条件下永远不会出现在屏幕上的片段,但仍然基于每像素深度值在可见球体冒名顶替者之间提供平滑交叉.这在我的粗略插图中描述:

分层球体和不透明度测试

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

我没有想过要禁用深度写入,但在进行最后一个渲染阶段时会留下深度测试.这是阻止冒名顶替者相互堆叠,但仍然使用PowerVR GPU内部的一些硬件优化的关键.

在我的基准测试中,渲染我上面使用的测试模型每帧产生18-35毫秒的时间,相比之前我所获得的35-68毫秒,渲染速度接近翻倍.将相同的不透明几何体预渲染应用于光线追踪过程会使整体渲染性能提高一倍.

奇怪的是,当我试图通过使用插入和限制的八边形进一步细化时,这应该在绘制时覆盖约17%的像素,并且对于阻挡碎片更有效,性能实际上比使用简单方块更差.在最坏的情况下,Tiler利用率仍然低于60%,因此较大的几何结构可能导致更多的缓存未命中.

编辑(2011年5月31日):

基于Pivot的建议,我创建了用于代替我的矩形的内切和外接八边形,只有我遵循这里的建议优化光栅化的三角形.在之前的测试中,尽管删除了许多不必要的碎片并且让您更有效地阻挡被覆盖的碎片,但八边形的性能比方块更差.通过调整三角图如下:

光栅化优化八边形

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

编辑2(5/31/2011):

我重新审视了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);
}
Run Code Online (Sandbox Code Playgroud)

我已经在这里更新了测试样本,如果你希望看到这个新的方法与我最初的做法相比.

我仍然对其他建议持开放态度,但这对于这个应用来说是一个巨大的进步.


Tom*_*mmy 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);

}
Run Code Online (Sandbox Code Playgroud)


Piv*_*vot 8

发布答案的其他人已经介绍了其中的许多要点,但这里的首要主题是你的渲染做了大量的工作,将被丢弃:

  1. 着色器本身可以完成一些潜在的冗余工作.矢量的长度很可能被计算为sqrt(dot(vector, vector)).你不需要sqrt来拒绝圆圈之外的碎片,无论如何你都要平算长度来计算深度.另外,你看看深度值的显式量化是否真的是必要的,或者你可以使用硬件从浮点到帧缓冲的整数转换(可能还有额外的偏差以确保你的准-depth测试后来出来)?

  2. 许多碎片都在圆圈之外.只有π/ 4的四边形区域会产生有用的深度值.在这一点上,我想你的应用程序严重偏向于片段处理,因此您可能需要考虑增加绘制的顶点数量,以换取减少必须遮蔽的区域.由于您通过正交投影绘制球体,所以任何外接规则多边形都可以,但根据缩放级别可能需要一些额外的大小,以确保光栅化足够的像素.

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

请注意,2)和3)也适用于光线跟踪着色器.

  • 我没有机会在实际的设备上玩这个,但我从发布的消息来源看到你使用围绕中心点的风扇进行三角测量,这对于光栅化来说是次优的(http:// www. humus.name/index.php?page=News&ID=228).我会对链接中描述的三角测量方案是否会更好地工作感兴趣,因为这相当于从您的刻字方块开始并添加4个额外的阻塞块. (2认同)