Utilisation de deux shaders au lieu d'un avec des instructions IF


9

J'ai travaillé sur le portage d'une source opengl ES 1.1 relativement grande vers ES 2.0.

Dans OpenGL ES 2.0 (ce qui signifie que tout utilise des shaders), je veux dessiner une théière trois fois.

  1. Le premier, avec une couleur uniforme (ala l'ancien glColor4f).

  2. Le second, avec une couleur par sommet (la théière a également son tableau de couleurs de sommet)

  3. Le troisième, avec une texture par sommet

  4. Et peut-être un quatrième, avec une texture et une couleur par sommet. Et puis peut-être un 5ème, avec des normales aussi ..

Pour autant que je sache, j'ai deux choix avec l'implémentation. La première consiste à créer un shader qui prend en charge tout ce qui précède, avec un uniforme qui est configuré pour changer le comportement (par exemple, utilisez l'uniforme de couleur singulière ou l'uniforme de couleur par sommet).

Le deuxième choix consiste à créer un shader différent pour chaque situation. Avec certains prétraitements de shaders personnalisés, ce n'est pas si compliqué à faire, mais le problème est le coût de performance pour basculer les shaders entre les objets de dessin. J'ai lu que ce n'est pas trivialement petit.

Je veux dire, la meilleure façon de procéder est de construire les deux et de mesurer, mais ce serait bien d'entendre toutes les entrées.

Réponses:


10

Le coût de performance de la ramification ne peut pas non plus être insignifiant. Dans votre cas, tous les sommets et fragments dessinés emprunteront le même chemin à travers vos shaders, donc sur le matériel de bureau moderne , ce ne serait pas aussi mauvais que cela pourrait l'être, mais vous utilisez ES2, ce qui implique que vous n'utilisez pas moderne matériel de bureau.

Le pire des cas avec la ramification ressemblera à ceci:

  • les deux côtés de la branche sont évalués.
  • une instruction "mix" ou "step" sera générée par le compilateur de shader et insérée dans votre code pour décider de quel côté utiliser.

Et toutes ces instructions supplémentaires seront exécutées pour chaque sommet ou fragment que vous dessinez. Cela représente potentiellement des millions d'instructions supplémentaires à mettre en balance avec le coût d'un changement de shader.

Le " Guide de programmation OpenGL ES pour iOS " d' Apple (qui peut être considéré comme représentatif de votre matériel cible) a ceci à dire sur la ramification:

Évitez les branchements

Les branches sont déconseillées dans les shaders, car elles peuvent réduire la possibilité d'exécuter des opérations en parallèle sur des processeurs graphiques 3D. Si vos shaders doivent utiliser des branches, suivez ces recommandations:

  • Meilleure performance: branchez sur une constante connue lorsque le shader est compilé.
  • Acceptable: branche sur une variable uniforme.
  • Potentiellement lent: branchement sur une valeur calculée à l'intérieur du shader.

Au lieu de créer un grand shader avec de nombreux boutons et leviers, créez des shaders plus petits spécialisés pour des tâches de rendu spécifiques. Il existe un compromis entre la réduction du nombre de branches dans vos shaders et l'augmentation du nombre de shaders que vous créez. Testez différentes options et choisissez la solution la plus rapide.

Même si vous êtes satisfait que vous êtes dans la case "Acceptable" ici, vous devez toujours considérer qu'avec 4 ou 5 cas à sélectionner, vous allez augmenter le nombre d'instructions dans vos shaders. Vous devez connaître les limites du nombre d'instructions sur votre matériel cible et vous assurer de ne pas les dépasser, en citant à nouveau le lien Apple ci-dessus:

Les implémentations d'OpenGL ES ne sont pas requises pour implémenter un logiciel de secours lorsque ces limites sont dépassées; au lieu de cela, le shader échoue simplement à compiler ou à lier.

Rien de tout cela ne veut dire que la ramification n'est pas la meilleure solution pour votre besoin. Vous avez correctement identifié le fait que vous devez profiler les deux approches, c'est donc la recommandation finale. Mais sachez que, à mesure que les shaders deviennent plus complexes, une solution basée sur les branchements pourrait bien entraîner des frais généraux beaucoup plus élevés que quelques changements de shaders.


3

Le coût de la liaison des shaders n'est peut-être pas trivial, mais ce ne sera pas votre goulot d'étranglement à moins que vous ne rendiez des milliers d'articles sans regrouper tous les objets qui utilisent les mêmes shaders.

Bien que je ne sois pas sûr que cela s'applique aux appareils mobiles, mais les GPU ne sont pas horriblement lents avec des branches si la condition est entre une constante et un uniforme. Les deux sont valides, les deux ont été utilisés dans le passé et continueront d'être utilisés à l'avenir, choisissez celui qui, selon vous, serait le plus propre dans votre cas.

De plus, il existe plusieurs autres façons d'y parvenir: "Uber-shaders" et une petite astuce avec la façon dont les programmes de shaders OpenGL sont liés.

Les "Uber-shaders" sont essentiellement le premier choix, moins la ramification, mais vous aurez plusieurs shaders. Au lieu d'utiliser des ifdéclarations, vous utilisez le préprocesseur - #define, #ifdef, #else, #endifet compiler différentes versions, y compris le bon #defines pour ce dont vous avez besoin.

vec4 color;
#ifdef PER_VERTEX_COLOR
color = in_color;
#else
color = obj_color;
#endif

Vous pouvez également diviser le shader en fonctions distinctes. Avoir un shader qui définit les prototypes pour toutes les fonctions et les appelle, lier un tas de shaders supplémentaires qui incluent les implémentations appropriées. J'ai utilisé cette astuce pour le mappage d'ombres, pour faciliter la permutation du filtrage sur tous les objets sans avoir à modifier tous les shaders.

//ins, outs, uniforms

float getShadowCoefficient();

void main()
{
    //shading stuff goes here

    gl_FragColor = color * getShadowCoefficient();
}

Ensuite, je pourrais avoir plusieurs autres fichiers de shaders qui définissent getShadowCoefficient(), les uniformes nécessaires et rien d'autre. Par exemple, shadow_none.glslcontient:

float getShadowCoefficient()
{
    return 1;
}

Et shadow_simple.glslcontient (simplifié par rapport à mon shader qui implémente les CSM):

in vec4 eye_position;

uniform sampler2DShadow shad_tex;
uniform mat4 shad_mat;

float getShadowCoefficient()
{
    vec4 shad_coord = shad_mat * eye_position;
    return texture(shad_tex, shad_coord).x;
}

Et vous pouvez simplement choisir si vous souhaitez ou non ombrer en liant un ombrage différent shadow_*. Cette solution peut très bien avoir plus de surcharge, mais j'aimerais penser que le compilateur GLSL est assez bon pour optimiser toute surcharge supplémentaire par rapport à d'autres façons de le faire. Je n'ai effectué aucun test à ce sujet, mais c'est ainsi que j'aime le faire.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.