Dans un système de matériaux basé sur des graphiques, comment puis-je prendre en charge une variété de types d'entrée et de sortie?

11

entrez la description de l'image ici

J'essaie de comprendre comment les systèmes matériels comme celui - ci sont mis en œuvre. Ces systèmes puissants et conviviaux de type graphique semblent être relativement courants comme méthode permettant aux programmeurs et aux non-programmeurs de créer rapidement des shaders. Cependant, à partir de mon expérience relativement limitée en programmation graphique, je ne sais pas exactement comment ils fonctionnent.


Contexte:

Ainsi, lorsque j'ai déjà programmé des systèmes de rendu OpenGL simples , je crée généralement une classe Material qui charge, compile et lie des shaders à partir de fichiers GLSL statiques que j'ai créés manuellement. Je crée également généralement cette classe comme un simple wrapper pour accéder aux variables uniformes GLSL. À titre d'exemple simple, imaginez que j'ai un vertex shader de base et un fragment shader, avec un Texture2D extra uniforme pour passer une texture. Ma classe Material chargerait et compilerait simplement ces deux shaders dans un matériau, et à partir de là, elle exposerait une interface simple pour lire / écrire l'uniforme Texture2D de ce shader.

Pour rendre ce système un peu plus flexible, je l'écris généralement d'une manière qui me permet d'essayer de passer des uniformes de n'importe quel nom / type [par exemple: SetUniform_Vec4 ("AmbientColor", colorVec4); qui définirait l'uniforme AmbientColor sur un vecteur 4d particulier appelé "colorVec4" si cet uniforme existe dans le matériau.] .

class Material
{
    private:
       int shaderID;
       string vertShaderPath;
       string fragSahderPath;

       void loadShaderFiles(); //load shaders from files at internal paths.
       void buildMaterial(); //link, compile, buffer with OpenGL, etc.      

    public:
        void SetGenericUniform( string uniformName, int param );
        void SetGenericUniform( string uniformName, float param );
        void SetGenericUniform( string uniformName, vec4 param );
        //overrides for various types, etc...

        int GetUniform( string uniformName );
        float GetUniform( string uniformName );
        vec4 GetUniform( string uniformName );
        //etc...

        //ctor, dtor, etc., omitted for clarity..
}

Cela fonctionne, mais cela ressemble à un mauvais système car le client de la classe Material doit accéder aux uniformes sur la foi seule - l' utilisateur doit être quelque peu conscient des uniformes qui se trouvent dans chaque objet matériel car ils sont obligés de passez-les par leur nom GLSL. Ce n'est pas énorme quand ce n'est que 1-2 personnes qui travaillent avec le système, mais je ne peux pas imaginer que ce système évoluerait très bien du tout, et avant de faire ma prochaine tentative de programmation d'un système de rendu OpenGL, je veux mettre à niveau un peu.


Question:

C'est où j'en suis jusqu'à présent, j'ai donc essayé d'étudier comment d'autres moteurs de rendu gèrent leurs systèmes de matériaux.

Cette approche basée sur les nœuds est excellente et il semble que ce soit un système extrêmement courant pour créer des systèmes de matériaux conviviaux dans des moteurs et des outils modernes. D'après ce que je peux dire, ils sont basés sur une structure de données de graphique où chaque nœud représente un aspect shader de votre matériau et chaque chemin représente une sorte de relation entre eux.

D'après ce que je peux dire, l'implémentation de ce type de système serait aussi simple qu'une classe MaterialNode avec une variété de sous-classes (TextureNode, FloatNode, LerpNode, etc.). Où chaque sous-classe MaterialNode aurait des MaterialConnections.

class MaterialConnection
{
    MatNode_Out * fromNode;
    MatNode_In * toNode;
}

class LerpNode : MaterialNode
{
    MatNode_In x;
    MatNode_In y;
    MatNode_In alpha;

    MatNode_Out result;
}

C'est l' idée très basique , mais je suis un peu incertain sur la façon dont certains aspects de ce système fonctionneraient:

1.) Si vous regardez les différentes «expressions matérielles» (nœuds) utilisées par Unreal Engine 4 , vous verrez qu'elles ont chacune des connexions d'entrée et de sortie de différents types. Certains nœuds flottent en sortie, certains vecteur de sortie2, certains vecteur de sortie4, etc. Comment puis-je améliorer les nœuds et les connexions ci-dessus afin qu'ils puissent prendre en charge une variété de types d'entrée et de sortie? Le sous-classement de MatNode_Out avec MatNode_Out_Float et MatNode_Out_Vec4 (et ainsi de suite) serait-il un choix judicieux?

