mettez à jour (cf git pull) ou clonez gkit3 :
git clone
https://forge.univ-lyon1.fr/JEAN-CLAUDE.IEHL/gkit3.git
puis dans le répertoire de gKit3,
clonez gKit3GL,
la partie openGL de gKit :
cd gkit3
git clone
https://forge.univ-lyon1.fr/JEAN-CLAUDE.IEHL/gkit3GL
cd gkit3GL
premake5 gmake
make tp1
bin/tp1
vous pouvez compiler l'exemple tp1 et vérifier que tout est correct.
pour windows, et linux, il faut installer les librairies openGL, les différentes étapes sont résumées sur le dépot.
ce tp est volontairement rédigé avec pas mal de détails (ie il y a
plein de texte...) pour que vous puissiez le faire en
quasi-autonomie, à votre rythme, et me laisser le temps de régler
les n problèmes rigolo d'installation...
pour dessiner un objet avec openGL, il faut créer une fenêtre avec les bons paramètres pour qu'openGL puisse dessiner dedans. cette étape n'est pas très intéressante, mais surtout elle est différente d'un système à l'autre, ie X11 ne crée pas les fenêtres de la même manière que Wayland, ni que Windows et encore moins que Macos, ou Android ou WebGL, ou, etc...
on va donc utiliser une librairie pour faire ça de manière portable, par exemple : SDL2.
l'utilisation de la librairie n'est pas très intéressante non plus, il suffit de lire la doc sur la création des fenêtres et du contexte openGL et d'appeler les fonctions dans le bon ordre. et comme c'est toujours la même chose, il n'a pas été trop difficile de créer 2 fonctions utilitaires : create_window() et create_context() définies dans window.h
pour les curieux, il y a un résumé dans la doc en ligne, cf écrire
une application openGL
maintenant que la fenêtre est créée, il suffit de dessiner dedans
et de recommencer tant que l'application existe...
mais pour savoir que l'utilisateur veut fermer l'application, il
faut le tester explicitement. ça s'appelle la gestion des
évènements...
c'est un peu barbare, mais ce n'est pas compliqué, par contre, il y a pleins d'évènements, cf la doc.
pour savoir si la souris vient de cliquer sur le bouton 'fermer'
de la fenêtre de l'application, il faut surveiller l'évènement
SDL_WindowEvent, on peut aussi surveiller le clavier, ie
SDL_KeyboardEvent, pour détecter que la touche 'echap' est
enfoncée ou que 'ctrl'+'w' / 'ctrl'+'q' est enfoncé. ie toutes les
manières classiques de fermer la fenêtre d'une application.
c'est SDL_PollEvent() qui permet de récupérer les évènements et
de réagir, le code ressemblera à :
bool close= false;// recuperer un evenement a la fois// PollEvent() renvoie faux lorsque tous les
evenements ont ete traiteSDL_Event event;while(SDL_PollEvent(&event)){ if(event.type == SDL_QUIT)
close= true; // sortir si click sur le bouton 'fermer' de
la fenetre else if(event.type ==
SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE)
close= true; // sortir si la touche esc / echapp est
enfoncee}projets/tp1.cpp est un exemple complet et fonctionnel.projets/tp1.cpp
doit être un peu plus claire. Window window= create_window(1024, 576);
Context context=
create_context(window); std::vector<Point> positions;
std::vector<unsigned> indices;
// lit le fichier, et recupere un
tableau de positions de sommets et un tableau d'indice des sommets
des triangles de l'objet
if(!read_indexed_positions("../data/robot.obj", positions,
indices))
return
false; // erreur de lecture
count= indices.size();
// utilitaire. alloue de la memoire
gpu et transfere les tableaux positions et indices
sur le gpu. necessaire pour dessiner...
vao= create_buffers(positions,
indices);
bool close= false;
while(!close)
{
SDL_Event
event;
while(SDL_PollEvent(&event))
{
if(event.type == SDL_QUIT)
close= true; // sortir si click sur le bouton 'fermer' de la
fenetre
else if(event.type == SDL_KEYDOWN && event.key.keysym.sym
== SDLK_ESCAPE)
close= true; // sortir si la touche esc / echapp est
enfoncee
}
// dessiner
draw();
// presenter
/ montrer le resultat
SDL_GL_SwapWindow(window);
} release_buffers(vao); /* utilitaire. dessine des TRIANGLES avec
count indices (ie indices/3 triangles...)
en appliquant les
transformations model, view et projection aux coordonnées des
sommets.
*/
draw(vao, GL_TRIANGLES, count,
model, view, projection);
pour dessiner un objet, il faut décrire sa forme avec des points,
des lignes ou des triangles, le placer dans le monde, ainsi que
placer et définir une camera pour l'observer.
par exemple, pour décrire un objet composé d'un seul triangle, il
suffit d'indiquer les coordonnées de ses sommets :
ou, équivalent, mais plus rapide à taper en utilisant l'initialisation par défaut :std::vector<Point> triangle= {
Point(0, 0, 0),
Point(1, 0, 0),
Point(0, 1, 0)
};
std::vector<Point> triangle= {
{ 0, 0, 0 },
{ 1, 0, 0 },
{ 0, 1, 0 }
};
avec ce tableau de points, on peut maintenant créer le buffer openGL et un Vertex Array Object (VAO) qui décrit les informations stockées dans le buffer. pour l'instant, on va utiliser un utilitaire de gkit3 :
GLuint vao= create_buffers( triangle );
ce qui permettra de le dessiner avec un autre utilitaire de gkit3
:
draw(vao, GL_TRIANGLES, 3 );
pour les curieux : écrire le code openGL correspondant est
décrit dans la doc
en ligne. on verra cette partie plus tard.
on peut également associer d'autres informations aux sommets d'un
objet en complément de sa position. une couleur, par exemple, ou
une normale (utile pour les calculs d'éclairage de la fin du
tp...) ou des coordonnées de texture... il suffit de passer les
paramètres dans le bon ordre à la fonction create_buffers().
il est également possible d'indexer les sommets pour décrire la
géométrie. dans un maillage classique chaque sommet est partagé
par 3 voire 6 triangles, l'indexation évite de stocker et de
transformer plusieurs fois le même sommet. le tableau d'indices
est aussi un paramètre de create_buffers().
exemple, un quad avec 2 triangles, en indexant leurs sommets :
std::vector<Point> positions= {
{ 0.5, -0.5, -0.5 },
{ -0.5, -0.5, -0.5 },
{ -0.5, 0.5, -0.5 },
{ 0.5, 0.5, -0.5 }
};
std::vector<unsigned> indices= {
0, 1, 2, // premier triangle
2, 3, 0 // suivant
};
GLuint vao= create_buffers( positions, indices, /* texcoords */ {}, /* normals */ {}, /* colors */ {} );
si l'on souhaite associer une couleur aux 3 sommets du triangle du tout premier exemple, il suffit de fournir un tableau de Color à create_buffer() :
std::vector<Color> couleurs= {
Red(), // ou Color(1, 0, 0)
Green(),
Blue()
};
GLuint vao= create_buffers( triangle, /* indices */ {}, /* texcoords */ {}, /* normals */ {}, /* colors */ couleurs );
il faut faire attention à l'ordre des paramètres, mais ce n'est pas difficile.
on peut constuire les axes d'un repère, ce sera assez pratique
pour expérimenter avec les transformations pour placer plusieurs
objets dans la scène... par exemple :
std::vector<Point> positions;
std::vector<Color> colors;
colors.push_back( Red() ); positions.push_back( { 0, .1, 0 } );
colors.push_back( Red() ); positions.push_back( { 2, .1, 0 } );
colors.push_back( Green() ); positions.push_back( { 0, .1, 0 } );
colors.push_back( Green() ); positions.push_back( { 0, 2, 0 } );
colors.push_back( Blue() ); positions.push_back( { 0, .1, 0 } );
colors.push_back( Blue() ); positions.push_back( { 0, .1, 2 } );
GLuint vao= create_buffers( positions, {}, {}, {}, colors );
on peut aussi matérialiser le sol en construisant une grille de n
par n cases autour de l'origine du repère :
std::vector<Point> positions;
std::vector<Color> colors;
// grille
for(int x= 0; x < n; x++)
{
float px= float(x) - float(n-1)/2;
positions.push_back( { px, 0, - float(n-1)/2 } );
positions.push_back( { px, 0, float(n-1)/2 } );
}
for(int z= 0; z < n; z++)
{
float pz= float(z) - float(n-1)/2;
positions.push_back( { - float(n-1)/2, 0, pz });
positions.push_back( { float(n-1)/2, 0, pz } );
}

