Maybaygiare.org

Blog Network

Shaders

Comme mentionné dans le chapitre Bonjour Triangle, les shaders sont de petits programmes qui reposent sur le GPU. Ces programmes sont exécutés pour chaque section spécifique du pipeline graphique. Dans un sens fondamental, les shaders ne sont rien de plus que des programmes transformant des entrées en sorties. Les shaders sont également des programmes très isolés en ce sens qu’ils ne sont pas autorisés à communiquer entre eux; la seule communication qu’ils ont est via leurs entrées et sorties.

Dans le chapitre précédent, nous avons brièvement abordé la surface des shaders et comment les utiliser correctement. Nous allons maintenant expliquer les shaders, et plus particulièrement le langage d’ombrage OpenGL, d’une manière plus générale.

GLSL

Les shaders sont écrits dans le langage GLSL de type C. GLSL est conçu pour être utilisé avec des graphiques et contient des fonctionnalités utiles spécifiquement destinées à la manipulation de vecteurs et de matrices.

Les shaders commencent toujours par une déclaration de version, suivie d’une liste de variables d’entrée et de sortie, d’uniformes et de sa fonction principale. Le point d’entrée de chaque shader est à sa fonction principale où nous traitons toutes les variables d’entrée et produisons les résultats dans ses variables de sortie. Ne vous inquiétez pas si vous ne savez pas ce que sont les uniformes, nous y reviendrons sous peu.

Un shader a généralement la structure suivante :

#version version_numberin type in_variable_name;in type in_variable_name;out type out_variable_name; uniform type uniform_name; void main(){ // process input(s) and do some weird graphics stuff ... // output processed stuff to output variable out_variable_name = weird_stuff_we_processed;}

Lorsque nous parlons spécifiquement du vertex shader, chaque variable d’entrée est également connue sous le nom d’attribut de sommet. Il y a un nombre maximum d’attributs de sommet que nous sommes autorisés à déclarer limités par le matériel. OpenGL garantit qu’il y a toujours au moins 16 attributs de sommet à 4 composants disponibles, mais certains matériels peuvent en permettre d’autres que vous pouvez récupérer en interrogeant GL_MAX_VERTEX_ATTRIBS:

int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

Cela renvoie souvent le minimum de 16 qui devrait être plus que suffisant pour la plupart des fins.

Types

GLSL a, comme tout autre langage de programmation, des types de données pour spécifier le type de variable avec lequel nous voulons travailler. GLSL a la plupart des types de base par défaut que nous connaissons dans des langages tels que C : intfloatdoubleuint et bool. GLSL propose également deux types de conteneurs que nous utiliserons beaucoup, à savoir vectors et matrices. Nous discuterons des matrices dans un chapitre ultérieur.

Vecteurs

Un vecteur dans GLSL est un conteneur de 1,2,3 ou 4 composants pour l’un des types de base que nous venons de mentionner. Ils peuvent prendre la forme suivante (n représente le nombre de composants) :

  • vecn : le vecteur par défaut de n flotte.
  • bvecn: un vecteur de booléens n.
  • ivecn: un vecteur d’entiers n.
  • uvecn: un vecteur d’entiers non signés n.
  • dvecn: un vecteur de n composants doubles.

La plupart du temps, nous utiliserons le vecn de base car les flotteurs sont suffisants pour la plupart de nos objectifs.

Les composants d’un vecteur sont accessibles via vec.xx est le premier composant du vecteur. Vous pouvez utiliser .x.y.z et .w pour accéder respectivement à leur premier, deuxième, troisième et quatrième composant. GLSL vous permet également d’utiliser rgba pour les couleurs ou stpq pour les coordonnées de texture, en accédant aux mêmes composants.

Le type de données vectorielles permet une sélection de composants intéressante et flexible appelée swizzling. Swizzling nous permet d’utiliser une syntaxe comme celle-ci:

