39 votes

Comment puis-je améliorer les performances de ma génération personnalisée de texture de profondeur OpenGL ES 2.0 ?

J'ai une application iOS open source qui utilise des shaders OpenGL ES 2.0 personnalisés pour afficher des représentations 3D de structures moléculaires. Pour ce faire, elle utilise des imposteurs de sphères et de cylindres générés de manière procédurale et dessinés sur des rectangles, au lieu de ces mêmes formes construites à l'aide de nombreux sommets. L'inconvénient de cette approche est que les valeurs de profondeur pour chaque fragment de ces objets imposteurs doivent être calculées dans un fragment shader, pour être utilisées lorsque les objets se chevauchent.

Malheureusement, OpenGL ES 2.0 ne vous permet pas d'écrire dans gl_FragDepth J'ai donc eu besoin de transmettre ces valeurs à une texture de profondeur personnalisée. J'effectue un passage sur ma scène en utilisant un objet framebuffer (FBO), ne rendant qu'une couleur correspondant à une valeur de profondeur, les résultats étant stockés dans une texture. Cette texture est ensuite chargée dans la deuxième partie de mon processus de rendu, où l'image d'écran réelle est générée. Si un fragment à ce stade se trouve au niveau de profondeur stocké dans la texture de profondeur pour ce point de l'écran, il est affiché. Dans le cas contraire, il est jeté. Pour plus d'informations sur le processus, y compris des diagrammes, consultez mon article. aquí .

La génération de cette texture de profondeur est un goulot d'étranglement dans mon processus de rendu et je cherche un moyen de l'accélérer. Elle semble plus lente qu'elle ne devrait l'être, mais je n'arrive pas à comprendre pourquoi. Afin d'obtenir la génération correcte de cette texture de profondeur, GL_DEPTH_TEST est désactivé, GL_BLEND est activé avec glBlendFunc(GL_ONE, GL_ONE) y glBlendEquation() est réglé sur GL_MIN_EXT . Je sais qu'une scène produite de cette manière n'est pas la plus rapide sur un système de rendu différé basé sur des tuiles comme la série PowerVR des appareils iOS, mais je ne vois pas de meilleur moyen de le faire.

Mon shader de fragment de profondeur pour les sphères (l'élément d'affichage le plus courant) semble être au cœur de ce goulot d'étranglement (l'utilisation du moteur de rendu dans Instruments est évaluée à 99 %, ce qui indique que je suis limité par le traitement des fragments). La situation actuelle est la suivante :

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

Sur un iPad 1, il faut 35 à 68 ms pour rendre une image d'un modèle de remplissage de l'espace par l'ADN en utilisant un shader passthrough pour l'affichage (18 à 35 ms sur un iPhone 4). D'après le compilateur PowerVR PVRUniSCo (qui fait partie de l'application leur SDK ), ce shader utilise 11 cycles GPU au mieux, 16 cycles au pire. Je suis conscient que l'on vous conseille de ne pas utiliser de branchement dans un shader, mais dans ce cas, cela a conduit à de meilleures performances qu'autrement.

Quand je le simplifie en

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

il faut 18 à 35 ms sur l'iPad 1, mais seulement 1,7 à 2,4 ms sur l'iPhone 4. Le nombre de cycles GPU estimé pour ce shader est de 8 cycles. La modification du temps de rendu en fonction du nombre de cycles ne semble pas linéaire.

Enfin, si je sors juste une couleur constante :

precision mediump float;

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

le temps de rendu tombe à 1,1 - 2,3 ms sur l'iPad 1 (1,3 ms sur l'iPhone 4).

L'échelle non linéaire du temps de rendu et le changement soudain entre l'iPad et l'iPhone 4 pour le deuxième shader me font penser qu'il y a quelque chose qui m'échappe ici. Un projet source complet contenant ces trois variantes de shaders (regardez dans le fichier SphereDepth.fsh et commentez les sections appropriées) et un modèle de test peut être téléchargé à l'adresse suivante aquí si vous souhaitez essayer vous-même.

Si vous avez lu jusqu'ici, ma question est la suivante : sur la base de ces informations de profilage, comment puis-je améliorer les performances de rendu de mon shader de profondeur personnalisé sur les appareils iOS ?

19voto

Brad Larson Points 122629

Sur la base des recommandations de Tommy, Pivot et rotoglup, j'ai implémenté quelques optimisations qui ont conduit à un doublement de la vitesse de rendu à la fois pour la génération de texture de profondeur et pour le pipeline de rendu global de l'application.

