OpenGL Moderne

Tutoriel 2 : le premier triangle

Opengl-tutorial.org

Traducteur

Alexandre Laurent

OpenGL 3 facilite l'écriture des choses compliquées, mais possède l'inconvénient de rendre l'affichage d'un simple triangle relativement difficile.

Commentez

- Titre : OpenGL Moderne

- Auteur : Opengl-tutorial.org

- Traducteur : Alexandre Laurent

- Parution : 26 mars 2014

- Mise à jour : 5 février 2015

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 opengl-tutorial.org. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Navigation

- I. Introduction

- II. Les Vertex Array Object

- III. Coordonnées écran

- IV. Dessiner notre triangle

- V. Shaders

- V-A. Compilation de shader

- V-B. Notre vertex shader

- V-C. Notre fragment shader

- VI. Rassembler le tout

- VII. Remerciements

- Navigation

Navigation

Tutoriel précédent : ouvrir une fenêtre

Sommaire

Tutoriel suivant : matrices

I - Introduction

Cela va être un autre long tutoriel.

OpenGL 3 facilite l'écriture des choses compliquées, mais possède l'inconvénient de rendre l'affichage d'un simple triangle relativement difficile.

N'oubliez pas de copier/coller le code régulièrement.

Si le programme crashe au démarrage, vous l'exécutez certainement à partir du mauvais répertoire. Lisez PRÉCAUTIONNEUSEMENT le premier tutoriel sur comment configurer Visual Studio.

II - Les Vertex Array Object

Je ne vais pas m'enfoncer dans les détails maintenant, mais vous devez créer un Vertex Array Object (VAO) et le définir comment objet courant.

GLuint VertexArrayID;
glGenVertexArrays(1, &VertexArrayID);
glBindVertexArray(VertexArrayID);

Faites-le une fois que votre fenêtre est créée (= après la création du contexte OpenGL) et avant tout autre appel OpenGL.

Si vous souhaitez vraiment en apprendre plus sur les VAO, il y a quelques autres tutoriels sur le Web, mais ce n'est pas très important.

III - Coordonnées écran

Un triangle est défini par trois points. Lorsque l'on parle de « points » en graphismes 3D, on utilise habituellement le terme de « sommet » (en anglais « vertex », « vertices » au pluriel). Un sommet possède trois coordonnées : X, Y et Z. Vous pouvez imaginer ces trois coordonnées de la manière suivante :

- X est sur votre droite ;

- Y, vers le haut ;

- Z est derrière vous (oui, derrière et non devant).

Mais voici une meilleure méthode pour les visualiser : utilisez la règle de la main droite :

- X est votre pouce ;

- Y, votre index ;

- Z est votre majeur. Si vous placez votre pouce sur la droite et votre index vers le ciel, votre majeur pointera aussi derrière votre dos.

Il est étrange d'avoir l'axe Z dans cette direction. Pourquoi est-ce ainsi ? Pour faire court : car cent années de « règle de la main droite » vous donneront des outils pratiques. La seule contrepartie est un axe Z contre-intuitif.

Mis à part cela, remarquez aussi que vous pouvez bouger votre main librement : votre X, Y et Z suivront. On reviendra sur ce point.

Donc, on a besoin de trois points 3D afin de faire un triangle ; les voici :

// Un tableau de trois vecteurs qui représentent trois sommets
static const GLfloat g_vertex_buffer_data[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
0.0f,  1.0f, 0.0f,
};

Le premier sommet est (-1, -1, 0). Cela signifie que, sauf si nous le transformons d'une quelconque manière, il sera affiché à la position (-1, -1) à l'écran. Qu'est-ce que cela donne ? L'origine de l'écran est au centre, l'axe X va vers la droite, comme toujours et l'axe Y vers le haut. Voici ce que cela donne sur un écran large :

C'est une chose que vous ne pouvez modifier, c'est intégré dans votre carte graphique. Donc (-1, -1) est le coin inférieur gauche de votre écran. (1, -1) est le coin inférieur droit et (0, 1), le milieu haut. Donc ce triangle devrait couvrir la majorité de l'écran.

IV - Dessiner notre triangle

La prochaine étape est de fournir ce triangle à OpenGL. Pour cela il faut créer un tampon(1) :

// Ceci identifiera notre tampon de sommets
GLuint vertexbuffer;

// Génère un tampon et place l'identifiant dans 'vertexbuffer'
glGenBuffers(1, &vertexbuffer);

// Les commandes suivantes vont parler de notre tampon 'vertexbuffer'
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);

// Fournit les sommets à OpenGL.
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);

Cela n'est nécessaire qu'une seule fois au lancement du programme.

Maintenant, la boucle principale, où on a l'habitude de ne « rien » dessiner. On peut maintenant dessiner un fantastique triangle :