vec2 someVec;vec4 differentVec = someVec.xyxx;vec3 anotherVec = differentVec.zyw;vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

Vous pouvez utiliser n’importe quelle combinaison de jusqu’à 4 lettres pour créer un nouveau vecteur (du même type) tant que le vecteur d’origine a ces composants; il n’est pas autorisé à accéder au composant .z d’un vec2 par exemple. Nous pouvons également passer des vecteurs comme arguments à différents appels de constructeurs de vecteurs, réduisant ainsi le nombre d’arguments requis :

vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);

Les vecteurs sont donc un type de données flexible que nous pouvons utiliser pour toutes sortes d’entrées et de sorties. Tout au long du livre, vous verrez de nombreux exemples de la façon dont nous pouvons gérer de manière créative les vecteurs.

Tenants et aboutissants

Les shaders sont de jolis petits programmes en eux-mêmes, mais ils font partie d’un tout et pour cette raison, nous voulons avoir des entrées et des sorties sur les shaders individuels afin que nous puissions déplacer des choses. GLSL a défini les mots clés in et out spécifiquement à cette fin. Chaque shader peut spécifier des entrées et des sorties à l’aide de ces mots-clés et chaque fois qu’une variable de sortie correspond à une variable d’entrée de l’étape de shader suivante, elle est transmise. Le vertex et le fragment shader diffèrent cependant un peu.

Le vertex shader devrait recevoir une certaine forme d’entrée sinon il serait assez inefficace. Le vertex shader diffère dans son entrée, en ce qu’il reçoit son entrée directement des données de sommet. Pour définir comment les données de sommet sont organisées, nous spécifions les variables d’entrée avec les métadonnées d’emplacement afin que nous puissions configurer les attributs de sommet sur le PROCESSEUR. Nous l’avons vu dans le chapitre précédent comme layout (location = 0). Le vertex shader nécessite donc une spécification de disposition supplémentaire pour ses entrées afin que nous puissions le lier aux données de sommet.

Il est également possible d’omettre le spécificateurlayout (location = 0)et de rechercher les emplacements des attributs dans votre code OpenGL via glGetAttribLocation, mais je préférerais les définir dans le vertex shader. Il est plus facile à comprendre et vous permet d’économiser (et OpenGL) du travail.

L’autre exception est que le shader de fragments nécessite une variable de sortie de couleur vec4, car les shaders de fragments doivent générer une couleur de sortie finale. Si vous ne spécifiez pas de couleur de sortie dans votre shader de fragments, la sortie du tampon de couleur pour ces fragments ne sera pas définie (ce qui signifie généralement qu’OpenGL les rendra en noir ou en blanc).

Donc, si nous voulons envoyer des données d’un shader à l’autre, nous devons déclarer une sortie dans le shader d’envoi et une entrée similaire dans le shader de réception. Lorsque les types et les noms sont égaux des deux côtés, OpenGL liera ces variables ensemble et il sera alors possible d’envoyer des données entre les shaders (ceci est fait lors de la liaison d’un objet programme). Pour vous montrer comment cela fonctionne dans la pratique, nous allons modifier les shaders du chapitre précédent pour laisser le vertex shader décider de la couleur du fragment shader.

Vertex shader

#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0 out vec4 vertexColor; // specify a color output to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color}

Fragment shader

#version 330 coreout vec4 FragColor; in vec4 vertexColor; // the input variable from the vertex shader (same name and same type) void main(){ FragColor = vertexColor;} 

Vous pouvez voir que nous avons déclaré une variable vertexColor en tant que sortie vec4 que nous avons définie dans le vertex shader et que nous déclarons une entrée vertexColor similaire dans le fragment shader. Comme ils ont tous les deux le même type et le même nom, le vertexColor dans le fragment shader est lié au vertexColor dans le vertex shader. Parce que nous définissons la couleur sur une couleur rouge foncé dans le vertex shader, les fragments résultants doivent également être rouge foncé. L’image suivante montre la sortie:

Voilà ! Nous avons juste réussi à envoyer une valeur du vertex shader au fragment shader. Pimentons un peu et voyons si nous pouvons envoyer une couleur de notre application au fragment shader!

Uniformes

Les uniformes sont un autre moyen de transmettre les données de notre application sur le processeur aux shaders du GPU. Les uniformes sont cependant légèrement différents par rapport aux attributs de sommet. Tout d’abord, les uniformes sont mondiaux. Global, ce qui signifie qu’une variable uniforme est unique par objet de programme de shader, et peut être accédée à partir de n’importe quel shader à n’importe quelle étape du programme de shader. Deuxièmement, quelle que soit la valeur de l’uniforme que vous définissez, les uniformes conserveront leurs valeurs jusqu’à ce qu’ils soient réinitialisés ou mis à jour.

Pour déclarer un uniforme en GLSL, nous ajoutons simplement le mot clé uniform à un shader avec un type et un nom. À partir de ce moment, nous pouvons utiliser l’uniforme nouvellement déclaré dans le shader. Voyons si cette fois, nous pouvons définir la couleur du triangle via un uniforme:

#version 330 coreout vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code.void main(){ FragColor = ourColor;} 

Nous avons déclaré un uniforme vec4ourColor dans le fragment shader et avons défini la couleur de sortie du fragment sur le contenu de cette valeur uniforme. Puisque les uniformes sont des variables globales, nous pouvons les définir dans n’importe quelle étape de shader, donc pas besoin de passer à nouveau par le vertex shader pour obtenir quelque chose au fragment shader. Nous n’utilisons pas cet uniforme dans le vertex shader, il n’est donc pas nécessaire de le définir là-bas.

Si vous déclarez un uniforme qui n’est utilisé nulle part dans votre code GLSL, le compilateur supprimera silencieusement la variable de la version compilée, ce qui est la cause de plusieurs erreurs frustrantes; gardez cela à l’esprit!

L’uniforme est actuellement vide ; nous n’avons pas encore ajouté de données à l’uniforme, alors essayons ça. Nous devons d’abord trouver l’index / l’emplacement de l’attribut uniform dans notre shader. Une fois que nous avons l’index / l’emplacement de l’uniforme, nous pouvons mettre à jour ses valeurs. Au lieu de passer une seule couleur au fragment shader, pimentons les choses en changeant progressivement de couleur au fil du temps:

float timeValue = glfwGetTime();float greenValue = (sin(timeValue) / 2.0f) + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUseProgram(shaderProgram);glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

Tout d’abord, nous récupérons le temps d’exécution en secondes via glfwGetTime(). Ensuite, nous varions la couleur dans la plage de 0.01.0 en utilisant la fonction sin et stockons le résultat dans greenValue.

Ensuite, nous interrogeons l’emplacement de l’uniforme ourColor en utilisant glGetUniformLocation. Nous fournissons le programme de shader et le nom de l’uniforme (à partir duquel nous voulons récupérer l’emplacement) à la fonction de requête. Si glGetUniformLocation renvoie -1, il n’a pas pu trouver l’emplacement. Enfin, nous pouvons définir la valeur uniforme en utilisant la fonction glUniform4f. Notez que la recherche de l’emplacement uniforme ne nécessite pas que vous utilisiez d’abord le programme shader, mais la mise à jour d’un uniforme nécessite que vous utilisiez d’abord le programme (en appelant glUseProgram), car il définit l’uniforme sur le programme shader actuellement actif.

Parce qu’OpenGL est en son cœur une bibliothèque C, il n’a pas de support natif pour la surcharge de fonctions, donc partout où une fonction peut être appelée avec différents types, OpenGL définit de nouvelles fonctions pour chaque type requis; glUniform en est un parfait exemple. La fonction nécessite un postfix spécifique pour le type d’uniforme que vous souhaitez définir. Quelques-uns des postfixes possibles sont :

  • f: la fonction attend un float comme valeur.
  • i : la fonction attend un int comme valeur.
  • ui : la fonction attend un unsigned int comme valeur.
  • 3f: la fonction attend 3 floats comme valeur.
  • fv: la fonction attend un vecteur/tableau float comme valeur.

