iphone ios ipad opengl-es glsl
herehere

iphone - ¿Cómo puedo mejorar el rendimiento de mi generación personalizada de texturas de profundidad OpenGL ES 2.0?



ios ipad (4)

Tengo una aplicación de iOS de código abierto que usa sombreadores de OpenGL ES 2.0 personalizados para mostrar representaciones tridimensionales de estructuras moleculares. Lo hace mediante el uso de impostores de esfera y cilindro generados por procedimientos dibujados sobre rectángulos, en lugar de estas mismas formas construidas usando muchos vértices. La desventaja de este enfoque es que los valores de profundidad para cada fragmento de estos objetos impostores deben calcularse en un sombreador de fragmentos, que se utilizará cuando los objetos se superponen.

Desafortunadamente, OpenGL ES 2.0 no te permite escribir en gl_FragDepth , así que he necesitado dar salida a estos valores a una textura de profundidad personalizada. Hago un pase sobre mi escena usando un objeto framebuffer (FBO), solo obteniendo un color que corresponde a un valor de profundidad, con los resultados almacenados en una textura. Esta textura luego se carga en la segunda mitad de mi proceso de renderizado, donde se genera la imagen de la pantalla real. Si un fragmento en esa etapa está en el nivel de profundidad almacenado en la textura de profundidad para ese punto en la pantalla, se muestra. Si no, es lanzado. Se puede encontrar más información sobre el proceso, incluidos los diagramas, en mi publicación here .

La generación de esta textura de profundidad es un cuello de botella en mi proceso de renderizado y estoy buscando una manera de hacerlo más rápido. Parece más lento de lo que debería ser, pero no puedo entender por qué. Para lograr la generación adecuada de esta textura de profundidad, GL_DEPTH_TEST está desactivado, GL_BLEND está habilitado con glBlendFunc(GL_ONE, GL_ONE) y glBlendEquation() se establece en GL_MIN_EXT . Sé que una salida de escena de esta manera no es la más rápida en un renderizador diferido basado en mosaicos como la serie PowerVR en dispositivos iOS, pero no puedo pensar en una mejor manera de hacerlo.

Mi sombreador de fragmentos de profundidad para esferas (el elemento de visualización más común) parece estar en el centro de este cuello de botella (la utilización de Renderer en instrumentos está vinculada al 99%, lo que indica que estoy limitado por el procesamiento de fragmentos). Actualmente se ve así:

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

En un iPad 1, esto lleva de 35 a 68 ms renderizar un cuadro de un modelo de relleno de ADN con un sombreador de paso para su visualización (de 18 a 35 ms en el iPhone 4). Según el compilador PowerVR PVRUniSCo (parte de su SDK ), este sombreador usa 11 ciclos de GPU en el mejor de los casos, 16 ciclos en el peor. Soy consciente de que se te aconseja no usar la bifurcación en un sombreador, pero en este caso eso condujo a un mejor rendimiento.

Cuando lo simplifico para

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

toma de 18 a 35 ms en el iPad 1, pero solo de 1.7 a 2.4 ms en el iPhone 4. El conteo estimado de ciclos de la GPU para este sombreador es de 8 ciclos. El cambio en el tiempo de renderizado basado en el conteo de ciclos no parece lineal.

Finalmente, si acabo de mostrar un color constante:

precision mediump float; void main() { gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0); }

el tiempo de renderización se reduce a 1.1 - 2.3 ms en el iPad 1 (1.3 ms en el iPhone 4).

La escala no lineal en el tiempo de renderizado y el cambio repentino entre iPad y iPhone 4 para el segundo sombreador me hace pensar que me falta algo aquí. Un proyecto de fuente completa que contiene estas tres variantes de sombreado (busque en el archivo SphereDepth.fsh y comente las secciones correspondientes) y puede descargar un modelo de prueba desde here , si desea probarlo usted mismo.

Si ha leído hasta aquí, mi pregunta es: en base a esta información de perfiles, ¿cómo puedo mejorar el rendimiento de renderizado de mi sombreador de profundidad personalizado en dispositivos con iOS?


Basado en las recomendaciones de Tommy, Pivot y Rotoglup, he implementado algunas optimizaciones que han llevado a duplicar la velocidad de renderizado tanto para la generación de texturas de profundidad como para la línea de renderizado general en la aplicación.