une fois la description de la géométrie des objets terminée, il
reste à placer les objets dans le monde et à les dessiner...
il y a plusieurs solutions pour décrire les transformations /
changements de repères qui permettent de placer et d'orienter des
objets dans le monde, mais les api 3d comme openGL, Vulkan,
DirectX ou Metal imposent d'utiliser des matrices 4x4 homogènes.
c'est un outil qu'il faut apprendre à manipuler.
pour placer un objet dans le monde, on modifie la matrice model :
avec une translation, une rotation, un changement de taille, ou la
composition de plusieurs transformations. mat.h
définit les transformations usuelles, cf Translation, Rotation,
Scale...
pour décrire une camera, on utilise aussi une matrice, view, pour représenter le passage entre le repère du monde et le repère de la camera. on construit ce changement de repère comme pour placer les objets dans le monde, ie avec des translations, rotations, etc. la projection réalisée par la camera est également décrite par une matrice de projection, cf Perspective, par exemple.
la suite de la lecture dans la doc en ligne : premiers
objets et transformations, à partir de la section "placer un
objet dans le monde". le début du tuto portant sur la description
des objets n'est pas adapté à gkit3.
remarque : la doc en
ligne et ses exemples sont rédigés pour gkit2light, il y a donc
quelques différences. par exemple, la classe Mesh n'existe plus
dans gkit3, elle est remplacée par la fonction create_buffers().
les paramètres de la fonction draw(), qui affiche les objets, sont
également différents.
voici comment adapter les exemples de la doc, par exemple le 1er :
draw(m_objet, model, camera());
// version gkit3
Transformmodel=Translation(0, 2, 0);draw(vao, GL_TRIANGLES, count, model, view, projection);
et d'en profiter pour écrire des fonctions utilitaires pour construire une grille avec les axes XYZ comme dans l'illustration au dessus :struct Mesh
{
GLenum primitives;
GLuint vao;
unsigned count;
};
// utilisation
Mesh mesh= { ... };
draw(mesh.vao, mesh.primitives, mesh.count, model, view, projection);
Mesh make_grid( const int n= 10 )
{
std::vector<Point> positions;
std::vector<Color> colors;
// grille
for(int x= 0; x < n; x++)
{
float px= float(x) - float(n-1)/2;
colors.push_back( White() ); positions.push_back( { px, 0, - float(n-1)/2 } );
colors.push_back( White() ); positions.push_back( { px, 0, float(n-1)/2 } );
}
for(int z= 0; z < n; z++)
{
float pz= float(z) - float(n-1)/2;
colors.push_back( White() ); positions.push_back( { - float(n-1)/2, 0, pz });
colors.push_back( White() ); positions.push_back( { float(n-1)/2, 0, pz } );
}
// axes XYZ en rouge / vert / bleu
colors.push_back( Red() ); positions.push_back( { 0, .1, 0 } );
colors.push_back( Red() ); positions.push_back( {1, .1, 0 } );
colors.push_back( Green() ); positions.push_back( { 0, .1, 0 } );
colors.push_back( Green() ); positions.push_back( { 0, 1, 0 } );
colors.push_back( Blue() ); positions.push_back( { 0, .1, 0 } );
colors.push_back( Blue() ); positions.push_back( { 0, .1, 1 } );
glLineWidth(2);
return { GL_LINES, create_buffers( positions, {}, {}, {}, colors ), positions.size() };
}