Chaque fois que vous souhaitez configurer une option d’OpenGL, choisissez simplement la fonction surchargée qui correspond à votre type. Dans notre cas, nous voulons définir 4 flotteurs de l’uniforme individuellement afin que nous transmettions nos données via glUniform4f (notez que nous aurions également pu utiliser la versionfv).

Maintenant que nous savons comment définir les valeurs des variables uniformes, nous pouvons les utiliser pour le rendu. Si nous voulons que la couleur change progressivement, nous voulons mettre à jour cet uniforme à chaque image, sinon le triangle conserverait une seule couleur unie si nous ne la définissions qu’une seule fois. Nous calculons donc la valeur verte et mettons à jour l’uniforme à chaque itération de rendu:

while(!glfwWindowShouldClose(window)){ // input processInput(window); // render // clear the colorbuffer glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // be sure to activate the shader glUseProgram(shaderProgram); // update the uniform color float timeValue = glfwGetTime(); float greenValue = sin(timeValue) / 2.0f + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // now render the triangle glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); // swap buffers and poll IO events glfwSwapBuffers(window); glfwPollEvents();}

Le code est une adaptation relativement simple du code précédent. Cette fois, nous mettons à jour une valeur uniforme à chaque image avant de dessiner le triangle. Si vous mettez à jour l’uniforme correctement, vous devriez voir la couleur de votre triangle passer progressivement du vert au noir et revenir au vert.

Vérifiez le code source ici si vous êtes bloqué.

Comme vous pouvez le voir, les uniformes sont un outil utile pour définir des attributs qui peuvent changer chaque image, ou pour échanger des données entre votre application et vos shaders, mais que se passe-t-il si nous voulons définir une couleur pour chaque sommet ? Dans ce cas, nous devrions déclarer autant d’uniformes que nous avons de sommets. Une meilleure solution serait d’inclure plus de données dans les attributs de sommet, ce que nous allons faire maintenant.

Plus d’attributs !

Nous avons vu dans le chapitre précédent comment nous pouvons remplir un VBO, configurer des pointeurs d’attributs de sommet et tout stocker dans un VAO. Cette fois, nous voulons également ajouter des données de couleur aux données de sommet. Nous allons ajouter des données de couleur en tant que 3float s au tableau des sommets. Nous attribuons une couleur rouge, verte et bleue à chacun des coins de notre triangle respectivement:

float vertices = { // positions // colors 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top }; 

Puisque nous avons maintenant plus de données à envoyer au vertex shader, il est nécessaire d’ajuster le vertex shader pour recevoir également notre valeur de couleur en tant qu’entrée d’attribut de sommet. Notez que nous définissons l’emplacement de l’attribut aColor sur 1 avec le spécificateur de mise en page :

#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0layout (location = 1) in vec3 aColor; // the color variable has attribute position 1 out vec3 ourColor; // output a color to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); ourColor = aColor; // set ourColor to the input color we got from the vertex data} 

Puisque nous n’utilisons plus d’uniforme pour la couleur du fragment, mais utilisons maintenant la variable de sortie ourColor, nous devrons également changer le shader de fragment :

#version 330 coreout vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0);}

Car nous avons ajouté un autre attribut de sommet et mis à jour les VBO mémoire nous devons reconfigurer les pointeurs d’attributs de sommet. Les données mises à jour dans la mémoire du VBO ressemblent maintenant un peu à ceci:

Données entrelacées de position et de couleur dans le VBO à configurer avec functionglVertexAttribPointer/function

Connaissant la disposition actuelle, nous pouvons mettre à jour le format de sommet avec glVertexAttribPointer:

// position attributeglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// color attributeglVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));glEnableVertexAttribArray(1);

Les premiers arguments de glVertexAttribPointer sont relativement simples. Cette fois, nous configurons l’attribut vertex sur l’emplacement de l’attribut 1. Les valeurs de couleur ont une taille de 3float s et nous ne normalisons pas les valeurs.

Comme nous avons maintenant deux attributs de sommet, nous devons recalculer la valeur de la foulée. Pour obtenir la valeur d’attribut suivante (par exemple la composante suivante x du vecteur de position) dans le tableau de données, nous devons déplacer 6float s vers la droite, trois pour les valeurs de position et trois pour les valeurs de couleur. Cela nous donne une valeur de foulée de 6 fois la taille d’un float en octets (=24 octets).
De plus, cette fois, nous devons spécifier un décalage. Pour chaque sommet, l’attribut de sommet de position est le premier, nous déclarons donc un décalage de 0. L’attribut color commence après les données de position, de sorte que le décalage est 3 * sizeof(float) en octets (=12 octets).

L’exécution de l’application devrait entraîner l’image suivante:

Vérifiez le code source ici si vous êtes bloqué.

L’image n’est peut-être pas exactement ce à quoi vous vous attendez, car nous n’avons fourni que 3 couleurs, pas l’énorme palette de couleurs que nous voyons en ce moment. Tout cela est le résultat de quelque chose appelé interpolation de fragment dans le fragment shader. Lors du rendu d’un triangle, l’étape de rastérisation entraîne généralement beaucoup plus de fragments que les sommets spécifiés à l’origine. Le rastériseur détermine ensuite les positions de chacun de ces fragments en fonction de l’endroit où ils résident sur la forme du triangle.
Sur la base de ces positions, il interpole toutes les variables d’entrée du fragment shader. Disons par exemple que nous avons une ligne où le point supérieur a une couleur verte et le point inférieur une couleur bleue. Si le shader de fragment est exécuté sur un fragment qui réside autour d’une position à 70% de la ligne, son attribut d’entrée de couleur résultant serait alors une combinaison linéaire de vert et de bleu ; pour être plus précis : 30% bleu et 70% vert.

C’est exactement ce qui s’est passé au triangle. Nous avons 3 sommets et donc 3 couleurs, et à en juger par les pixels du triangle, il contient probablement environ 50000 fragments, où le fragment shader interpolait les couleurs parmi ces pixels. Si vous regardez bien les couleurs, vous verrez que tout a du sens: le rouge au bleu devient d’abord violet puis bleu. L’interpolation de fragment est appliquée à tous les attributs d’entrée du fragment shader.

Notre propre classe de shader

Écrire, compiler et gérer les shaders peut être assez fastidieux. Comme touche finale au sujet du shader, nous allons nous faciliter la vie en construisant une classe de shader qui lit les shaders à partir du disque, les compile et les lie, vérifie les erreurs et est facile à utiliser. Cela vous donne également une idée de la façon dont nous pouvons encapsuler certaines des connaissances que nous avons apprises jusqu’à présent dans des objets abstraits utiles.

Nous allons créer la classe de shader entièrement dans un fichier d’en-tête, principalement à des fins d’apprentissage et de portabilité. Commençons par ajouter les includes requis et en définissant la structure de classe:

#ifndef SHADER_H#define SHADER_H#include <glad/glad.h> // include glad to get all the required OpenGL headers #include <string>#include <fstream>#include <sstream>#include <iostream> class Shader{public: // the program ID unsigned int ID; // constructor reads and builds the shader Shader(const char* vertexPath, const char* fragmentPath); // use/activate the shader void use(); // utility uniform functions void setBool(const std::string &name, bool value) const; void setInt(const std::string &name, int value) const; void setFloat(const std::string &name, float value) const;}; #endif

Nous avons utilisé plusieurs directives de préprocesseur en haut du fichier d’en-tête. L’utilisation de ces petites lignes de code informe votre compilateur de n’inclure et de compiler ce fichier d’en-tête que s’il n’a pas encore été inclus, même si plusieurs fichiers incluent l’en-tête du shader. Cela évite les conflits de liaison.

La classe shader contient l’ID du programme shader. Son constructeur nécessite les chemins de fichiers du code source du vertex et du fragment shader respectivement que nous pouvons stocker sur le disque sous forme de simples fichiers texte. Pour ajouter un petit plus, nous ajoutons également plusieurs fonctions utilitaires pour nous faciliter un peu la vie: use active le programme shader, et tout est réglé… les fonctions interrogent un emplacement uniforme et définissent sa valeur.

Lecture du fichier

Nous utilisons des flux de fichiers C++ pour lire le contenu du fichier en plusieurs objets string:

Shader(const char* vertexPath, const char* fragmentPath){ // 1. retrieve the vertex/fragment source code from filePath std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // ensure ifstream objects can throw exceptions: vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); try { // open files vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; // read file's buffer contents into streams vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // close file handlers vShaderFile.close(); fShaderFile.close(); // convert stream into string vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch(std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; } const char* vShaderCode = vertexCode.c_str(); const char* fShaderCode = fragmentCode.c_str(); 

Ensuite, nous devons compiler et lier les shaders. Notez que nous examinons également si la compilation / liaison a échoué et si c’est le cas, imprimez les erreurs de compilation. Ceci est extrêmement utile lors du débogage (vous aurez éventuellement besoin de ces journaux d’erreurs) :

// 2. compile shadersunsigned int vertex, fragment;int success;char infoLog; // vertex Shadervertex = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);// print compile errors if anyglGetShaderiv(vertex, GL_COMPILE_STATUS, &success);if(!success){ glGetShaderInfoLog(vertex, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}; // similiar for Fragment Shader // shader ProgramID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);// print linking errors if anyglGetProgramiv(ID, GL_LINK_STATUS, &success);if(!success){ glGetProgramInfoLog(ID, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;} // delete the shaders as they're linked into our program now and no longer necessaryglDeleteShader(vertex);glDeleteShader(fragment);

La fonction d’utilisation est simple:

void use() { glUseProgram(ID);} 

De même pour l’une des fonctions de réglage uniformes:

void setBool(const std::string &name, bool value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); }void setInt(const std::string &name, int value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), value); }void setFloat(const std::string &name, float value) const{ glUniform1f(glGetUniformLocation(ID, name.c_str()), value); } 