Primero, volví a habilitar la profundidad de la esfera precalculada y la textura de iluminación que había utilizado antes con poco efecto, solo que ahora lowp los valores correctos de precisión lowp al manejar los colores y otros valores de esa textura. Esta combinación, junto con el mipmapping adecuado para la textura, parece producir un aumento de rendimiento del ~ 10%.

Más importante aún, ahora hago un pase antes de representar tanto mi textura de profundidad como los impostores de trazado de rayos finales, donde establezco una geometría opaca para bloquear los píxeles que nunca se renderizarán. Para hacer esto, habilito la prueba de profundidad y luego dibujo los cuadrados que componen los objetos en mi escena, reducidos por sqrt (2) / 2, con un simple sombreador opaco. Esto creará cuadrados insertados que cubren el área que se sabe que es opaca en una esfera representada.

Luego deshabilito las escrituras de profundidad usando glDepthMask(GL_FALSE) y renderizo el impostor de esfera cuadrada en una ubicación más cercana al usuario en un radio. Esto permite que el hardware de renderizado diferido basado en mosaicos en los dispositivos iOS elimine eficientemente fragmentos que nunca aparecerían en pantalla bajo ninguna circunstancia, y aun así proporciona intersecciones suaves entre los impostores de esferas visibles en función de los valores de profundidad por píxel. Esto se muestra en mi ilustración cruda a continuación:

En este ejemplo, los cuadrados de bloqueo opacos para los dos impostores superiores no impiden que se renderice ninguno de los fragmentos de esos objetos visibles, pero bloquean una porción de los fragmentos del impostor más bajo. Los impostores frontales pueden usar pruebas por píxel para generar una intersección suave, mientras que muchos de los píxeles del impostor posterior no desperdician los ciclos de la GPU al renderizarse.

No había pensado en desactivar las escrituras de profundidad, pero dejarlas en la prueba de profundidad al hacer la última etapa de reproducción. Esta es la clave para evitar que los impostores se apilen entre sí y, sin embargo, utilicen algunas de las optimizaciones de hardware dentro de las GPU PowerVR.

En mis puntos de referencia, la renderización del modelo de prueba que utilicé arriba produce tiempos de 18 a 35 ms por cuadro, en comparación con los 35 a 68 ms que recibía anteriormente, casi el doble de la velocidad de procesamiento. La aplicación de esta misma preproducción de geometría opaca al pase de trazado de rayos produce una duplicación del rendimiento general de la representación.

Curiosamente, cuando traté de refinar esto más utilizando octágonos insertados y circunscritos, que deberían cubrir ~ 17% menos píxeles cuando se dibujaban, y ser más eficientes con el bloqueo de fragmentos, el rendimiento era peor que cuando se usaban cuadrados simples para esto. La utilización de Tiler todavía era inferior al 60% en el peor de los casos, por lo que tal vez la geometría más grande estaba dando como resultado más errores de caché.

EDITAR (31/05/2011):

Basado en la sugerencia de Pivot, creé octágonos inscritos y circunscritos para usar en lugar de mis rectángulos, solo seguí las recomendaciones here para optimizar los triángulos para la rasterización. En pruebas previas, los octágonos arrojaron un rendimiento peor que los cuadrados, a pesar de eliminar muchos fragmentos innecesarios y permitirle bloquear los fragmentos cubiertos de manera más eficiente. Ajustando el dibujo del triángulo de la siguiente manera:

Pude reducir el tiempo total de renderizado en un promedio de 14% sobre las optimizaciones descritas anteriormente al cambiar a octógonos de cuadrados. La textura de profundidad ahora se genera en 19 ms, con caídas ocasionales de 2 ms y picos de hasta 35 ms.

EDICION 2 (31/05/2011):

He revisado la idea de Tommy de usar la función de paso, ahora que tengo menos fragmentos para descartar debido a los octágonos. Esto, combinado con una textura de búsqueda en profundidad para la esfera, ahora conduce a un tiempo de renderización promedio de 2 ms en el iPad 1 para la generación de textura de profundidad para mi modelo de prueba. Considero que es tan bueno como podría desear en este caso de revocación, y una mejora enorme desde donde comencé. Para la posteridad, aquí está el sombreador de profundidad que ahora uso:

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

He actualizado la muestra de prueba here , si desea ver este nuevo enfoque en acción en comparación con lo que estaba haciendo inicialmente.

Todavía estoy abierto a otras sugerencias, pero este es un gran paso adelante para esta aplicación.


