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 #line
commandes, 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 glVertexAttribPointer
appels 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.