Tout d'abord, j'ai réactivé la profondeur de sphère précalculée et la texture d'éclairage que j'avais utilisées auparavant avec peu d'effet, sauf que maintenant, j'utilise la texture appropriée. lowp lors de la manipulation des couleurs et des autres valeurs de cette texture. Cette combinaison, ainsi qu'un mipmapping approprié pour la texture, semble produire un gain de performance d'environ 10 %.

Plus important encore, je fais maintenant une passe avant de rendre à la fois ma texture de profondeur et les imposteurs finaux de raytraced où je pose une géométrie opaque pour bloquer les pixels qui ne seront jamais rendus. Pour ce faire, j'active le test de profondeur, puis je dessine les carrés qui constituent les objets de ma scène, rétrécis par sqrt(2) / 2, avec un simple shader opaque. Cela créera des carrés d'insertion couvrant la zone connue pour être opaque dans une sphère représentée.

Je désactive ensuite les écritures en profondeur en utilisant glDepthMask(GL_FALSE) et rendre l'imposteur sphère carrée à un endroit plus proche de l'utilisateur d'un rayon. Cela permet au matériel de rendu différé basé sur les tuiles des appareils iOS d'éliminer efficacement les fragments qui n'apparaîtraient jamais à l'écran, quelles que soient les conditions, tout en donnant des intersections lisses entre les imposteurs de sphères visibles sur la base des valeurs de profondeur par pixel. C'est ce que montre l'illustration grossière ci-dessous :

Layered spheres and opacity testing

Dans cet exemple, les carrés de blocage opaques des deux imposteurs supérieurs n'empêchent pas le rendu des fragments de ces objets visibles, mais ils bloquent une partie des fragments de l'imposteur inférieur. Les imposteurs les plus en avant peuvent alors utiliser des tests par pixel pour générer une intersection lisse, tandis que de nombreux pixels de l'imposteur arrière ne gaspillent pas les cycles du GPU en étant rendus.

Je n'avais pas pensé à désactiver les écritures de profondeur, tout en laissant les tests de profondeur lors de la dernière étape de rendu. C'est la clé pour empêcher les imposteurs de s'empiler les uns sur les autres, tout en utilisant certaines des optimisations matérielles des GPU PowerVR.

Dans mes benchmarks, le rendu du modèle de test que j'ai utilisé ci-dessus donne des temps de 18 à 35 ms par image, par rapport aux 35 à 68 ms que j'obtenais auparavant, soit un quasi-doublement de la vitesse de rendu. L'application de ce même pré-rendu de géométrie opaque à la passe de raytracing permet de doubler les performances globales de rendu.

Curieusement, lorsque j'ai essayé d'affiner ce processus en utilisant des octogones insérés et circonscrits, qui devraient couvrir ~17% de pixels en moins lorsqu'ils sont dessinés, et être plus efficaces avec les fragments de blocage, les performances étaient en fait pires que lorsque j'utilisais de simples carrés pour cela. L'utilisation des tuiles était toujours inférieure à 60% dans le pire des cas, donc peut-être que la géométrie plus grande entraînait plus de manques dans le cache.

MODIFIER (31/05/2011) :

Sur la base de la suggestion de Pivot, j'ai créé des octogones inscrits et circonscrits à utiliser à la place de mes rectangles, seulement j'ai suivi les recommandations suivantes aquí pour optimiser les triangles pour le rasterization. Lors de tests précédents, les octogones ont donné des performances inférieures à celles des carrés, bien qu'ils aient supprimé de nombreux fragments inutiles et permis de bloquer plus efficacement les fragments couverts. En ajustant le dessin du triangle comme suit :

Rasterization optimizing octagons

J'ai pu réduire le temps de rendu global de 14 % en moyenne, en plus des optimisations décrites ci-dessus, en passant des carrés aux octogones. La texture de profondeur est maintenant générée en 19 ms, avec des baisses occasionnelles à 2 ms et des pics à 35 ms.

EDIT 2 (5/31/2011) :

J'ai réexaminé l'idée de Tommy d'utiliser la fonction de pas, maintenant que j'ai moins de fragments à rejeter à cause des octogones. Ceci, combiné avec une texture de recherche de profondeur pour la sphère, conduit maintenant à un temps de rendu moyen de 2 ms sur l'iPad 1 pour la génération de la texture de profondeur pour mon modèle de test. Je considère que c'est à peu près ce que je pouvais espérer dans ce cas de rendu, et que c'est une amélioration considérable par rapport à mon point de départ. Pour la postérité, voici le shader de profondeur que j'utilise maintenant :

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

J'ai mis à jour l'échantillon de test aquí si vous souhaitez voir cette nouvelle approche en action par rapport à ce que je faisais initialement.

Je suis toujours ouvert à d'autres suggestions, mais c'est un grand pas en avant pour cette application.

9voto

Tommy Points 56749

