Game Engine Design - Ubershader - Conception de la gestion des shaders [fermé]

18

Je souhaite implémenter un système Ubershader flexible, avec un ombrage différé. Mon idée actuelle est de créer des shaders à partir de modules, qui traitent de certaines fonctionnalités, telles que FlatTexture, BumpTexture, Mapping de déplacement, etc. Il y a aussi de petits modules qui décodent les couleurs, font le mappage des tons, etc. remplacer certains types de modules si le GPU ne les prend pas en charge, afin que je puisse m'adapter aux capacités actuelles du GPU. Je ne sais pas si cette conception est bonne. Je crains de ne pouvoir faire un mauvais choix de conception, maintenant et plus tard.

Ma question est de savoir où trouver des ressources, des exemples, des articles sur la façon de mettre en œuvre efficacement un système de gestion de shaders? Est-ce que quelqu'un sait comment les moteurs de gros gibier font cela?

Michael Staud
la source
3
Pas assez longtemps pour une vraie réponse: vous ferez bien avec cette approche si vous commencez petit et laissez-le grandir organiquement en fonction de vos besoins au lieu d'essayer de construire la MegaCity-One de shaders à l'avant. Premièrement, vous atténuez votre plus gros souci de faire trop de conception à l'avance et de le payer plus tard si cela ne fonctionne pas, deuxièmement, vous évitez de faire un travail supplémentaire qui ne sera jamais utilisé.
Patrick Hughes
Malheureusement, nous n'acceptons plus les questions de "demande de ressources".
Gnemlock

Réponses:

23

Une approche semi-courante consiste à rendre ce que j'appelle des composants de shader , similaire à ce que je pense que vous appelez des modules.