2.) Enfin, comment ce type de système est-il lié aux shaders GLSL? En examinant à nouveau UE4 (et de même pour les autres systèmes liés ci-dessus), l'utilisateur doit finalement brancher un nœud de matériau dans un grand nœud avec divers paramètres qui représentent les paramètres du shader (couleur de base, métal, brillant, émissivité, etc.) . Mon hypothèse initiale était que UE4 avait une sorte de «master shader» codé en dur avec une variété d'uniformes, et tout ce que l'utilisateur fait dans son «matériel» est simplement transmis au «master shader» lorsqu'il branche ses nœuds dans le « nœud maître ».

Cependant, la documentation UE4 indique:

"Chaque nœud contient un extrait de code HLSL, conçu pour effectuer une tâche spécifique. Cela signifie que lorsque vous construisez un matériau, vous créez du code HLSL via un script visuel."

Si c'est vrai, ce système génère-t-il un vrai script de shader? Comment est-ce que cela fonctionne exactement?

MrKatSwordfish
la source
1
Relatif à votre question: gameangst.com/?p=441
glampert

Réponses:

10

J'essaierai de répondre au mieux de mes connaissances, avec peu de connaissances sur le cas spécifique de l'UE4, mais plutôt sur la technique générale.

Les matériaux basés sur des graphiques sont autant de programmation que d'écrire le code vous-même. Il n'en a tout simplement pas envie pour les personnes sans expérience du code, ce qui semble plus facile. Ainsi, lorsqu'un concepteur relie un nœud "Ajouter", il écrit essentiellement add (valeur1, valeur2) et relie la sortie à autre chose. C'est ce qu'ils signifient que chaque nœud générera du code HLSL, que ce soit un appel de fonction ou simplement des instructions simples.

En fin de compte, utiliser le graphe des matériaux, c'est comme programmer des shaders bruts avec une bibliothèque de fonctions prédéfinies qui font des choses utiles courantes, et c'est aussi ce que fait UE4. Il a une bibliothèque de code de shader qu'un compilateur de shader prendra et injectera dans la source finale du shader, le cas échéant.

Dans le cas de UE4, s'ils prétendent qu'il a été converti en HLSL, je suppose qu'ils utilisent un outil de conversion qui est capable de convertir le code d'octets HLSL en code d'octets GLSL, donc utilisable sur les plateformes GL. Mais d'autres bibliothèques ont juste plusieurs compilateurs de shader, qui liront le graphique et généreront directement les sources de langage d'ombrage nécessaires.

Le graphique des matériaux est également un bon moyen d'abstraire des spécificités de la plate-forme et de se concentrer sur ce qui compte du point de vue de la direction artistique. Comme il n'est pas lié à un langage et à un niveau beaucoup plus élevé, il est plus facile à optimiser pour la plate-forme cible et à injecter dynamiquement d'autres codes comme la manipulation légère dans le shader.

1) Maintenant, pour répondre plus directement à vos questions, vous devriez avoir une approche basée sur les données pour concevoir un tel système. Trouvez un format plat qui peut être défini dans des structures très simples, et même défini dans un fichier texte. En substance, chaque graphique doit être un tableau de nœuds, avec un type, un ensemble d'entrées et de sorties, et chacun de ces champs doit avoir un link_id local pour s'assurer que les connexions du graphique ne sont pas ambiguës. En outre, chacun de ces champs peut avoir une configuration supplémentaire à ce que le champ prend en charge (quelle plage de types de données est prise en charge, par exemple).

