4/17
3.1. Shader program
Qu’est ce qu’un shader
Un shader est un programme défini pour l’utilisateur qui est exécuté sur la carte graphique (et donc en parallèle). Les shaders ont été introduits pour permettre au développeur d’adapter la manière dont le rendu des données est réalisé (/adaptation du Pipe-Line Graphique). En particulier, les shaders permettent de modifier l’apparence des matériaux (apparence visuelle de la "matière" des objets virtuels) en fonction de l’orientation de la lumière - appelé shading. Le nom de shader est resté, bien qu’ils puissent aujourd’hui être utilisés au-delà de l’action de la lumière sur la forme (ex. déformation géométrique, subdivision, calculs génériques, etc).
Les shaders OpenGL sont écrits en langage GLSL (OpenGL Shading Language). Le GLSL ressemble à du code C++ simplifié, et spécialisé pour la manipulation de données 3D à l’aide de vecteurs et matrices.
Pour être exécuté par la carte graphique, le code GLSL doit être compilé et lié en un shader program binaire. A l’opposé de la compilation d’un code C++ , la compilation des shader est très rapide, celle-ci est réalisée à chaque lancement du programme exécutable (compilation dite jit: just in time).
Un shader program est composé, d’au moins, deux parties différentes: Le vertex shader et le fragment shader [1]
-
Vertex shaders correspond à un programme exécuté en parallèle sur chaque sommet (vertex) des données. Le rôle typique du vertex shader consiste à appliquer une transformation entre les coordonnées des sommets exprimées dans l’espace du monde (voir dans un repère local) vers l’espace de vue normalisée d’OpenGL (plus précisément les clip coordinates) en appliquant une matrice dite de projection sur les coordonnées des sommets. Plus généralement, un vertex shader reçoit en entrée des sommets, et renvoie des informations de sommets modifiés.
-
Fragment shaders correspond à un programme exécuté en parallèle sur chaque fragment visible dans la fenêtre. Un fragment correspond à un élément de primitive rasterisé (\(\simeq\) un pixel appartenant à un morceau visible d’un triangle). Le rôle typique du fragment shader consiste à calculer la couleur finale d’un fragment (et donc d’un pixel affiché). Plus généralement, un fragment shader reçoit en entrée des données de sommets interpolées linéairement sur la primitive, et renvoie en sortie une couleur.
Application
Nous considérons dans cet exemple un shader minimaliste: aucune transformation n’est appliquée sur les sommets, et la couleur de sortie est rouge uniforme.
Vertex shader correspondant
#version 330 core
layout (location = 0) in vec4 position;
void main()
{
gl_Position = position;
}
-
#version 330 coreindique l’utilisation de GLSL 3.30 (en accord avec la version d’OpenGL) -
layout (location = 0) in vec4 positionindique que le premier paramètre associé à un vertex (position indicée par 0) est reçu dans une variable nommée position. Cette variable est un vecteur 4D \((x,y,z,w)\). Dans ce cas, il n’y a qu’un seul paramètre par sommet. -
Une fois que les variables globales ont été déclarées (ici position) le code GLSL définit une fonction main avec le code à exécuter.
-
L’un des objectifs du vertex shader est de compléter la variable globale de sortie gl_Position. Cette variable contient les coordonnées dans l’espace des clip coordinates utilisés par OpenGL pour afficher la primitive dans la fenêtre.
Le fragment shader est le suivant
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
-
Dans ce cas, la variable globale de sortie est FragColor (vecteur 4D) interprété comme la couleur du fragment court (r,g,b,a). Ici, le vecteur (1,0,0,0) indique une couleur rouge uniforme (la composante a n’ayant pas d’influence dans cet exemple).
Notons que dans le code C++ fourni, les shaders sont codés en dur en tant que chaine de caractère dans le code.
L’étape suivante consiste à compiler chaque shader, puis les lier pour un générer un _shader program. Le code correspondant est le suivant
// ************************************************* //
// 1 - Setup Shaders //
// ************************************************* //
std::cout<<"*** Setup Shader ***"<<std::endl;
// ******************************** //
// 1.1 Define vertex and fragment shader
// ******************************** //
// Here GLSL code is hard-coded inline as strings
const char* vertex_shader_txt = " \n \
#version 330 core \n \
layout (location = 0) in vec4 position; \n \
void main() \n \
{ \n \
gl_Position = position; \n \
}";
const char* fragment_shader_txt = " \n \
#version 330 core \n \
out vec4 FragColor; \n \
void main() \n \
{ \n \
FragColor = vec4(1.0, 0.0, 0.0, 1.0); \n \
}";
// ******************************** //
// 1.2 Create shader program
// ******************************** //
// (Warning: the following code doesn't perform error checking)
// A. Compile each shader separately
// ******************************************* //
// Create identifiant for vertex and fragment shaders
const GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
const GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
// Set source from the shader
glShaderSource(vertex_shader, 1, &vertex_shader_txt, nullptr);
// Compile the current shader (here vertex shader)
glCompileShader(vertex_shader);
// Set source from the shader
glShaderSource(fragment_shader, 1, &fragment_shader_txt, nullptr);
// Compile the current shader (here fragment shader)
glCompileShader(fragment_shader);
// B. Link shaders into shader program
// ******************************************* //
// Create identifiant for shader program
const GLuint shader = glCreateProgram();
// Attach vertex and fragment shader before linking
glAttachShader(shader, vertex_shader);
glAttachShader(shader, fragment_shader);
// Perform the link of the two shaders into a program
glLinkProgram(shader);
// Compiled shaders can be safely deleted
glDeleteShader(vertex_shader);
glDeleteShader(fragment_shader);
Document supplémentaire sur les différentes fonctions