L'idée est similaire à un graphique de post-traitement. Vous écrivez des morceaux de code de shader qui incluent à la fois les entrées nécessaires, les sorties générées, puis le code pour réellement travailler dessus. Vous avez une liste qui indique les shaders à appliquer dans n'importe quelle situation (si ce matériau a besoin d'un composant de mappage de relief, si le composant différé ou avancé est activé, etc.).

Vous pouvez maintenant prendre ce graphique et en générer du code shader. Cela signifie principalement "coller" le code des morceaux en place, avec le graphique ayant déjà assuré qu'ils sont dans l'ordre nécessaire, puis coller dans les entrées / sorties du shader comme approprié (dans GLSL, cela signifie définir votre "global" dans , out et variables uniformes).

Ce n'est pas la même chose qu'une approche ubershader. Les Ubershaders sont l'endroit où vous mettez tout le code nécessaire pour tout dans un seul ensemble de shaders, peut-être en utilisant #ifdefs et uniformes et autres pour activer et désactiver les fonctionnalités lors de la compilation ou de leur exécution. Je méprise personnellement l'approche ubershader, mais certains moteurs AAA plutôt impressionnants les utilisent (Crytek en particulier me vient à l'esprit).

Vous pouvez gérer les morceaux de shader de plusieurs manières. Le moyen le plus avancé - et utile si vous prévoyez de prendre en charge GLSL, HLSL et les consoles - est d'écrire un analyseur pour un langage de shader (probablement aussi proche de HLSL / Cg ou GLSL que possible pour une "compréhensibilité" maximale par vos développeurs). ) qui peut ensuite être utilisé pour les traductions de source à source. Une autre approche consiste à simplement envelopper des morceaux de shader dans des fichiers XML ou similaires, par exemple

<shader name="example" type="pixel">
  <input name="color" type="float4" source="vertex" />
  <output name="color" type="float4" target="output" index="0" />
  <glsl><![CDATA[
     output.color = vec4(input.color.r, 0, 0, 1);
  ]]></glsl>
</shader>

Notez qu'avec cette approche, vous pouvez créer plusieurs sections de code pour différentes API ou même versionner la section de code (vous pouvez donc avoir une version GLSL 1.20 et une version GLSL 3.20). Votre graphique peut même exclure automatiquement des morceaux de shader qui n'ont pas de section de code compatible afin que vous puissiez obtenir une dégradation semi-gracieuse sur du matériel plus ancien (donc quelque chose comme un mappage normal ou tout ce qui est juste exclu sur du matériel plus ancien qui ne peut pas le prendre en charge sans que le programmeur ait besoin de faire un tas de vérifications explicites).

L'exemple XMl peut alors générer quelque chose de similaire (excuses s'il s'agit d'un GLSL invalide, cela fait un moment que je ne me suis pas soumis à cette API):

layout (location=0) in vec4 input_color;
layout (location=0) out vec4 output_color;

struct Input {
  vec4 color;
};
struct Output {
  vec4 color;
}

void main() {
  Input input;
  input.color = input_color;
  Output output;

  // Source: example.shader
#line 5
  output.color = vec4(input.color.r, 0, 0, 1);

  output_color = output.color;
}

Vous pourriez être un peu plus intelligent et générer du code plus "efficace", mais honnêtement, tout compilateur de shader qui n'est pas de la merde totale supprimera pour vous les redondances de ce code généré. Peut-être que le GLSL plus récent vous permet également de mettre le nom de fichier dans les #linecommandes, mais je sais que les anciennes versions sont très déficientes et ne le prennent pas en charge.

Si vous avez plusieurs morceaux, leurs entrées (qui ne sont pas fournies en sortie par un morceau ancêtre dans l'arborescence) sont concaténées dans le bloc d'entrée, tout comme les sorties, et le code est juste concaténé. Un petit travail supplémentaire est effectué pour s'assurer que les étapes correspondent (vertex vs fragment) et que les dispositions d'entrée d'attribut vertex "fonctionnent juste". Un autre avantage intéressant de cette approche est que vous pouvez écrire des indices de liaison d'attributs uniformes et d'entrée explicites qui ne sont pas pris en charge dans les anciennes versions de GLSL et les gérer dans votre bibliothèque de génération / liaison de shaders. De même, vous pouvez utiliser les métadonnées dans la configuration de vos VBO et glVertexAttribPointerappels pour assurer la compatibilité et que tout "fonctionne".

Malheureusement, il n'existe pas déjà de bonne bibliothèque multi-API comme celle-ci. Cg se rapproche un peu, mais il a un support de merde pour OpenGL sur les cartes AMD et peut être extrêmement lent si vous utilisez des fonctionnalités de génération de code autres que les plus élémentaires. Le framework d'effets DirectX fonctionne également mais n'a bien sûr aucune prise en charge pour tout autre langage que HLSL. Il existe des bibliothèques incomplètes / boguées pour GLSL qui imitent les bibliothèques DirectX, mais étant donné leur état la dernière fois que j'ai vérifié, j'écrirais juste la mienne.

L'approche ubershader signifie simplement définir des directives de préprocesseur "bien connues" pour certaines fonctionnalités, puis recompiler pour différents matériaux avec une configuration différente. Par exemple, pour tout matériau avec une carte normale, vous pouvez définir USE_NORMAL_MAPPING=1, puis dans votre ubershader au stade pixel, il vous suffit de:

#if USE_NORMAL_MAPPING
  vec4 normal;
  // all your normal mapping code
#else
  vec4 normal = normalize(in_normal);
#endif

Un gros problème ici est de gérer cela pour HLSL précompilé, où vous devez précompiler toutes les combinaisons utilisées. Même avec GLSL, vous devez être capable de générer correctement une clé de toutes les directives de préprocesseur utilisées pour éviter de recompiler / mettre en cache des shaders identiques. L'utilisation d'uniformes peut réduire la complexité, mais contrairement aux préprocesseurs, les uniformes ne réduisent pas le nombre d'instructions et peuvent encore avoir un impact mineur sur les performances.

Juste pour être clair, les deux approches (ainsi que l'écriture manuelle d'une tonne de variations de shaders) sont toutes utilisées dans l'espace AAA. Utilisez celui qui vous convient le mieux.

Sean Middleditch
la source