// premier tampon d'attributs : les sommets
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glVertexAttribPointer(
0,                  // attribut 0. Aucune raison particulière pour 0, mais cela doit correspondre au « layout » dans le shader
3,                  // taille
GL_FLOAT,           // type
GL_FALSE,           // normalisé ?
0,                  // nombre d'octets séparant deux sommets dans le tampon
(void*)0            // décalage du tableau de tampon
);

// Dessine le triangle !
glDrawArrays(GL_TRIANGLES, 0, 3); // Démarre à partir du sommet 0; 3 sommets au total -> 1 triangle

glDisableVertexAttribArray(0);

Si vous avec une carte NVIDIA, vous pouvez voir le résultat (pour les autres cartes, continuez de lire) :

Ce blanc est bien ennuyeux. Voyons voir comment l'améliorer en l'affichant en rouge. Cela peut être fait avec quelque chose appelé « shader ».

V - Shaders

V-A - Compilation de shader

Dans la configuration la plus simple, vous avez besoin de deux shaders : un appelé « Vertex Shader », qui sera exécuté pour chaque sommet et l'autre appelé « Fragment Shader », qui sera exécuté pour chaque fragment. Comme on utilise un antialiasing 4x, on a quatre échantillons pour chaque pixel.

Les shaders se programment avec un langage appelé GLSL : GL Shader Language, qui est intégré à OpenGL. Contrairement au C ou au Java, le GLSL doit être compilé durant l'exécution du programme, ce qui signifie que chaque fois que vous lancez votre application, tous vos shaders sont recompilés.

Les deux shaders sont généralement dans des fichiers distincts. Dans cet exemple, nous avons SimpleFragmentShader.fragmentshader et SimpleVertexShader.vertexshader. L'extension importe peu, cela aurait pu être .txt ou .glsl.

Voici enfin le code pour charger des shaders. Il n'est pas très important de le comprendre entièrement, car vous ne le faites qu'une seule fois dans le programme, les commentaires suffiront. Comme cette fonction va être utilisée dans tous les autres tutoriels, elle est placée dans un fichier à part : common/loadShader.cpp. Notez que tout comme les tampons, les shaders ne sont pas directement accessibles : nous n'avons qu'un identifiant. L'implémentation actuelle est cachée par le pilote.

GLuint LoadShaders(const char * vertex_file_path,const char * fragment_file_path){

// Crée les shaders
GLuint VertexShaderID = glCreateShader(GL_VERTEX_SHADER);
GLuint FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);

// Lit le code du vertex shader à partir du fichier
std::string VertexShaderCode;
std::ifstream VertexShaderStream(vertex_file_path, std::ios::in);
if(VertexShaderStream.is_open())
{
std::string Line = "";
while(getline(VertexShaderStream, Line))
VertexShaderCode += "\n" + Line;
VertexShaderStream.close();
}

// Lit le code du fragment shader à partir du fichier
std::string FragmentShaderCode;
std::ifstream FragmentShaderStream(fragment_file_path, std::ios::in);
if(FragmentShaderStream.is_open()){
std::string Line = "";
while(getline(FragmentShaderStream, Line))
FragmentShaderCode += "\n" + Line;
FragmentShaderStream.close();
}

GLint Result = GL_FALSE;
int InfoLogLength;

// Compile le vertex shader
printf("Compiling shader : %s\n", vertex_file_path);
char const * VertexSourcePointer = VertexShaderCode.c_str();
glShaderSource(VertexShaderID, 1, &VertexSourcePointer , NULL);
glCompileShader(VertexShaderID);

