[C++] Comment expédier les shaders GLSL dans votre logiciel C ++


Answers

Avec c ++ 11, vous pouvez également utiliser la nouvelle fonctionnalité des littéraux de chaînes brutes. Placez ce code source dans un fichier distinct nommé shader.vs :

R"(
#version 420 core

void main(void)
{
    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)"

puis importez-le comme une chaîne comme ceci:

const std::string vs_source =
#include "shader.vs"
;

L'avantage est qu'il est facile à maintenir et déboguer, et vous obtenez des numéros de ligne corrects en cas d'erreurs du compilateur de shaders OpenGL. Et vous n'avez toujours pas besoin d'expédier des shaders séparés.

Le seul inconvénient que je peux voir est les lignes ajoutées en haut et en bas du fichier ( R") et )" ) et la syntaxe qui est un peu étrange pour obtenir la chaîne en code C ++.

Question

Lors de l'initialisation OpenGL, le programme est censé faire quelque chose comme:

<Get Shader Source Code>
<Create Shader>
<Attach Source Code To Shader>
<Compile Shader>

Obtenir le code source pourrait être aussi simple que de le mettre dans une chaîne comme: (Exemple tiré de SuperBible, 6th Edition )

static const char * vs_source[] =
{
    "#version 420 core                             \n"
    "                                              \n"
    "void main(void)                               \n"
    "{                                             \n"
    "    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);   \n"
    "}                                             \n"
};

Le problème est qu'il est difficile d'éditer, déboguer et maintenir les shaders GLSL directement dans une chaîne. Obtenir le code source dans une chaîne à partir d'un fichier est donc plus facile à développer:

std::ifstream vertexShaderFile("vertex.glsl");
std::ostringstream vertexBuffer;
vertexBuffer << vertexShaderFile.rdbuf();
std::string vertexBufferStr = vertexBuffer.str();
// Warning: safe only until vertexBufferStr is destroyed or modified
const GLchar *vertexSource = vertexBufferStr.c_str();

Le problème est maintenant de savoir comment expédier les shaders avec votre programme? En effet, l'envoi du code source avec votre application peut poser problème. OpenGL supporte les "shaders binaires pré-compilés" mais l' Open Wiki indique que:

Les formats binaires du programme ne sont pas destinés à être transmis. Il n'est pas raisonnable de s'attendre à ce que différents fournisseurs de matériel acceptent les mêmes formats binaires. Il n'est pas raisonnable de s'attendre à ce qu'un matériel différent du même fournisseur accepte les mêmes formats binaires. [...]

Comment expédier pratiquement les shaders GLSL avec votre logiciel C ++?




Au lieu de garder les shaders GLSL directement dans une chaîne, je suggère de considérer cette bibliothèque que je suis en train de développer: ShaderBoiler (Apache-2.0).

Il est en version alpha et a certaines limitations qui peuvent restreindre son utilisation.

Le concept principal est d'écrire en C ++ des constructions similaires au code GLSL, qui construiraient un graphe de calcul à partir duquel le code GLSL final serait généré.

Par exemple, considérons le code C ++ suivant

#include <shaderboiler.h>
#include <iostream>

void main()
{
    using namespace sb;

    context ctx;
    vec3 AlbedoColor           = ctx.uniform<vec3>("AlbedoColor");
    vec3 AmbientLightColor     = ctx.uniform<vec3>("AmbientLightColor");
    vec3 DirectLightColor      = ctx.uniform<vec3>("DirectLightColor");
    vec3 LightPosition         = ctx.uniform<vec3>("LightPosition");

    vec3 normal   = ctx.in<vec3>("normal");
    vec3 position = ctx.in<vec3>("position");
    vec4& color   = ctx.out<vec4>("color");

    vec3 normalized_normal = normalize(normal);

    vec3 fragmentToLight = LightPosition - position;

    Float squaredDistance = dot(fragmentToLight, fragmentToLight);

    vec3 normalized_fragmentToLight = fragmentToLight / sqrt(squaredDistance);

    Float NdotL = dot(normal, normalized_fragmentToLight);

    vec3 DiffuseTerm = max(NdotL, 0.0) * DirectLightColor / squaredDistance;

    color = vec4(AlbedoColor * (AmbientLightColor + DiffuseTerm), 1.0);

    std::cout << ctx.genShader();
}