En el escritorio, muchos de los primeros dispositivos programables admitieron que, si bien podían procesar 8 o 16 fragmentos o cualquier otro simultáneamente, tenían efectivamente solo un contador de programa para todos ellos (ya que eso también implica solo una unidad de captación / decodificación y uno de todos los demás, siempre y cuando trabajen en unidades de 8 o 16 píxeles). De ahí la prohibición inicial de los condicionales y, durante un tiempo después de eso, la situación en la que si las evaluaciones condicionales para los píxeles que se procesarían juntas devolvieran valores diferentes, esos píxeles se procesarían en grupos más pequeños en alguna disposición.

Aunque PowerVR no es explícito, sus recomendaciones de desarrollo de aplicaciones tienen una sección sobre control de flujo y hacen muchas recomendaciones sobre ramas dinámicas, por lo general, es una buena idea solo cuando el resultado es razonablemente predecible, lo que me hace pensar que están obteniendo el mismo tipo de cosa. Por lo tanto, sugiero que la disparidad de velocidad puede deberse a que ha incluido un condicional.

Como primera prueba, ¿qué ocurre si pruebas lo siguiente?

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


Muchos de estos puntos han sido cubiertos por otros que han publicado respuestas, pero el tema principal aquí es que su representación hace mucho trabajo que será desechada:

  1. El sombreador mismo hace un trabajo potencialmente redundante. Es probable que la longitud de un vector se calcule como sqrt(dot(vector, vector)) . No necesita el sqrt para rechazar fragmentos fuera del círculo, y está cuadrando la longitud para calcular la profundidad, de todos modos. Además, ¿ha analizado si la cuantificación explícita de los valores de profundidad es o no necesaria, o puede salirse con la suya simplemente utilizando la conversión del hardware de coma flotante a entero para el framebuffer (potencialmente con un sesgo adicional para asegurarse de que su cuasi ¿Las pruebas de profundidad saldrán más tarde?

  2. Muchos fragmentos están trivialmente fuera del círculo. Solo π / 4 del área de los cuádriceps que está dibujando produce valores de profundidad útiles. En este punto, imagino que su aplicación está muy sesgada hacia el procesamiento de fragmentos, por lo que puede considerar aumentar la cantidad de vértices que dibuje a cambio de una reducción en el área que debe sombrear. Como dibuja esferas a través de una proyección ortográfica, cualquier polígono regular circunscrito servirá, aunque es posible que necesite un pequeño tamaño adicional según el nivel de zoom para asegurarse de que rasterice suficientes píxeles.

  3. Muchos fragmentos están trivialmente ocluidos por otros fragmentos. Como han señalado otros, no está utilizando la prueba de profundidad del hardware y, por lo tanto, no aprovecha al máximo la capacidad de TBDR de eliminar el trabajo de sombreado antes de tiempo. Si ya has implementado algo para 2), todo lo que tienes que hacer es dibujar un polígono regular inscrito a la profundidad máxima que puedas generar (un plano en el centro de la esfera), y dibujar tu polígono real a la profundidad mínima (el frente de la esfera). Tanto las publicaciones de Tommy como las de Rotoglup ya contienen los detalles del vector de estado.

Tenga en cuenta que 2) y 3) también se aplican a sus sombreadores de raytracing.


No soy un experto en plataformas móviles, pero creo que lo que te pica es eso:

  • su sombreador de profundidad es bastante caro
  • Experimente un sobregiro masivo en su pase de profundidad cuando desactive la prueba GL_DEPTH

¿No sería útil un pase adicional, dibujado antes de la prueba de profundidad?

Este pase podría hacer un GL_DEPTH GL_DEPTH, por ejemplo, dibujando cada esfera representada como una cámara de cuatro lados (o un cubo, que puede ser más fácil de configurar), y contenida en la esfera asociada. Este pase se puede dibujar sin máscara de color o sombreador de fragmentos, solo con GL_DEPTH_TEST y glDepthMask habilitados. En plataformas de escritorio, este tipo de pases se dibuja más rápido que el color + profundidad.

Luego, en su paso de cálculo de profundidad, puede habilitar GL_DEPTH_TEST y desactivar glDepthMask , de esta manera su sombreador no se ejecutará en píxeles que están ocultos por una geometría más cercana.

Esta solución implicaría emitir otro conjunto de llamadas de sorteo, por lo que esto puede no ser beneficioso.