Partage de code entre plusieurs shaders GLSL

30

Je me retrouve souvent à copier-coller du code entre plusieurs shaders. Cela inclut à la fois certains calculs ou données partagés entre tous les shaders dans un même pipeline, et des calculs communs dont tous mes vertex shaders ont besoin (ou toute autre étape).

Bien sûr, c'est une pratique horrible: si je dois changer le code n'importe où, je dois m'assurer de le changer partout ailleurs.

Existe-t-il une meilleure pratique acceptée pour garder au SEC ? Les gens ajoutent-ils simplement un seul fichier commun à tous leurs shaders? Écrivent-ils leur propre préprocesseur rudimentaire de style C qui analyse les #includedirectives? S'il existe des modèles acceptés dans l'industrie, j'aimerais les suivre.

Martin Ender
la source
4
Cette question peut être un peu controversée, car plusieurs autres sites SE ne veulent pas de questions sur les meilleures pratiques. C'est intentionnel de voir comment cette communauté se positionne face à ces questions.
Martin Ender
2
Hmm, ça me va bien. Je dirais que nous sommes dans une large mesure un peu "plus larges" / "plus généraux" dans nos questions que, disons, StackOverflow.
Chris dit Réintégrer Monica
2
StackOverflow est passé d'un tableau «demandez-nous» à un tableau «ne nous demandez pas, sauf si vous devez faire plaisir».
intérieur du
S'il s'agit de déterminer la pertinence du sujet, qu'en est-il d'une question Meta associée?
SL Barth - Reinstate Monica

Réponses:

18

Il y a un tas d'approches, mais aucune n'est parfaite.

Il est possible de partager du code en utilisant glAttachShaderpour combiner des shaders, mais cela ne permet pas de partager des choses comme des déclarations de structure ou des #defineconstantes -d. Cela fonctionne pour le partage des fonctions.