La sortie vers la console sera:

uniform vec3 AlbedoColor;
uniform vec3 AmbientLightColor;
uniform vec3 LightPosition;
uniform vec3 DirectLightColor;

in vec3 normal;
in vec3 position;

out vec4 color;

void main(void)
{
        vec3 sb_b = LightPosition - position;
        float sb_a = dot(sb_b, sb_b);
        color = vec4(AlbedoColor * (AmbientLightColor + max(dot(normal, sb_b / sqrt(sb_a)), 0.0000000) * DirectLightColor / sb_a), 1.000000);
}

La chaîne créée avec le code GLSL peut être utilisée avec l'API OpenGL pour créer un shader.




Je ne sais pas si cela fonctionnera, mais vous pouvez intégrer le fichier .vs dans votre exécutable avec binutils comme le programme comme g2bin, et vous pouvez déclarer vos programmes de shader comme externes puis vous y accéder comme ressources normales intégrées dans l'exécutable. Voir qrc dans Qt, ou vous pouvez voir mon petit programme pour l'incorporation de trucs dans les exécutables ici: https://github.com/heatblazer/binutil qui est appelé comme commande de pré-construction à l'IDE.




Ma suggestion serait de faire de l'incorporation de shader dans votre binaire une partie de votre processus de construction. J'utilise CMake dans mon code pour scanner un dossier pour les fichiers source shader et ensuite générer un en-tête avec une énumération de tous les shaders disponibles:

#pragma once
enum ShaderResource {
    LIT_VS,
    LIT_FS,
    // ... 
    NO_SHADER
};

const std::string & getShaderPath(ShaderResource shader);

De même, CMake crée un fichier CPP qui, à l'aide d'une ressource, renvoie le chemin du fichier vers le shader.

const string & getShaderPath(ShaderResource res) {
  static map<ShaderResource, string> fileMap;
  static bool init = true;
  if (init) {
   init = false;
   fileMap[LIT_VS] =
    "C:/Users/bdavis/Git/OculusRiftExamples/source/common/Lit.vs";
   // ...
  }
  return fileMap[res];
}

Il ne serait pas trop difficile de faire en sorte que le script CMake modifie le comportement de sorte que dans une version de construction au lieu de fournir le chemin du fichier, il fournisse la source du shader, et dans le fichier cpp, le contenu du les shaders eux-mêmes (ou dans le cas d'une cible Windows ou Apple font d'eux une partie des ressources exécutables / ensemble exécutable).

L'avantage de cette approche est qu'il est beaucoup plus facile de modifier les shaders à la volée pendant le débogage s'ils ne sont pas cuits dans l'exécutable. En fait, mon code de chargement de programme GLSL regarde réellement le temps de compilation du shader par rapport aux timestamps modifiés des fichiers source et rechargera le shader si les fichiers ont changé depuis la dernière compilation (cela n'en est encore qu'à ses débuts, cela signifie que vous perdez tous les uniformes qui étaient auparavant liés au shader, mais je travaille dessus).

C'est vraiment moins un problème de shader qu'un problème générique de 'ressources non-C ++'. Le même problème existe avec tout ce que vous pourriez vouloir charger et traiter ... des images pour des textures, des fichiers son, des niveaux, qu'avez-vous.




Une autre alternative au stockage des fichiers texte glsl ou des fichiers glsl précompilés est un générateur de shader, qui prend un arbre d'ombrage en entrée et sort le code glsl (ou hlsl, ...), qui est ensuite compilé et lié à l'exécution ... Suivant cette approche vous pouvez plus facilement vous adapter aux capacités du matériel gfx. Vous pouvez également prendre en charge hlsl, si vous avez beaucoup de temps, pas besoin de la langue d'ombrage cg. Si vous pensez assez à glsl / hlsl, vous verrez que la transformation des arbres d'ombrage en code source était à l'arrière des concepteurs de langage.