gKit2 light
|
maintenant que construire ou charger des objets et les afficher est plus clair, cf première application avec gKit, que peut-on faire de plus ?
écrire des shaders !
un shader est une fonction assez classique exécutée par les processeurs de la carte graphique, il y a 2 types de shaders :
les shaders d'openGL s'écrivent en GLSL, c'est un langage très proche du C/C++ avec quelques différences quand meme, et une librairie de fonctions standards pour faire des calculs sur des points, des matrices, des vecteurs, des couleurs, etc...
pourquoi transformer les sommets ?
au miminum, on souhaite dessiner les objets pour le point de vue de la camera... il faut bien déterminer si les objets sont devant, derriere, etc, et surtout on a besoin de savoir sur quels pixels de l'image ils se projettent pour pouvoir ensuite dessiner la forme de chaque objet en coloriant tout ces pixels.
lorsque l'application dessine un objet, que se passe-t-il ? ou sont les shaders ?
pour l'instant, ils sont cachés... pour simplifier la prise en main, gKit construit un shader en fonction du contenu du mesh qu'il dessine. mais la seule chose à retenir est que dessiner \( n \) triangles, provoque l'exécution de \( 3 n \) vertex shaders qui transforment les sommets des triangles vers le repère projectif de la camera. ensuite, la carte graphique détermine sur quels pixels dessiner chaque triangle. un fragment shader est exécuté pour calculer la couleur de chacun de ces pixels (mais on ne connait pas le nombre de fragment shaders exécutés à l'avance...)
rappel : la surface des objets est triangulée pour être dessinée par la carte graphique...
fragment ou pixel ? les pixels sont les éléments de l'image et les fragments sont les morceaux des triangles qui couvrent / remplissent un pixel. un fragment est un bout de geometrie avec toutes les informations associées, normale, couleur, matiere, etc... et plusieurs fragments peuvent se projetter sur le meme pixel, le fragment le plus proche de la camera sera le seul visible dans l'image à la fin du dessin / calcul.
chaque shader a besoin de paramètres pour s'exécuter (les matrices de transformation, par exemple, ou une couleur de base), comme n'importe quelle fonction de l'application. une grosse difference existe par contre : ce n'est pas l'application qui appelle / exécute directement la fonction, c'est le pipeline graphique / la carte graphique. pourquoi ? tout ça est fait en parallèle automatiquement sur les processeurs de la carte graphique, et l'application ne controle pas ce mécanisme.
du coup, on ne peut pas écrire vertex_shader( Translation(0, 2, 0) );
pour déplacer tous les sommets le long de l'axe Y... vu que l'appel de la fonction n'est pas immédiat, l'application affecte une valeur à chaque paramètre (avec un mécanisme particulier), et enfin l'application utilise une fonction draw()
pour dessiner les triangles. au final, c'est le pipeline graphique qui exécute les shaders et leur passe les paramètres.
pour les curieux : les shaders par défaut de gKit sont construits par DrawParam::create_program() dans draw.cpp.
les shaders s'écrivent en GLSL, cf shaders et GLSL, et ils utilisent une convention particulière : la fonction se déclare void main( )
et les paramètres se déclarent comme des variables globales, par exemple :
les résultats (obligatoires...) calculés par les shaders utilisent aussi une convention particulière, ce sont aussi des variables globales déjà déclarées, vec4 gl_Position
pour un vertex shader, et vec4 gl_FragColor
pour un fragment shader.
resultats obligatoires ? les shaders ont un role précis dans le pipeline graphique, chaque étape du pipeline doit produire des resultats qui sont traités par les étapes suivantes. le role du vertex shader est de produire les coordonnées de chaque sommet dans le repère projectif de la camera, et le role du fragment shader est de produire la couleur du fragment...
autre difference, il y a 2 types de paramètres, les uniform
et les autres, que gKit affecte automatiquement. les uniform
sont les paramètres classiques. par exemple, on peut déclarer une matrice de transformation 4x4 avec le type mat4
utilisé dans un vertex shader :
pour un fragment shader, on peut ecrire :
il faut ensuite compiler ces 2 shaders et les linker pour obtenir un shader program utilisable pour dessiner des objets.
par lire shaders et GLSL qui fournit plus d'informations sur GLSL et les paramètres des shaders.
attention : la doc inclut aussi pas mal de renvois sur le fonctionnement et l'utilisation d'openGL, il n'est pas nécessaire de les lire pour démarrer.
ensuite, le plus simple est d'utiliser shader_kit pour écrire et tester les premiers shaders. lorsqu'ils fonctionnent on peut les intégrer à une application en suivant les explications de tuto9.cpp.
si ce n'est pas déjà fait, lisez comment utiliser shader_kit.
les coordonnées des sommets visibles / dessinés sont entre -1 et 1 sur x, y, z (dans le repere projectif de la camera). shader_kit dessine 1 triangle, donc 3 vertex shader sont exécutés pour transformer les sommets d'indice 0, 1, et 2. l'indice du sommet pour lequel le vertex shader s'execute est dans le paramètre (initialisé par le pipeline graphique) gl_VertexID
.
et sans shader_kit ? comme il n'y a pas de (coordonnées de) sommets à stocker dans l'application (le shader produit les sommets tout seul), on ne peut pas utiliser Mesh et Mesh::draw(), mais on peut bien sur utiliser directement une fonction draw()
d'openGL. et comme ces shaders n'ont pas de paramètres non plus, il suffit de dessiner un triangle / 3 sommets pour que le pipeline graphique exécute les shaders.
le vertex shader doit produire des coordonnées dans le repère projectif de la camera. si on veut décrire la position des sommets d'un triangle avec les coordonnées des pixels de l'image, il faut transformer ces coordonnées du repère image vers le repère projectif... avec l'inverse de la matrice viewport. ce parametre mat4 viewportInvMatrix
est affecte par shader_kit.
et sans shader_kit ? cf l'exemple précédent. mais dans ce cas, il faut aussi affecter une valeur au paramètre mat4 viewportInvMatrix
avant l'appel de draw()
:
il suffit d'utiliser uniform float time;
qui donne le temps écoulé depuis le démarrage de shader_kit en millisecondes. on peut par exemple deplacer les sommets du triangle en fonction du temps... mais pas trop vite...
et sans shader_kit ? cf l'exemple précédent. mais dans ce cas, il faut aussi affecter une valeur au paramètre float time
avant l'appel de draw()
:
il faut déclarer les attributs de sommets en respectant la convention de shader kit / Mesh et récupérer la matrice de transformation entre le repère objet et le repère projectif, toujours en fonction des paramètres gérés par shader_kit, cf uniform mat4 mvpMatrix
.
et sans shader_kit ? dans ce cas, il y a bien des coordonnées de sommets à stocker, on va pouvoir utiliser Mesh comme prévu. quels sont les paramètres des shaders ?
in vec3 position
: coordonnées des sommets, géré par Mesh::draw(),uniform mat4 mvpMatrix
: composition des transformations standard, cf tuto9.cpp pour les explicationsles shaders précédents donnent la même couleur à tous les pixels, ce qui permet de dessiner une forme simple mais devient vite très limité lorsque l'on charge un objet 3d...
en reprenant le premier exemple, rien n'empêche d'associer une couleur à chaque sommet du triangle... mais le vertex shader ne peut pas écrire de couleur dans un pixel, c'est le rôle du fragment shader. il faut donc transmettre la couleur au fragment shader en déclarant une sortie dans le vertex shader, cf out vec3 vertex_color;
et l'entrée associée dans le fragment shader, cf in vec3 vertex_color;
. ce paramètre s'appelle un varying, cette valeur n'existe que pendant l'exécution du pipeline graphique, elle n'est pas accessible à l'application...
ce résultat est quand même un peu surprenant, non ? pourquoi la couleur est-elle interpolée entre les sommets ? chaque fragment recoit un mélange de la couleur des 3 sommets (le 1er sommet est rouge, le second vert et le dernier bleu...)
ce mécanisme d'interpolation des varyings, des sorties du vertex shader, fait parti du fonctionnement du pipeline graphique.
et sans shader_kit ? comme dans le tout premier exemple...