Avec cette approche, vous pouvez facilement définir le champ d'un nœud comme étant (float | double) et le laisser déduire le type à partir des connexions, ou y forcer un type, sans hiérarchie de classe ni ingénierie excessive. C'est à vous de concevoir cette structure de données graphique aussi rigide ou flexible que vous le souhaitez. Tout ce dont vous avez besoin, c'est qu'il dispose de suffisamment d'informations pour que le générateur de code n'ait pas d'ambiguïté et ne puisse donc pas gérer correctement ce que vous voulez faire. L'important est qu'au niveau de la structure de données de base, vous restiez flexible et concentré sur la résolution de la tâche de définir un matériau seul.

Lorsque je dis «définir un matériau», je me réfère très précisément à la définition d'une surface maillée, au-delà de ce que la géométrie elle-même fournit. Cela inclut l'utilisation d'attributs de sommet supplémentaires pour configurer l'aspect de la surface, y ajouter un déplacement avec une carte de hauteur, perturber les normales avec des normales par pixel, modifier les paramètres physiques, modifier les BRDF, etc. Vous ne voulez pas décrire autre chose comme HDR, tonemapping, animation de skinning, manipulation légère ou beaucoup d'autres choses faites dans des shaders.

2) Il appartient ensuite au générateur de shaders du moteur de rendu de parcourir cette structure de données et, en lisant ses informations, d'assembler un ensemble de variables et de les lier ensemble à l'aide de fonctions prédéfinies et en injectant le code qui calcule l'éclairage et d'autres effets. N'oubliez pas que les shaders varient non seulement de différentes API graphiques, mais également entre différents rendus (un rendu différé vs un rendu basé sur des tuiles vs un rendu avancé nécessitent tous des shaders différents pour fonctionner), et avec un système matériel tel que celui-ci, vous pouvez faire abstraction du méchant couche de bas niveau et se concentrer uniquement sur la description de la surface.´

Pour UE4, ils ont trouvé une liste de choses pour ce nœud de sortie final que vous mentionnez, qui, selon eux, décrit 99% des surfaces des jeux anciens et modernes. Ils ont développé cet ensemble de paramètres au cours des décennies et l'ont prouvé avec la quantité folle de jeux que le moteur Unreal a produit jusqu'à présent. Par conséquent, tout ira bien si vous faites les choses de la même manière que l'irréel.

Pour conclure, je suggère un fichier .material juste pour gérer chaque graphique. Pendant le développement, il contiendrait peut-être un format basé sur du texte à déboguer, puis serait empaqueté ou compilé en binaire pour publication. Chaque .material serait composé de N nœuds et N connexions, un peu comme une base de données SQL. Chaque nœud aurait N champs, avec un nom et des indicateurs pour les types acceptés, si son entrée ou sa sortie, si les types sont inférés, etc. La structure de données d'exécution pour contenir le matériau chargé serait tout aussi plate et simple, donc le L'éditeur peut facilement l'adapter et le sauvegarder dans un fichier.

Et puis laissez le gros du travail pour la génération finale de shaders, ce qui est vraiment la partie la plus difficile à faire. La belle partie est que votre matériau reste agnostique à la plate-forme de rendu, en théorie, cela fonctionnerait avec n'importe quelle technique de rendu et API tant que vous représentez le matériau dans son langage d'ombrage approprié.

Faites-moi savoir si vous avez besoin de détails supplémentaires ou d'une correction dans ma réponse, j'ai perdu la vue d'ensemble sur tout le texte.

Grimshaw
la source
Je ne saurais trop vous remercier d'avoir rédigé une réponse aussi élaborée et excellente. J'ai l'impression d'avoir une bonne idée d'où je dois aller d'ici! Merci!
MrKatSwordfish
1
Pas de problème mec, n'hésitez pas à m'envoyer un message si vous avez besoin d'aide. Je travaille actuellement sur quelque chose d'équivalent pour mes propres outils, donc si vous voulez échanger des idées, soyez mon invité! Passez un bon après-midi: D
Grimshaw