Navigation▲
Tutoriel précédent : ouvrir une fenêtre |
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.0
f, -
1.0
f, 0.0
f,
1.0
f, -
1.0
f, 0.0
f,
0.0
f, 1.0
f, 0.0
f,
}
;
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 |
Tutoriel suivant : matrices |