// Vérifie le vertex shader
glGetShaderiv(VertexShaderID, GL_COMPILE_STATUS, &Result);
glGetShaderiv(VertexShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
std::vector<char> VertexShaderErrorMessage(InfoLogLength);
glGetShaderInfoLog(VertexShaderID, InfoLogLength, NULL, &VertexShaderErrorMessage[0]);
fprintf(stdout, "%s\n", &VertexShaderErrorMessage[0]);

// Compile le fragment shader
printf("Compiling shader : %s\n", fragment_file_path);
char const * FragmentSourcePointer = FragmentShaderCode.c_str();
glShaderSource(FragmentShaderID, 1, &FragmentSourcePointer , NULL);
glCompileShader(FragmentShaderID);

// Vérifie le fragment shader
glGetShaderiv(FragmentShaderID, GL_COMPILE_STATUS, &Result);
glGetShaderiv(FragmentShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
std::vector<char> FragmentShaderErrorMessage(InfoLogLength);
glGetShaderInfoLog(FragmentShaderID, InfoLogLength, NULL, &FragmentShaderErrorMessage[0]);
fprintf(stdout, "%s\n", &FragmentShaderErrorMessage[0]);

// Lit le programme
fprintf(stdout, "Linking program\n");
GLuint ProgramID = glCreateProgram();
glAttachShader(ProgramID, VertexShaderID);
glAttachShader(ProgramID, FragmentShaderID);
glLinkProgram(ProgramID);

// Vérifie le programme
glGetProgramiv(ProgramID, GL_LINK_STATUS, &Result);
glGetProgramiv(ProgramID, GL_INFO_LOG_LENGTH, &InfoLogLength);
std::vector<char> ProgramErrorMessage( max(InfoLogLength, int(1)) );
glGetProgramInfoLog(ProgramID, InfoLogLength, NULL, &ProgramErrorMessage[0]);
fprintf(stdout, "%s\n", &ProgramErrorMessage[0]);

glDeleteShader(VertexShaderID);
glDeleteShader(FragmentShaderID);

return ProgramID;
}

V-B - Notre vertex shader

Écrivons le vertex shader.

La première ligne indique au compilateur que l'on va utiliser la syntaxe de OpenGL 3.

#version 330 core

La seconde ligne déclare les données d'entrées :

layout(location = 0) in vec3 vertexPosition_modelspace;

Expliquons-la en détail :

- « vec3 » est un vecteur de trois composantes dans le GLSL. Il est similaire (mais différent) au glm::vec3 que nous avons utilisé pour définir notre triangle. Le point important est que si on utilise trois composantes en C++, nous utilisons aussi trois composantes dans le GLSL ;

- « layout(location = 0) » se réfère au tampon que l'on fournit à l'attribut vertexPosition_modelspace. Chaque sommet peut avoir de nombreux attributs : une position, une ou plusieurs couleurs, une ou plusieurs coordonnées de texture et plein d'autres choses. OpenGL ne sait pas ce qu'est une couleur : il ne voit qu'un vec3. Donc on doit lui dire quel tampon correspond à quelle entrée. Nous le faisons en définissant le « layout » à la même valeur que le premier paramètre de la fonction glVertexAttribPointer. La valeur « 0 » n'est pas importante, cela aurait pu être 12 (mais, elle ne peut être supérieure à glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &v)), la chose importante est qu'elle soit la même des deux côtés ;

- « vertexPosition_modelspace » aurait pu être n'importe quoi. Il contiendra la position des sommets pour chaque exécution du vertex shader ;

- « in » signifie que ce sont des données d'entrée. Bientôt on va voir le mot clé « out ».

La fonction qui est appelée pour chaque sommet est appelée main, tout comme en C :

void main(){

La fonction principale va simplement définir la position du vertex à ce qui est dans le tampon. Donc, si on donne (1, 1), le triangle aura l'un de ces sommets au coin supérieur droit de l'écran. On verra dans le prochain tutoriel comment effectuer des calculs plus intéressants sur les positions passées au shader.

gl_Position.xyz = vertexPosition_modelspace;
gl_Position.w = 1.0;
}

gl_Position est l'une des variables du langage : vous devez assigner une valeur à celle-ci. Tout le reste est optionnel ; on verra « tout le reste » dans le quatrième tutoriel.

V-C - Notre fragment shader

Pour le premier fragment shader, on va faire quelque chose de très simple : définir la couleur de chaque fragment à rouge. (Rappelez-vous, il y a quatre fragments dans un pixel, car nous utilisons l'antialiasing 4x.)

#version 330 core
out vec3 color;

void main(){
color = vec3(1,0,0);
}

Donc voilà, vec3(1,0,0) signifie rouge. Cela est dû aux écrans d'ordinateur. La couleur est représentée par un triplet rouge, vert, bleu, dans cet ordre. Donc (1, 0, 0) indique complètement rouge, pas de vert et pas de bleu.

VI - Rassembler le tout

Avant la boucle principale, on appelle la fonction LoadShaders :

// Crée et compile notre programme GLSL à partir des shaders
GLuint programID = LoadShaders( "SimpleVertexShader.vertexshader", "SimpleFragmentShader.fragmentshader" );

En premier, dans la boucle principale, on nettoie l'écran. Cela changera la couleur de fond en bleu foncé à cause de l'appel glClearColor(0.0f, 0.0f, 0.4f, 0.0f) au-dessus de la boucle principale.

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Puis on indique à OpenGL que l'on souhaite les shaders vus précédemment :

// Utilise notre shader
glUseProgram(programID);

// Dessine le triangle...

… et voilà, un triangle rouge !

Dans le prochain tutoriel, on étudiera les transformations : comment définir une caméra, déplacer les objets, etc.

VII - Remerciements

Cet article est une traduction autorisée dont le texte original peut être trouvé sur opengl-tutorials.org.

Navigation

Tutoriel précédent : ouvrir une fenêtre

Sommaire

Tutoriel suivant : matrices

(1)Note du traducteur : en anglais « buffer » : un espace mémoire qui contiendra des données