在OpenGL ES 2中,卷积着色器的最佳方法是什么,它足够实时快速?

时间:2013-09-22 03:09:55

标签: ios opengl-es-2.0 shader

注意:现在我正在模拟器中对此进行测试。但我的想法是,我得到了可接受的性能,比如iPhone 4s。 (我知道,我应该在设备上进行测试,但几天后我就没有设备了。)

我正在玩制作一个卷积着色器,可以使用支持3x3,5x5或7x7的滤镜对图像进行卷积,并可以选择多次通过。着色器本身就可以工作了。但我注意到以下几点:

  1. 一个简单的盒式滤镜3x3,单通,几乎不会模糊图像。因此,为了获得更明显的模糊,我必须做3x3 2遍或5x5。
  2. 最简单的情况(3x3,1档)已经足够慢,不能用30 fps。
  3. 到目前为止我尝试了两种方法(这是我为iPhone做的一些基于OGLES2的插件,这就是方法的原因):

    - (NSString *)vertexShader
    {
    
        return SHADER_STRING
        (
         attribute vec4 aPosition;
         attribute vec2 aTextureCoordinates0;
    
         varying vec2 vTextureCoordinates0;
    
         void main(void)
         {
             vTextureCoordinates0 = aTextureCoordinates0;
             gl_Position = aPosition;
         }
    
         );
    }
    
    - (NSString *)fragmentShader
    {
        return SHADER_STRING
        (
         precision highp float;
    
         uniform sampler2D uTextureUnit0;
         uniform float uKernel[49];
         uniform int uKernelSize;
         uniform vec2 uTextureUnit0Offset[49];
         uniform vec2 uTextureUnit0Step;
    
         varying vec2 vTextureCoordinates0;
    
         void main(void)
         {
             vec4 outputFragment = texture2D(uTextureUnit0, vTextureCoordinates0 + uTextureUnit0Offset[0] * uTextureUnit0Step) * uKernel[0];
             for (int i = 0; i < uKernelSize; i++) {
                 outputFragment += texture2D(uTextureUnit0, vTextureCoordinates0 + uTextureUnit0Offset[i] * uTextureUnit0Step) * uKernel[i];
             }
    
             gl_FragColor = outputFragment;
         }
    
         );
    }
    

    这种方法的想法是,过滤器值和offsetCoordinates to fetch texels都在Client / App中预先计算一次,然后在制服中设置。然后,着色器程序将始终在任何时候使用它们。请注意,统一数组(49)的大尺寸是因为我可以做到7x7内核。

    此方法每次传递需要.46s。

    然后我尝试了以下方法:

    - (NSString *)vertexShader
    {
    
        return SHADER_STRING
        (
         // Default pass-thru vertex shader:
         attribute vec4 aPosition;
         attribute vec2 aTextureCoordinates0;
    
         varying highp vec2 vTextureCoordinates0;
    
         void main(void)
         {
             vTextureCoordinates0 = aTextureCoordinates0;
             gl_Position = aPosition;
         }
    
         );
    }
    
    - (NSString *)fragmentShader
    {
        return SHADER_STRING
        (
         precision highp float;
    
         uniform sampler2D uTextureUnit0;
         uniform vec2 uTextureUnit0Step;
         uniform float uKernel[49];
         uniform float uKernelRadius;
    
         varying vec2 vTextureCoordinates0;
    
         void main(void)
         {
             vec4 outputFragment = vec4(0., 0., 0., 0.);
             int kRadius = int(uKernelRadius);
             int kSupport  = 2 * kRadius + 1;
             for (int t = -kRadius; t <= kRadius; t++) {
                 for (int s = -kRadius; s <= kRadius; s++) {
                     int kernelIndex = (s + kRadius) + ((t + kRadius) * kSupport);
                     outputFragment += texture2D(uTextureUnit0, vTextureCoordinates0 + (vec2(s,t) * uTextureUnit0Step)) * uKernel[kernelIndex];
                 }
             }
    
             gl_FragColor = outputFragment;
         }
    
         );
    }
    

    在这里,我仍然通过制服将预先计算的内核传递到片段着色器中。但我现在计算着色器中的纹素偏移甚至内核索引。我希望这种方法更慢,因为我不仅有2个for循环,而且我还为每个片段做了一堆额外的计算。

    有趣的是,这种方法需要0.42秒。实际上更快......

    此时,我能想到的另一件事就是将2D内核视为两个可分离的1D内核,将卷积制成2遍。尚未尝试过。

    仅仅为了比较,并且意识到以下示例是盒式过滤的特定实现,即A - 几乎是硬编码的,B - 并不真正遵循经典nxn线性过滤器的理论定义(它不是矩阵并且不等于1),我从OpenGL ES 2.0编程指南中尝试了这种方法:

    - (NSString *)fragmentShader
    {
        return SHADER_STRING
        (
         // Default pass-thru fragment shader:
         precision mediump float;
    
         // Input texture:
         uniform sampler2D uTextureUnit0;
    
         // Texel step:
         uniform vec2 uTextureUnit0Step;
    
    
         varying vec2 vTextureCoordinates0;
    
         void main() {
             vec4 sample0;
             vec4 sample1;
             vec4 sample2;
             vec4 sample3;
             float step = uTextureUnit0Step.x;
             sample0 = texture2D(uTextureUnit0, vec2(vTextureCoordinates0.x - step, vTextureCoordinates0.y - step));
             sample1 = texture2D(uTextureUnit0, vec2(vTextureCoordinates0.x + step, vTextureCoordinates0.y + step));
             sample2 = texture2D(uTextureUnit0, vec2(vTextureCoordinates0.x + step, vTextureCoordinates0.y - step));
             sample3 = texture2D(uTextureUnit0, vec2(vTextureCoordinates0.x - step, vTextureCoordinates0.y + step));
             gl_FragColor = (sample0 + sample1 + sample2 + sample3) / 4.0;
         }
         );
    }
    

    此方法每次传递0.06秒。 请注意,以上是我的改编,我在步骤中使用了与我在实现中使用的相同的纹素偏移量。通过此步骤,结果与我的实现非常相似,但OpenGL指南中的原始着色器使用了更大的步骤,模糊了更多。

    所以说到上述所有内容,我的问题实际上是双重的:

    1. 我将步/纹素偏移计算为vec2(1 /图像宽度,1 /图像高度)。有了这个偏移量,就像我说的那样,3x3盒式滤波器几乎看不到。它是否正确?或者我误解了步骤或其他的计算?
    2. 我还能做些什么来尝试让“一般情况下的卷积”方法足够快地实时运行?或者我是否一定需要像OpenGL示例那样进行简化?

2 个答案:

答案 0 :(得分:3)

如果您通过Instruments中的OpenGL ES分析工具或Xcode中的Frame Debugger运行它们,您可能会看到关于dependent texture reads的注释 - 您正在计算片段着色器中的texcoords,这意味着硬件无法获取纹素数据,直到它在评估着色器时达到这一点。如果已知纹素坐标进入片段着色器,硬件可以与其他任务并行预取texel数据,因此在片段着色器需要它时就可以使用了。

通过在顶点着色器中预先计算纹素坐标,可以大大加快速度。 Brad Larson在this answer中就类似的问题提供了一个很好的例子。

答案 1 :(得分:0)

我没有关于你的确切问题的答案,但你应该看看GPUImage框架 - 它实现了几个框模糊过滤器(见SO question) - 其中一个2遍9x9过滤器 - 您还可以看到本文针对不同方法的实时FPS:vImage VS GPUImage vs CoreImage