web-dev-qa-db-fra.com

Comment expédier des shaders GLSL avec votre logiciel C ++

Pendant l'initialisation d'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, 6e édition )

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, de déboguer et de maintenir les shaders GLSL directement dans une chaîne. Ainsi, obtenir le code source dans une chaîne à partir d'un fichier est 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 être un problème. OpenGL prend en charge les "shaders binaires précompilés" mais le Wiki ouvert déclare 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 d'un même fournisseur accepte les mêmes formats binaires. [...]

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

46
Korchkidu

Il suffit de "les stocker directement dans l'exécutable" ou de "les stocker dans un (des) fichier (s) séparé (s)", sans rien entre les deux. Si vous voulez un exécutable autonome, les mettre dans le binaire est une bonne idée. Notez que vous pouvez les ajouter en tant que ressources ou modifier votre système de build pour incorporer les chaînes de shader de fichiers de développement séparés dans des fichiers source pour faciliter le développement (avec l'ajout possible de pouvoir charger directement les fichiers séparés dans les builds de développement).

Pourquoi pensez-vous que l'expédition des sources du shader serait un problème? Il n'y a tout simplement pas d'autre moyen dans le GL. Les fichiers binaires précompilés ne sont utiles que pour la mise en cache des résultats de la compilation sur la machine cible. Avec les avancées rapides de la technologie GPU, l'évolution des architectures GPU et différents fournisseurs avec des ISA totalement incompatibles, les binaires shader précompilés n'ont aucun sens.

Notez que placer vos sources de shader dans l'exécutable ne les "protège" pas, même si vous les cryptez. Un utilisateur peut toujours se connecter à la bibliothèque GL et intercepter les sources que vous spécifiez au GL. Et les débogueurs GL là-bas font exactement cela).

MISE À JOUR 2016

Au SIGGRAPH 2016, le Conseil de révision de l'architecture OpenGL a publié le GL_ARB_gl_spirv extension. Cela permettra à un GL inmplementation d'utiliser le SPIRV langage intermédiaire binaire. Cela a quelques avantages potentiels:

  1. Les shaders peuvent être pré-"compilés" hors ligne (la compilation finale pour le GPU cible se fait toujours par le pilote plus tard). Vous n'avez pas à expédier le code source du shader, mais uniquement la représentation intermédiaire binaire.
  2. Il y a un frontend de compilateur standard ( glslang ) qui effectue l'analyse, donc les différences entre les analyseurs des différentes implémentations peuvent être éliminées.
  3. Plus de langages de shaders peuvent être ajoutés, sans avoir besoin de changer les implémentations GL.
  4. Il augmente quelque peu la portabilité vers vulkan.

Avec ce schéma, GL se rapproche de D3D et Vulkan à cet égard. Cependant, cela ne change pas la situation. Le bytecode SPIRV peut toujours être intercepté, désassemblé et décompilé. Il rend la rétro-ingénierie un peu plus difficile, mais pas beaucoup en fait. Dans un shader, vous ne pouvez généralement pas vous permettre des mesures d'obscurcissement étendues, car cela réduit considérablement les performances - ce qui est contraire à la fonction des shaders.

Gardez également à l'esprit que cette extension n'est pas largement disponible pour le moment (automne 2016). Et Apple a cessé de prendre en charge GL après 4.1, donc cette extension ne viendra probablement jamais sur OSX.

MISE À JOUR MINEURE 2017

GL_ARB_gl_spirv est désormais la fonctionnalité principale officielle de OpenGL 4.6 , de sorte que nous pouvons nous attendre à un taux d'adoption croissant pour cette fonctionnalité, mais cela ne change pas grand-chose.

41
derhass

Avec c ++ 11, vous pouvez également utiliser la nouvelle fonctionnalité des littéraux de chaîne bruts. Placez ce code source dans un fichier séparé 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 celle-ci:

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++.

43
Jan Rüegg

OpenGL prend en charge les binaires précompilés, mais pas de manière portative. Contrairement à HLSL, qui est compilé dans un format standard de bytcode par le compilateur de Microsoft et plus tard traduit en instruction native d'un GPU défini par le pilote, OpenGL n'a pas un tel format. Vous ne pouvez pas utiliser des binaires précompilés pour autre chose que la mise en cache des shaders GLSL compilés sur une seule machine pour accélérer le temps de chargement, et même dans ce cas, rien ne garantit que le binaire compilé fonctionnera si la version du pilote change ... et encore moins le GPU réel sur les changements de machine.

Vous pouvez toujours obscurcir vos shaders si vous êtes vraiment paranoïaque. Le fait est que, à moins que vous ne fassiez quelque chose de vraiment unique, personne ne se souciera de vos shaders et je le pense sincèrement. Cette industrie se nourrit de l'ouverture, tous les grands acteurs de l'industrie discutent régulièrement des techniques les plus récentes et les plus intéressantes lors de conférences telles que GDC, SIGGRAPH, etc. En fait, les shaders sont tellement spécifiques à la mise en œuvre que souvent vous ne pouvez pas faire grand-chose à partir de rétroconcevez-les que vous ne pourriez pas faire simplement en écoutant l'une de ces conférences.

Si vos préoccupations concernent les personnes qui modifient votre logiciel, je vous suggère d'implémenter un simple test de hachage ou de somme de contrôle. De nombreux jeux le font déjà pour éviter la triche, jusqu'où vous voulez aller c'est à vous de décider. Mais l'essentiel est que les shaders binaires dans OpenGL sont destinés à réduire le temps de compilation des shaders, pas pour une redistribution portable.

19
Andon M. Coleman

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 analyser un dossier pour les fichiers source du shader, puis 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, étant donné une ressource, renvoie le chemin d'accès au 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 (beaucoup de handwaving ici) de faire modifier le comportement du script CMake de sorte que dans une version, au lieu de fournir le chemin du fichier, il fournisse la source du shader, et dans le fichier cpp stocké le contenu du les shaders eux-mêmes (ou dans le cas d'une fenêtre Windows ou Apple target, faites-les partie des ressources exécutables/bundle 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 intégrés dans l'exécutable. En fait, mon programme GLSL récupérant du code regarde en fait le temps de compilation du shader par rapport aux horodatages modifiés des fichiers source et rechargera le shader si les fichiers ont changé depuis la dernière fois qu'il a été compilé (c'est encore à ses balbutiements, car cela signifie que vous perdez tous les uniformes qui étaient auparavant liés au shader, mais j'y travaille).

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 voudrez peut-être charger et traiter ... des images pour les textures, les fichiers audio, les niveaux, etc.

11
Jherico

Comme alternative de garder les shaders GLSL directement dans une chaîne, je suggère de considérer cette bibliothèque que je développe: ShaderBoiler (Apache-2.0).

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

Le concept principal est d'écrire dans des constructions C++ similaires au code GLSL, qui construiraient un graphe de calcul à partir duquel le code GLSL final est 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 OpenGL API pour créer un shader.

6
Podgorskiy

Le problème est qu'il est difficile d'éditer, de déboguer et de maintenir les shaders GLSL directement dans une chaîne.

Il est étrange que cette phrase ait été totalement ignorée par toutes les "réponses" jusqu'à présent, alors que le thème récurrent de ces réponses a été: "Vous ne pouvez pas résoudre le problème; il suffit de le gérer".

La réponse pour les rendre plus faciles à modifier, tout en les chargeant directement à partir d'une chaîne, est simple. Considérez le littéral de chaîne suivant:

    const char* gonFrag1 = R"(#version 330
// Shader code goes here
// and newlines are fine, too!)";

Tous les autres commentaires sont corrects dans la mesure où ils vont. En effet, comme on dit, la meilleure sécurité disponible est l'obscurité, car GL peut être intercepté. Mais pour garder les honnêtes gens honnêtes, et pour mettre un frein à la détérioration accidentelle du programme, vous pouvez faites comme ci-dessus en C++, et maintenez toujours facilement votre code.

Bien sûr, si vous DID voulez protéger le shader le plus révolutionnaire du monde contre le vol, l'obscurité pourrait être poussée à des extrêmes plutôt efficaces. Mais c'est une autre question pour un autre thread.

4
Thomas Poole

Vous pouvez également combiner plusieurs sources de shaders en un seul fichier (ou chaîne) à l'aide de directives de préprocesseur si vous ne souhaitez pas les garder séparées. Cela vous permet également d'éviter les répétitions (par exemple, les déclarations partagées) - les variables inutilisées sont optimisées par le compilateur la plupart du temps.

Voir http://www.gamedev.net/topic/651404-shaders-glsl-in-one-file-is-it-practical/

2
UXkQEZ7

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

1
Ilian Zapryanov

Une suggestion:

Dans votre programme, placez le shader dans:

const char shader_code = {
#include "shader_code.data"
, 0x00};

Dans shader_code.data, le code source du shader doit être une liste de nombres hexadécimaux séparés par des virgules. Ces fichiers doivent être créés avant la compilation en utilisant votre code de shader écrit normalement dans un fichier. Sous Linux, je mettrais des instructions sur Makefile pour exécuter le code:

cat shader_code.glsl | xxd -i > shader_code.data
1
Thiago Harry

Une autre alternative au stockage de fichiers texte glsl ou de fichiers glsl précompilés est un générateur de shaders, qui prend un arbre d'ombrage en entrée et génère du code glsl (ou hlsl, ...), qui est ensuite compilé et lié lors de l'exécution ... 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 du langage d'ombrage cg. Si vous pensez à glsl/hlsl assez profondément, vous verrez que la transformation des arbres d'ombrage en code source était à l'arrière-plan de l'esprit des concepteurs de langage.

1
user1095108