Certaines personnes aiment utiliser le tableau de chaînes passé pour glShaderSourceajouter des définitions communes avant votre code, mais cela présente certains inconvénients:

  1. Il est plus difficile de contrôler ce qui doit être inclus depuis le shader (vous avez besoin d'un système séparé pour cela.)
  2. Cela signifie que l'auteur du shader ne peut pas spécifier le GLSL #version, en raison de la déclaration suivante dans la spécification GLSL:

La directive #version doit apparaître avant tout dans un shader, sauf pour les commentaires et les espaces blancs.

En raison de cette déclaration, glShaderSourcene peut pas être utilisé pour ajouter du texte avant les #versiondéclarations. Cela signifie que la #versionligne doit être incluse dans vos glShaderSourcearguments, ce qui signifie que votre interface de compilation GLSL doit en quelque sorte être informée de la version de GLSL qui devrait être utilisée. De plus, le fait de ne pas spécifier de #versionrendra le compilateur GLSL par défaut pour utiliser GLSL version 1.10. Si vous souhaitez laisser les auteurs de shader spécifier le #versioncontenu du script de manière standard, vous devez en quelque sorte insérer #include-s après l' #versioninstruction. Cela pourrait être fait en analysant explicitement le shader GLSL pour trouver la #versionchaîne (si elle est présente) et faire vos inclusions après, mais en ayant accès à un#includeLa directive pourrait être préférable de contrôler plus facilement quand ces inclusions doivent être faites. D'autre part, puisque GLSL ignore les commentaires avant la #versionligne, vous pouvez ajouter des métadonnées pour les inclus dans les commentaires en haut de votre fichier (beurk).

La question est maintenant: existe-t-il une solution standard pour #include, ou avez-vous besoin de rouler votre propre extension de préprocesseur?

Il y a l' GL_ARB_shading_language_includeextension, mais elle a quelques inconvénients:

  1. Il est uniquement pris en charge par NVIDIA ( http://delphigl.de/glcapsviewer/listreports2.php?listreportsbyextension=GL_ARB_shading_language_include )
  2. Il fonctionne en spécifiant les chaînes d'inclusion à l'avance. Par conséquent, avant de compiler, vous devez spécifier que la chaîne "/buffers.glsl"(utilisée dans #include "/buffers.glsl") correspond au contenu du fichier buffer.glsl(que vous avez chargé précédemment).
  3. Comme vous l'avez peut-être remarqué au point (2), vos chemins doivent commencer par "/", comme les chemins absolus de style Linux. Cette notation n'est généralement pas familière aux programmeurs C et signifie que vous ne pouvez pas spécifier de chemins relatifs.

Une conception courante consiste à implémenter votre propre #includemécanisme, mais cela peut être délicat car vous devez également analyser (et évaluer) d'autres instructions de préprocesseur comme #ifpour gérer correctement la compilation conditionnelle (comme les gardes d'en-tête.)

Si vous implémentez le vôtre #include, vous avez également quelques libertés dans la façon dont vous souhaitez l'implémenter:

  • Vous pouvez passer des chaînes à l'avance (comme GL_ARB_shading_language_include).
  • Vous pouvez spécifier un rappel d'inclusion (cela se fait par la bibliothèque D3DCompiler de DirectX.)
  • Vous pouvez implémenter un système qui lit toujours directement à partir du système de fichiers, comme dans les applications C typiques.

Pour simplifier, vous pouvez insérer automatiquement des protections d'en-tête pour chaque inclusion dans votre couche de prétraitement, afin que votre couche de processeur ressemble à:

if (#include and not_included_yet) include_file();

(Nous remercions Trent Reed de m'avoir montré la technique ci-dessus.)

En conclusion , il n'existe pas de solution automatique, standard et simple. Dans une future solution, vous pourriez utiliser une interface OpenGL SPIR-V, auquel cas le compilateur GLSL vers SPIR-V pourrait être en dehors de l'API GL. Le fait d'avoir le compilateur en dehors du runtime OpenGL simplifie considérablement la mise en œuvre de choses comme, #includecar c'est un endroit plus approprié pour s'interfacer avec le système de fichiers. Je crois que la méthode répandue actuelle consiste simplement à implémenter un préprocesseur personnalisé qui fonctionne d'une manière que tout programmeur C devrait être familier.

Nicolas Louis Guillemot
la source
Les shaders peuvent également être séparés en modules à l'aide de glslify , bien que cela ne fonctionne qu'avec node.js.
Anderson Green
9

J'utilise généralement le fait que glShaderSource (...) accepte un tableau de chaînes comme entrée.

J'utilise un fichier de définition de shader basé sur json, qui spécifie comment un shader (ou un programme pour être plus correct) est composé, et là je spécifie le préprocesseur définit dont j'ai besoin, les uniformes qu'il utilise, le fichier de shaders de vertex / fragment, et tous les fichiers "dépendances" supplémentaires. Ce ne sont que des collections de fonctions qui sont ajoutées à la source avant la source réelle du shader.

Juste pour ajouter, AFAIK, l'Unreal Engine 4 utilise une directive #include qui est analysée et ajoute tous les fichiers pertinents, avant la compilation, comme vous le suggérez.

Matteo Bertello
la source
4

Je ne pense pas qu'il existe une convention commune, mais si je devine, je dirais que presque tout le monde implémente une forme simple d'inclusion textuelle comme étape de prétraitement (une #includeextension), car c'est très facile à faire alors. (En JavaScript / WebGL, vous pouvez le faire avec une simple expression régulière, par exemple). L'avantage de cela est que vous pouvez effectuer le prétraitement dans une étape hors ligne pour les versions "release", lorsque le code du shader n'a plus besoin d'être modifié.

En fait, une indication que cette approche est commune est le fait qu'une extension ARB a été introduite pour que: GL_ARB_shading_language_include. Je ne sais pas si cela est devenu une fonctionnalité principale maintenant ou non, mais l'extension a été écrite contre OpenGL 3.2.

glampert
la source
2
GL_ARB_shading_language_include n'est pas une fonctionnalité principale. En fait, seul NVIDIA le prend en charge. ( delphigl.de/glcapsviewer/… )
Nicolas Louis Guillemot
4

Certaines personnes ont déjà souligné que cela glShaderSourcepeut prendre un certain nombre de chaînes.

De plus, dans GLSL, la compilation ( glShaderSource, glCompileShader) et la liaison ( glAttachShader, glLinkProgram) du shader sont séparées.

Je l'ai utilisé dans certains projets pour diviser les shaders entre la partie spécifique et les parties communes à la plupart des shaders, qui est ensuite compilée et partagée avec tous les programmes de shaders. Cela fonctionne et n'est pas difficile à implémenter: il suffit de maintenir une liste de dépendances.

En termes de maintenabilité cependant, je ne suis pas sûr que ce soit une victoire. L'observation était la même, essayons de factoriser. Bien qu'il évite en effet la répétition, les frais généraux de la technique semblent importants. De plus, le shader final est plus difficile à extraire: vous ne pouvez pas simplement concaténer les sources du shader, car les déclarations se terminent dans un ordre que certains compilateurs rejetteront ou seront dupliqués. Il est donc plus difficile de faire un test de shader rapide dans un outil séparé.

Au final, cette technique résout certains problèmes SECS, mais elle est loin d'être idéale.

Sur un sujet secondaire, je ne sais pas si cette approche a un impact en termes de temps de compilation; J'ai lu que certains pilotes ne compilent vraiment le programme de shader que sur la liaison, mais je n'ai pas mesuré.

Julien Guertault
la source
D'après ma compréhension, je pense que cela ne résout pas le problème du partage des définitions de structure.
Nicolas Louis Guillemot
@NicolasLouisGuillemot: oui vous avez raison, seul le code des instructions est partagé de cette façon, pas les déclarations.
Julien Guertault