对 glDrawElements 的多次调用是不是比在 GLSL 中对每个片段执行相同的计算更有效?

Posted

技术标签:

【中文标题】对 glDrawElements 的多次调用是不是比在 GLSL 中对每个片段执行相同的计算更有效?【英文标题】:Is multiple calls to glDrawElements more efficient than doing the same calculations per-fragment in GLSL?对 glDrawElements 的多次调用是否比在 GLSL 中对每个片段执行相同的计算更有效? 【发布时间】:2012-10-29 23:45:26 【问题描述】:

我正在尝试使用 GLSL(在 ios 中),并且我编写了一个简单的着色器,它采用两个圆圈(centerradiusedgeSmoothing)的颜色值和参数。它是在整个屏幕上使用单个四边形绘制的,着色器使用gl_FragCoord 并确定每个点是在圆圈内还是在圆圈外 - 它在圆圈内计算 1.0 的 alpha,在 radius + edgeSmoothing 外平滑着色到 0.0,然后它对 alpha 应用镜像式钳位(三角形波以获得奇偶填充规则效果)并设置gl_FragColor = mix(vec4(0.0), color, alpha);

这很好用,但我想要 5 种不同颜色的 10 个圆圈,所以我调用 glUniform 为所有着色器制服和 glDrawElements 分别绘制五次四边形(使用不同的颜色和圆圈参数),我的混合模式是加法的,所以不同的颜色很好地叠加起来,得到我想要的图案,完美!

请记住,这是一个实验,所以我试图了解 GL 和 GLSL,而不是画圆圈。

现在我认为只绘制一次四边形并将所有 10 个圆的参数传递到统一数组(centers[10]radii[10] 等)中,在 GLSL 中循环遍历它们并将它们在着色器中产生的颜色相加。所以我编写了这个着色器并重构了我的代码以一次传递所有的圆形参数。我得到了正确的结果(输出看起来完全一样),但我的帧速率从 15fps 下降到大约 3fps - 它慢了五倍!

着色器代码现在有循环,但使用相同的数学计算每对圆的 alpha 值。为什么这么慢?当然,我所做的工作比填充整个屏幕五次而 GL 进行五次加法混合(即读取像素值、混合和写回)要少吗?现在我只是计算累积颜色并填充整个屏幕一次?

谁能解释为什么我认为的优化会产生相反的效果?

更新:将此代码粘贴到ShaderToy,看看我在说什么。

#ifdef GL_ES
precision highp float;
#endif

uniform float time;

void main(void)

    float r, d2, a0, a1, a2;
    vec2 pos, mid, offset;
    vec4 bg, fg;

    bg = vec4(.20, .20, .40, 1.0);
    fg = vec4(.90, .50, .10, 1.0);
    mid = vec2(256.0, 192.0);

    // Circle 0
    pos = gl_FragCoord.xy - mid;
    d2 = dot(pos, pos);
    r = 160.0;
    a0 = smoothstep(r * r, (r + 1.0) * (r + 1.0), d2);

    // Circle 1
    offset = vec2(110.0 * sin(iGlobalTime*0.8), 110.0 * cos(iGlobalTime));
    pos = gl_FragCoord.xy - mid + offset;
    d2 = dot(pos, pos);
    r = 80.0;
    a1 = smoothstep(r * r, (r + 1.0) * (r + 1.0), d2);

    // Circle 2
    offset = vec2(100.0 * sin(iGlobalTime*1.1), -100.0 * cos(iGlobalTime*0.7));
    pos = gl_FragCoord.xy - mid + offset;
    d2 = dot(pos, pos);
    r = 80.0;
    a2 = smoothstep(r * r, (r + 1.0) * (r + 1.0), d2);

    // Calculate the final alpha
    float a = a0 + a1 + a2;
    a = abs(mod(a, 2.0) - 1.0);

    gl_FragColor = mix(bg, fg, a);

【问题讨论】:

如果没有看到你的着色器,我会说你在这里有动态分支问题,因此无法利用完整的 GPU 管道。尝试手动展开循环,看看是否能解决问题。 【参考方案1】:

增加片段着色器中操作的复杂性会对渲染时间产生非线性影响。在某些情况下,即使添加一个看起来很简单的分支操作也会使着色器慢 10 倍。

在 iOS 设备上的片段着色器中,循环尤其可怕,所以我会不惜一切代价避免它们。我敢打赌,如果您将该循环展开为一系列针对您的统一值的检查,它会执行得更好。

但是,对你的制服运行 10 次检查,这听起来像是涉及步骤或平滑步骤,当应用于帧缓冲区中的每个像素时将非常昂贵。这也相当浪费,因为屏幕的很大一部分不会被任何特定的圆圈覆盖。

无需使用单独的glDrawElements() 调用来绘制单独的圆圈,或者通过绘制屏幕大小的四边形来绘制。我描述了我在this answer 内的开源应用程序中绘制球体冒名顶替者的过程,我可以在最新的 iOS 设备上以 60 FPS 的速度在屏幕上绘制数千个圆圈(球体)。为此,我为每个足够大的圆圈传入一个四边形,该圆圈包含该圆圈并且不大于该圆圈。这些四边形都聚集在一个数组中并立即绘制。每个圆的附加参数作为属性与顶点数据一起传入。例如,我不需要指定半径,因为我在顶点旁边使用从 (-1, -1) 到 (1, 1) 的冒名顶替空间坐标,并进行简单的计算以确定一个点是否在圆内。

如果您只绘制每个圆圈所需的片段,而不是更多,您将减轻管道片段处理部分的大量负载。您仍然需要启用混合模式,但四边形大小的减小,加上片段着色器中执行的操作的简化,将带来更好的整体性能。

【讨论】:

感谢您的回答 - 这个和进一步的阅读确实帮助我更多地了解 GLSL 以及分支和循环如何杀死并行化。我很想这样做,但我不认为 GL 混合可以实现我想要的奇偶效果(请参阅我的更新中的着色器代码) - 因为一切都太快了。有没有办法让加法效果超过 1.0,然后使用我自己的钳位规则 (a = abs(mod(a, 2.0) - 1.0))? @jhabbott - 你仍然可以做一个加法混合,你的圈子的每一次通过的输出值按你正在使用的圈子数量的倒数缩放(在这种情况下是 1/10 )。然后,您可以将模数作为第二遍应用,从第一遍获取输入并将其按圈数放大。你会失去一点动态范围(基于所涉及的圈数),但它仍然会很快。 我也想过,但是你没有在片段着色器中读取目标缓冲区数据,所以我怎么能访问之前写入的数据? @jhabbott - 您将首先对所有圆圈进行加法混合,并让您渲染的 FBO 得到纹理的支持。对于第二遍,您将从该纹理加载并对其中的颜色进行模数运算。或者,您可以使用新的 iOS 6.0 帧缓冲区加载操作来更快地读取以前的输出颜色。 啊,我没想到使用单独的纹理作为临时存储,谢谢:)

以上是关于对 glDrawElements 的多次调用是不是比在 GLSL 中对每个片段执行相同的计算更有效?的主要内容,如果未能解决你的问题,请参考以下文章

glDrawElements 抛出 GL_INVALID_VALUE 错误

glDrawElements 崩溃

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

当有许多重复顶点时,glDrawElements() 是不是比 glDrawArrays() 更有效?

verilog中,对一个模块的多次调用,比如前一个调用还没有结束的情况下,就再次调用,后者是不是会覆盖前者?

OpenGL ES之实例化渲染(Instancing)