2012-09-18 28 views
15

Estoy tratando de implementar un sombreador de contorno 2D en OpenGL ES2.0 para iOS. Es terriblemente lento. Como en 5 fps lento. Lo he rastreado hasta las llamadas texture2D(). Sin embargo, sin ellos, cualquier sombreado de convolución no se puede deshacer. He intentado usar lowp en lugar de mediump, pero con eso todo es simplemente negro, aunque da otros 5 fps, pero sigue inutilizable.El sombreador de convolución GLSL simple es atrozmente lento

Aquí está mi sombreador de fragmentos.

varying mediump vec4 colorVarying; 
    varying mediump vec2 texCoord; 

    uniform bool enableTexture; 
    uniform sampler2D texture; 

    uniform mediump float k; 

    void main() { 

     const mediump float step_w = 3.0/128.0; 
     const mediump float step_h = 3.0/128.0; 
     const mediump vec4 b = vec4(0.0, 0.0, 0.0, 1.0); 
     const mediump vec4 one = vec4(1.0, 1.0, 1.0, 1.0); 

     mediump vec2 offset[9]; 
     mediump float kernel[9]; 
     offset[0] = vec2(-step_w, step_h); 
     offset[1] = vec2(-step_w, 0.0); 
     offset[2] = vec2(-step_w, -step_h); 
     offset[3] = vec2(0.0, step_h); 
     offset[4] = vec2(0.0, 0.0); 
     offset[5] = vec2(0.0, -step_h); 
     offset[6] = vec2(step_w, step_h); 
     offset[7] = vec2(step_w, 0.0); 
     offset[8] = vec2(step_w, -step_h); 

     kernel[0] = kernel[2] = kernel[6] = kernel[8] = 1.0/k; 
     kernel[1] = kernel[3] = kernel[5] = kernel[7] = 2.0/k; 
     kernel[4] = -16.0/k; 

     if (enableTexture) { 
       mediump vec4 sum = vec4(0.0); 
      for (int i=0;i<9;i++) { 
       mediump vec4 tmp = texture2D(texture, texCoord + offset[i]); 
       sum += tmp * kernel[i]; 
      } 

      gl_FragColor = (sum * b) + ((one-sum) * texture2D(texture, texCoord)); 
     } else { 
      gl_FragColor = colorVarying; 
     } 
    } 

Ésta es sin optimizar, no ha sido finalizado, pero necesito para abrir el rendimiento antes de continuar. He intentado reemplazar la llamada texture2D() en el bucle con solo un vec4 sólido y no corre ningún problema, a pesar de todo lo demás que está sucediendo.

¿Cómo puedo optimizar esto? Sé que es posible porque he visto efectos mucho más complicados en 3D sin problemas. No puedo ver por qué esto está causando ningún problema.

+0

"* He intentado reemplazar la llamada texture2D() en el bucle con solo un vec4 sólido y no se ejecuta ningún problema *" ¿Qué significa eso? ¿Se hizo más rápido? ¿No cambió el rendimiento? ¿Que pasó? –

+0

"* No puedo ver por qué esto está causando ningún problema. *" ¿Estás haciendo * diez accesos de textura * por invocación de sombreado, y no ves lo que podría estar causando un problema? Además, accedes al centro texel dos veces. –

+0

Obtengo un sólido 60 fps sin las búsquedas de texturas (excluyendo el final). Como dije, no está optimizado, pero no hay manera de evitar esas llamadas de textura. El filtro no podría funcionar de otra manera. Pero he visto muchos juegos, móviles y no, que usan efectos basados ​​en filtros de convolución, y no parecen tener ningún problema. ¿A menos que haya algún truco para evitarlos? – user1137704

Respuesta

6

La única manera que conozco de reducir el tiempo empleado en este sombreado es mediante la reducción del número de recuperaciones de textura. Debido a que su sombreador toma muestras de texturas de puntos igualmente espaciados sobre los píxeles centrales y los combina linealmente, puede reducir el número de recuperaciones haciendo uso del modo de disponibilidad GL_LINEAR para el muestreo de texturas.

