gKit2 light
|
C'est la classe Mesh qui permet de décrire / représenter la géométrie des objets que l'on veut afficher. openGL ne sait dessiner que des points, segments et des triangles. La première étape consiste à initialiser un objet Mesh en fonction du type de primitives que l'on va utiliser pour décrire l'objet :
Ensuite, il ne reste plus qu'à décrire les sommets des primitives : un triangle est composé de 3 sommets, un segment par 2, et un point par, ... 1 seul sommet. Pour ajouter un sommet dans un objet Mesh, on utilise Mesh::vertex( ) :
D'autres informations peuvent être utilisées pour décrire chaque sommet d'un objet : sa positon, bien sur, mais aussi une couleur, une normale, et des coordonnées de texture : cf Mesh::vertex(), Mesh::color(), Mesh::normal(), Mesh::texcoord(). par exemple pour décrire un triangle rouge, on peut écrire :
attention : il y a une convention à respecter, on peut décrire les informations d'un sommet dans n'importe quel ordre, mais il faut finir par Mesh::vertex(). cette convention permet aussi d'éviter de répéter les informations communes à plusieurs sommets. dans l'exemple précédent, tous les sommets sont rouges, et on peut aussi écrire :
autre exemple, dessiner les axes d'un repère, ce qui sera bien utile pour les parties suivantes :
remarque : Mesh::vertex(), et les autres sont surchargées avec les types de base : Point, Vector, Color, mais aussi avec les vecteurs vec2, vec3, vec4, en plus des versions acceptant les différentes composantes : x, y, z, w ou r, g, b, a.
on peut également completer cet objet utilitaire avec une grille, pour materialiser le "sol" de la scène et ses dimensions :
Dans certains cas, il est plus simple de décrire l'objet comme un ensemble de sommets indexés par les triangles. Un triangle est donc un triplet d'indices de sommets, au lieu d'être représenté par un triplet de sommets.
exemple :
et accessoirement, décrire les objets de cette manière permet à la carte graphique de les afficher un peu plus vite (c'est surtout visible pour les "gros" objets composés de >1M de triangles)
pour observer une scène / un objet, il faut également une camera, un point de vue. La classe Orbiter est un type particulier de camera, différente des cameras classiques : elle ne fait que tourner / orbiter autour d'un centre à une certaine distance. ce qui est très pratique pour observer quelques objets, mais complètement différent des cameras 1ère et 3ième personne.
Le plus simple pour commencer est d'utiliser la variante de la classe Application qui gère automatiquement une camera :
c'est AppCamera::camera() qui renvoie la camera gérée par l'application.
pour dessiner un objet, le plus simple est d'utiliser la fonction draw( ) qui prend en paramètre l'objet à dessiner, l'orbiter / la camera et une matrice / transformation qui permet de placer l'objet dans le monde et de l'observer :
La transform Identity() place tout simplement l'objet au centre du monde... les coordonnées des sommets de l'objet ne sont pas modifiées / transformées.
La camera initialisée par défaut observe le centre de la scène et une région assez petite autour. mais on peut décrire la région que la camera doit observer. Il y a 2 manières de décrire cette région observée par la camera, cf Orbiter::lookat() :
il est aussi possible de charger un fichier .obj / wavefront
pour initialiser un objet Mesh :
et de récupérer les dimensions de l'objet / la région occuppée par l'objet pour régler la camera, cf Mesh::bounds() :
pour placer un objet dans le monde, il suffit de modifier / transformer les coordonnées de ses sommets, c'est la matrice model
qui représente ce changement de repère et c'est un paramètre de draw() :
pour placer le cube ailleurs dans le monde, il suffit de modifier la transformation model
, par exemple avec une translation vers le haut, le long de l'axe Y (0,1,0) :
pour orienter / faire tourner un objet, on peut utiliser une rotation, autour de l'axe X, par exemple :
le résultat est sans surprise. mais comment placer et orienter un objet dans le monde ?
la réponse utilise la composition de transformations, le produit des matrices qui les représentent... et le produit de matrices n'est pas commutatif, \( A∗B \) n'est pas la même valeur que \( B∗A \)...
et l'autre solution :
il faut se rappeler ce qui arrive aux points qui sont transformés par ces matrices. par exemple si \( q \) est la transformation de \( p \) par une matrice \( m \). on peut écrire :
\( q=m∗p \)
le point est à droite de la matrice. Si \( m \) est le produit de 2 matrices \( m=a∗b \) :
\( q=a∗b∗p\)
le produit s'évalue de la droite vers la gauche :
\(q=a∗(b∗p) \)
donc \( p \) est d'abord multiplié par \( b \), et le résultat est ensuite multiplié par \( a \).
résultat avec les rotations et les translations : soit 1/ on tourne l'objet, sur place, puis 2/ on le pousse,
\( q=t∗r∗p \), cf solution 1
soit, 1/ on déplace l'objet, et 2/ il tourne ensuite sur lui meme :
\( q=r∗t∗p \), cf solution 2
c'est la meme chose, il suffit de composer la transformation du 2ieme avec celle du premier et de calculer le produit des matrices pour dessiner le 2ieme objet par rapport a la transformation du premier.
on peut connaitre l'état des touches / flêches directionnelles du clavier avec key_state( SDL_KeyCode )
. Les codes des fleches sont :
les codes des autres touches du clavier sont dans la doc de SDL2, colonne KeyCode
par exemple :
comment representer un deplacement en avant / en arriere ? tout simplement par une translation le long du vecteur Z (0,0,1)
il ne reste plus qu'à dessiner l'objet à sa nouvelle position... sauf qu'il faut aussi se rappeler sa position actuelle...
la solution est encore d'utiliser une composition de transformations :
le déplacement est composé avec l'ancienne position, pour obtenir la nouvelle position. et ca marche aussi avec des rotations (autour de l'axe Y, par exemple) :
code complet dans tuto_transformations.cpp
par contre, il faut ajuster la taille du deplacement : normalement render()
est appellée 60 fois par secondes, et la valeur du déplacement devient une vitesse, en 1 seconde l'objet de déplace 60 fois...
si, pour une raison quelconque, render()
n'est appellée que 20 ou 30 fois par seconde, la vitesse de déplacement de l'objet dans le monde va changer... comment s'assurer que la vitesse de déplacement est constante quelquesoit le nombre de fois ou render()
est appellé, même en cas de variations ?
Qu'est ce qu'une camera ? Pour un pipeline graphique, ce sont 2 matrices : la matrice view
et la matrice projection
. view
permet de transformer les coordonnées des points du repère de la scène vers le repère de la camera (elle represente la position et l'orientation de la camera dans le repère de la scène) et projection
, comme son nom l'indique, représente la transformation projective de la camera (on 'projette' les points 3d vers une image 2d...).
Jusqu'à présent, on a utilisé un raccourci pour afficher les objets avec draw(objet, model, camera)
, mais la classe Orbiter renvoie les matrices view et projection calculées en fonction des dimensions de la scène et de l'orientation de l'orbiter, et on peut utiliser uniquement ces matrices pour dessiner :
Quelle est la valeur de la matrice view ? ou comment construire cette matrice ? Cette matrice représente un changement de repère : le passage du repère du monde au repère camera. Jusqu'à présent, pour placer et orienter un objet dans la scène on a composé plusieurs rotations et translations, peut on faire la meme chose pour placer et orienter une camera ?
oui, bien sur !! mais il faut se rendre compte que la matrice view est l'inverse de la matrice qui permet de placer et d'orienter un objet quelque part...
rappel : la matrice model, que l'on utilise pour placer et orienter un objet dans la scène, représente le passage d'un repere arbitraire vers le repère de la scène, la matrice view represente le contraire : le passage du repere de la scène vers un autre repère arbitraire. Donc, si on connait la matrice view, on connait l'inverse de la matrice qui permet de placer et d'orienter la camera dans la scène. Si on connait une matrice model, elle correspond à l'inverse de la matrice view d'une camera...
Pour vous aider à visualiser cette relation, on va afficher les axes du repere et une petite pyramide qui represente la region de la scène que pourrait observer une camera placée au meme endroit que l'objet :
il suffit de modifer le code précedent pour afficher ce 'repere' à la place du cube qui tourne...
Il ne reste plus qu'à placer et orienter correctement la camera pour observer le cube au centre de la scene. Par exemple, en reculant (cf translation) et re-orientant (cf rotation) la camera :
remarque : pourquoi le RotationY(180) ?? on veut que la camera regarde devant le cube, pas derriere, il faut la 'retourner'...
Maintenant que la camera est bien positionnée derrière le cube, il ne reste plus qu'à l'utiliser pour dessiner la scène :
Il reste une dernière étape : la matrice projection, il suffit d'utiliser Perspective() définie dans mat.h (comme les autres transformations) :
Les parametres fov
et aspect
controlent le zoom (la 'focale' de la camera) et le rapport largeur / hauteur de l'image, ce qui permet de faire des images en mode portrait ou paysage, et surtout de s'adapter aux dimensions de la fenetre dans laquelle on dessine. Les valeurs classiques sont 45 (pour fov
) et float(window_width()) / float(window_height())
.
Les paramètres znear
et zfar
controlent la région de la scène qui sera affichée, les objets plus proches que znear
ou plus loin que zfar
ne seront pas dessinés. Ce sont des distances exprimées dans le repère camera. Notre scène est un carre de 20x20, zfar
doit etre assez grand pour dessiner toute la scène, au moins 40, par exemple (distance entre 2 coins opposés). znear
doit etre assez petit, mais il y a une limite inférieure (ie la projection divise les coordonnées x et y des points par z, si z est très proche de zero, la division dégénère...).
Et voila !! et comme la position de la camera est relative à la position du cube, lorsque l'on deplace le cube, la camera suit le cube est restant derrière !
le code complet est disponible, cf tuto_transformations_camera.cpp
c'est la même chose, la fonction Lookat()
définie dans mat.h construit la matrice view directement avec 3 informations :
La camera est placée en from
, est orientée pour observer to
et le dernier paramètre, up
indique la verticale dans le repère de la scène. Cette fonction construit les 3 axes du repère camera : l'axe Z est la direction entre from et to (devant), un produit vectoriel entre Z et up permet de construire X (à droite) et il ne reste plus qu'à calculer l'axe Y (en haut) avec un dernier produit vectoriel.
On peut placer une camera très simplement directement dans le repère de la scène : par exemple
Par contre, si l'on souhaite attacher cette camera derrière le cube, comme dans l'exemple précédant, il faut réfléchir un peu plus...
pourquoi ? il faut composer la position de la camera et la position du cube pour obtenir la transformation complète.
bien sur on peut écrire un truc moche qui fonctionne, Lookat()
donne l'inverse de la matrice qui permet de positionner la camera...
mais inverser une matrice n'est pas gratuit, en se rappelant que l'inverse du produit des matrices A et B s'ecrit aussi :
\( (A \times B)^{-1} = B^{-1} \times A^{-1}\)
on peut simplifier tout ca et ne calculer qu'une seule inversion :
le code complet est disponible, cf tuto_transformations_lookat.cpp