Sur le bureau, c'était le cas à de nombreuses début de dispositifs programmables que, bien qu'ils pourraient processus de 8 ou 16 ou n'importe quels fragments simultanément, ils ont effectivement eu un seul compteur de programme, pour beaucoup d'entre eux (puisque cela suppose également que seul un fetch/décoder et une unité de tout le reste, tant qu'ils travaillent dans des unités de 8 ou 16 pixels). Donc la première interdiction sur les conditions et, un moment après, la situation où, si la condition des évaluations pour les pixels qui seront traités ensemble retourné des valeurs différentes, les pixels seraient transformés en petits groupes dans un arrangement.

Bien que PowerVR ne sont pas explicites, leur application des recommandations de développement ont une section sur le contrôle des flux et de faire beaucoup de recommandations sur la dynamique des branches étant généralement une bonne idée seulement lorsque le résultat est raisonnablement prévisible, ce qui me fait penser qu'ils sont en arriver à la même chose. J'avais suggèrent donc que la vitesse de disparité peut-être parce que vous avez inclus un conditionnel.

Comme un premier test, ce qui se passe si vous essayez ce qui suit?

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

}

8voto

Pivot Points 2858

Beaucoup de ces points ont été abordés par d'autres personnes qui ont posté des réponses, mais le thème principal ici est que votre rendu fait beaucoup de travail qui sera jeté :

  1. Le shader lui-même effectue un travail potentiellement redondant. La longueur d'un vecteur est susceptible d'être calculée comme suit sqrt(dot(vector, vector)) . Vous n'avez pas besoin du sqrt pour rejeter les fragments à l'extérieur du cercle, et vous faites la quadrature du cercle de la longueur pour calculer la profondeur, de toute façon. De plus, avez-vous examiné si une quantification explicite des valeurs de profondeur est réellement nécessaire, ou pouvez-vous vous contenter d'utiliser la conversion matérielle de la virgule flottante en entier pour le framebuffer (avec potentiellement un biais supplémentaire pour vous assurer que vos tests de quasi-profondeur sont corrects par la suite) ?

  2. De nombreux fragments sont trivialement en dehors du cercle. Seuls π/4 de la surface des quads que vous dessinez produisent des valeurs de profondeur utiles. À ce stade, j'imagine que votre application est fortement orientée vers le traitement des fragments, vous pouvez donc envisager d'augmenter le nombre de sommets que vous dessinez en échange d'une réduction de la zone que vous devez ombrer. Puisque vous dessinez des sphères à travers une projection orthographique, n'importe quel polygone régulier circonscrit fera l'affaire, bien que vous puissiez avoir besoin d'un peu plus de taille en fonction du niveau de zoom pour vous assurer que vous tramez suffisamment de pixels.

  3. De nombreux fragments sont trivialement occultés par d'autres fragments. Comme d'autres l'ont souligné, vous n'utilisez pas le test de profondeur matériel et ne profitez donc pas pleinement de la capacité d'un TBDR à tuer le travail d'ombrage dès le début. Si vous avez déjà implémenté quelque chose pour 2), tout ce que vous avez à faire est de dessiner un polygone régulier inscrit à la profondeur maximale que vous pouvez générer (un plan passant par le milieu de la sphère), et de dessiner votre polygone réel à la profondeur minimale (l'avant de la sphère). Les posts de Tommy et de rotoglup contiennent déjà les spécificités du vecteur d'état.

Notez que les points 2) et 3) s'appliquent également à vos shaders de raytracing.

2voto

rotoglup Points 3662

Je ne suis pas du tout un expert en plateformes mobiles, mais je pense que ce qui vous mord, c'est ça :

  • votre shader de profondeur est assez coûteux
  • l'expérience d'une surcharge massive dans votre passe de profondeur comme vous désactivez GL_DEPTH test

Un passage supplémentaire, effectué avant le test de profondeur, ne serait-il pas utile ?

Cette passe pourrait faire un GL_DEPTH pré-remplissage, par exemple en dessinant chaque sphère représentée comme un quadruple face caméra (ou un cube, cela peut être plus facile à mettre en place), et contenue dans la sphère associée. Cette passe pourrait être dessinée sans color mask ou fragment shader, juste avec GL_DEPTH_TEST y glDepthMask activé. Sur les plates-formes de bureau, ce type de passes est dessiné plus rapidement que les passes de couleur + profondeur.

Ensuite, dans votre passe de calcul de la profondeur, vous pourriez activer GL_DEPTH_TEST et de désactiver glDepthMask De cette façon, votre shader ne sera pas exécuté sur des pixels qui sont cachés par une géométrie plus proche.

Cette solution impliquerait l'émission d'une autre série d'appels de tirage, ce qui n'est pas forcément avantageux.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X