Básicamente, en lugar de muestrear en cada téxel, muestra entre un par de téxeles para obtener directamente una suma ponderada linealmente.

Llamemos el muestreo en offset (-stepw, -steph) y (-stepw, 0) como x0 y x1 respectivamente. Entonces su suma es

sum = x0*k0 + x1*k1

Ahora, en lugar si usted muestrea entre estos dos texels, a una distancia de k0/(k0+k1) de x0 y por lo tanto k1/(k0+k1) de x1, a continuación, la GPU realizará la ponderación lineal durante la captación y darle,

y = x1*k1/(k0+k1) + x0*k0/(k1+k0)

Así suma se puede calcular como

sum = y*(k0 + k1) de una sola búsqueda!

Si repite esto para los otros píxeles adyacentes, terminará haciendo 4 recuperaciones de textura para cada uno de los desplazamientos adyacentes, y una recuperación de textura extra para el píxel central.

El link explica esto mucho mejor

38

lo he hecho exactamente lo mismo, y veo varias cosas que podrían ser optimizados aquí.

En primer lugar, eliminaría el condicional enableTexture y en su lugar dividiría su sombreador en dos programas, uno para el estado verdadero de esto y otro para falso. Los condicionales son muy caros en los sombreadores de fragmentos de iOS, especialmente los que tienen lecturas de texturas dentro de ellos.

En segundo lugar, tiene nueve lecturas de textura dependientes aquí. Estas son lecturas de textura donde las coordenadas de textura se calculan dentro del sombreador de fragmentos. Las lecturas de textura dependientes son muy costosas en las GPU PowerVR dentro de los dispositivos iOS, ya que impiden que el hardware optimice las lecturas de textura usando el almacenamiento en caché, etc. Dado que está tomando muestras de un desplazamiento fijo para los 8 píxeles circundantes y uno central, estos cálculos deben movido hacia arriba en el sombreador de vértices.Esto también significa que estos cálculos no tendrán que realizarse para cada píxel, solo una vez para cada vértice y luego la interpolación de hardware manejará el resto.

En tercer lugar, los bucles for() no han sido manejados tan bien por el compilador shader iOS hasta la fecha, por lo que tiendo a evitar aquellos donde puedo.

Como he mencionado, he hecho sombreadores de convolución como este en mi marco de código abierto iOS GPUImage. Para un filtro de convolución genérico, utilizo el siguiente shader vértice:

attribute vec4 position; 
attribute vec4 inputTextureCoordinate; 

uniform highp float texelWidth; 
uniform highp float texelHeight; 

varying vec2 textureCoordinate; 
varying vec2 leftTextureCoordinate; 
varying vec2 rightTextureCoordinate; 

varying vec2 topTextureCoordinate; 
varying vec2 topLeftTextureCoordinate; 
varying vec2 topRightTextureCoordinate; 

varying vec2 bottomTextureCoordinate; 
varying vec2 bottomLeftTextureCoordinate; 
varying vec2 bottomRightTextureCoordinate; 

void main() 
{ 
    gl_Position = position; 

    vec2 widthStep = vec2(texelWidth, 0.0); 
    vec2 heightStep = vec2(0.0, texelHeight); 
    vec2 widthHeightStep = vec2(texelWidth, texelHeight); 
    vec2 widthNegativeHeightStep = vec2(texelWidth, -texelHeight); 

    textureCoordinate = inputTextureCoordinate.xy; 
    leftTextureCoordinate = inputTextureCoordinate.xy - widthStep; 
    rightTextureCoordinate = inputTextureCoordinate.xy + widthStep; 

    topTextureCoordinate = inputTextureCoordinate.xy - heightStep; 
    topLeftTextureCoordinate = inputTextureCoordinate.xy - widthHeightStep; 
    topRightTextureCoordinate = inputTextureCoordinate.xy + widthNegativeHeightStep; 

    bottomTextureCoordinate = inputTextureCoordinate.xy + heightStep; 
    bottomLeftTextureCoordinate = inputTextureCoordinate.xy - widthNegativeHeightStep; 
    bottomRightTextureCoordinate = inputTextureCoordinate.xy + widthHeightStep; 
} 