faites le tuto dans la doc en ligne : premiers
objets et transformations, à partir de la section
"placer un objet dans le monde". les parties précédentes
portant sur la description des objets ne sont pas adaptées à
gkit3. par contre, vous avez maintenant les mêmes outils.
key_state() et clear_key_state(),
il faut utiliser la fonction utilitaire de gestion d'évènements
events() définie dans window.h. projets/tp3.cpp donne un exemple complet qui
utilise key_state() pour déplacer un objet. int x, y;
unsigned buttons= SDL_GetMouseState(&x, &y);
if(buttons & SDL_BUTTON(1))
{
// on vient de cliquer avec le bouton gauche de la souris sur le pixel (x, y) de la fenetre
...
}

que doit on calculer ?
il suffit de calculer le cosinus de l'angle entre la normale en p et la direction vers la lumière, et de se rappeler que les calculs sur les points et les vecteurs doivent se faire avec des coordonnées dans le même repère.
en résumé, il faut connaitre, dans le même repère :
quel shader fait le calcul ? et oui, il faudra probablement transformer certaines coordonnées.
solution 1 : vertex shadercomment récupérer toutes ces
informations dans le vertex shader ? une fois que les coordonnées
de tous ces points et vecteurs sont connues dans le même repère,
il ne reste plus qu'à faire le calcul...
dernière question, comment transmettre la couleur que l'on vient de calculer au fragment shader ? et pourquoi vouloir faire ça ? y-a-t-il une autre solution ?
solution 2 : fragment shaderle résultat est-il différent ? ou,
pourquoi ça marche ?
quelle est la meilleure solution ?