Et là, nous l’avons, une classe de shader terminée. L’utilisation de la classe shader est assez facile; nous créons un objet shader une fois et à partir de ce moment, commencez simplement à l’utiliser:

Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");while(...){ ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff();}

Ici, nous avons stocké le code source du shader de sommets et de fragments dans deux fichiers appelés shader.vs et shader.fs. Vous êtes libre de nommer vos fichiers shader comme bon vous semble; Personnellement, je trouve les extensions .vs et .fs assez intuitives.

Vous pouvez trouver le code source ici en utilisant notre classe de shader nouvellement créée. Notez que vous pouvez cliquer sur les chemins du fichier shader pour trouver le code source des shaders.

Exercices

  1. Ajustez le vertex shader pour que le triangle soit à l’envers: solution.
  2. Spécifiez un décalage horizontal via un uniforme et déplacez le triangle vers le côté droit de l’écran dans le vertex shader en utilisant cette valeur de décalage: solution.
  3. Affiche la position du sommet dans le shader de fragment à l’aide du mot-clé out et définit la couleur du fragment comme égale à cette position de sommet (voir comment même les valeurs de position du sommet sont interpolées à travers le triangle). Une fois que vous avez réussi à le faire; essayez de répondre à la question suivante: pourquoi le côté inférieur gauche de notre triangle est-il noir?: solution.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.