y la siguiente shader fragmento:

precision highp float; 

uniform sampler2D inputImageTexture; 

uniform mediump mat3 convolutionMatrix; 

varying vec2 textureCoordinate; 
varying vec2 leftTextureCoordinate; 
varying vec2 rightTextureCoordinate; 

varying vec2 topTextureCoordinate; 
varying vec2 topLeftTextureCoordinate; 
varying vec2 topRightTextureCoordinate; 

varying vec2 bottomTextureCoordinate; 
varying vec2 bottomLeftTextureCoordinate; 
varying vec2 bottomRightTextureCoordinate; 

void main() 
{ 
    mediump vec4 bottomColor = texture2D(inputImageTexture, bottomTextureCoordinate); 
    mediump vec4 bottomLeftColor = texture2D(inputImageTexture, bottomLeftTextureCoordinate); 
    mediump vec4 bottomRightColor = texture2D(inputImageTexture, bottomRightTextureCoordinate); 
    mediump vec4 centerColor = texture2D(inputImageTexture, textureCoordinate); 
    mediump vec4 leftColor = texture2D(inputImageTexture, leftTextureCoordinate); 
    mediump vec4 rightColor = texture2D(inputImageTexture, rightTextureCoordinate); 
    mediump vec4 topColor = texture2D(inputImageTexture, topTextureCoordinate); 
    mediump vec4 topRightColor = texture2D(inputImageTexture, topRightTextureCoordinate); 
    mediump vec4 topLeftColor = texture2D(inputImageTexture, topLeftTextureCoordinate); 

    mediump vec4 resultColor = topLeftColor * convolutionMatrix[0][0] + topColor * convolutionMatrix[0][1] + topRightColor * convolutionMatrix[0][2]; 
    resultColor += leftColor * convolutionMatrix[1][0] + centerColor * convolutionMatrix[1][1] + rightColor * convolutionMatrix[1][2]; 
    resultColor += bottomLeftColor * convolutionMatrix[2][0] + bottomColor * convolutionMatrix[2][1] + bottomRightColor * convolutionMatrix[2][2]; 

    gl_FragColor = resultColor; 
} 

El texelWidth y texelHeight uniformes son la inversa de la anchura y altura de la imagen de entrada y el uniforme convolutionMatrix especifica los pesos para las diversas muestras en su convolución.

En un iPhone 4, esto se ejecuta en 4-8 ms para un video de cámara de 640x480, que es lo suficientemente bueno para 60 FPS en ese tamaño de imagen. Si solo necesita hacer algo como detección de bordes, puede simplificar lo anterior, convertir la imagen a luminancia en un paso previo, y luego solo tomar muestras de un canal de color. Eso es aún más rápido, aproximadamente 2 ms por cuadro en el mismo dispositivo.

+0

Agradecimiento especial. ¡Me salvó! – hiepnd

+0

Gran ejemplo. tl; dr: ** evitar lecturas de textura dependientes **. Esforzarse, también, para probar las circunvoluciones separables mediante la prestación en dos pasadas para reducir el número de las extracciones (aunque para un ejemplo de 9 no se reduciría a menos de la mitad, por lo que en este caso podría ser posible un enfoque de dos pasadas). sea ​​una mala idea) –

+1

@StevenLu - Sorprendentemente, se produce una disminución en el rendimiento una vez que superas las 9 lecturas de textura más o menos en una sola pasada en muchas de estas GPU. Dividir esto en dos pasadas puede tener un impacto no lineal en el rendimiento, en comparación con el número de muestras en una sola pasada. He probado, y ejecutar esto en una sola pasada es mucho, mucho más lento que separar el núcleo, incluso para esta pequeña cantidad de muestras. –

Cuestiones relacionadas