gKit2 light
premiers objets, affichage et transformations

premiers objets

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 :

#include "mesh.h"
Mesh objet(GL_TRIANGLES); // ou GL_POINTS, ou GL_LINES
representation d'un objet / maillage.
Definition: mesh.h:112

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( ) :

#include "mesh.h"
Mesh objet( GL_TRIANGLES ); // cet objet est décrit par des triangles
// position des 3 sommets d'un triangle
objet.vertex( /* x */ 0 , /* y */ 0 , /* z */ 0 );
objet.vertex( 1, 0, 0 );
objet.vertex( 0, 1, 0 );
unsigned int vertex(const vec3 &p)
insere un sommet de position p, et ses attributs (s'ils sont definis par color(), texcoord(),...
Definition: mesh.cpp:109

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 :

#include "mesh.h"
#include "color.h"
Mesh objet( GL_TRIANGLES );
// couleur et position de chaque sommet du triangle
objet.color( Red() );
objet.vertex( 0, 0, 0 );
objet.color( Red() );
objet.vertex( 1, 0, 0 );
objet.color( Red() );
objet.vertex( 0, 1, 0 );
Mesh & color(const vec4 &c)
definit la couleur du prochain sommet.
Definition: mesh.cpp:78
Color Red()
utilitaire. renvoie une couleur rouge.
Definition: color.cpp:41

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 :

#include "mesh.h"
#include "color.h"
Mesh objet( GL_TRIANGLES );
// couleur et position de chaque sommet du triangle
objet.color( Red() );
objet.vertex( 0, 0, 0 );
objet.vertex( 1, 0, 0 );
objet.vertex( 0, 1, 0 );

autre exemple, dessiner les axes d'un repère, ce qui sera bien utile pour les parties suivantes :

Mesh grid= Mesh(GL_LINES);
// axes XYZ
grid.color( Red() );
grid.vertex( Point(0, .1, 0) );
grid.vertex( Point(1, .1, 0) );
grid.color( Green() );
grid.vertex( Point(0, .1, 0) );
grid.vertex( Point(0, 1, 0) );
grid.color( Blue() );
grid.vertex( Point(0, .1, 0) );
grid.vertex( Point(0, .1, 1) );
Color Blue()
utilitaire. renvoie une couleur bleue.
Definition: color.cpp:51
Color Green()
utilitaire. renvoie une couleur verte.
Definition: color.cpp:46
representation d'un point 3d.
Definition: vec.h:21

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 :

// genere une grille de n*n cases
Mesh grid= Mesh(GL_LINES);
// grille
grid.color( White() );
// ne trace que les lignes dans le plan XZ, à l'horizontale, centrées à l'origine
for(int x= 0; x < n; x++)
{
float px= float(x) - float(n)/2 + .5f;
grid.vertex( Point(px, 0, - float(n)/2 + .5f) );
grid.vertex( Point(px, 0, float(n)/2 - .5f) );
}
for(int z= 0; z < n; z++)
{
float pz= float(z) - float(n)/2 + .5f;
grid.vertex( Point(- float(n)/2 + .5f, 0, pz) );
grid.vertex( Point(float(n)/2 - .5f, 0, pz) );
}
Color White()
utilitaire. renvoie une couleur blanche.
Definition: color.cpp:36

maillage indexé

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 :

Mesh objet= Mesh(GL_TRIANGLES);
// stocke 3 sommets
// et conserve leurs indices
unsigned int a= objet.vertex( 0, 0, 0 );
unsigned int b= objet.vertex( 1, 0, 0 );
unsigned int c= objet.vertex( 0, 1, 0 );
// ajoute un triangle : indice des 3 sommets
objet.triangle(a, b, c);
Mesh & triangle(const unsigned int a, const unsigned int b, const unsigned int c)
Definition: mesh.cpp:190

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)

dessiner un objet / camera

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 :

#include "mesh.h"
#include "draw.h"
#include "app_camera.h"
class Ex1 : public AppCamera
{
Ex1( ) : AppCamera(1024, 640) {}
int init( )
{
m_objet= Mesh(GL_TRIANGLES);
{ ... }
}
int render( )
{
glClear(GL_COLOR_BUFFER_BIT);
draw(m_objet, /* model */ Identity(), camera());
}
int quit( ) { ... }
Mesh m_objet;
};
classe application.
Definition: app_camera.h:19
virtual int quit()=0
a deriver pour detruire les objets openGL. renvoie -1 pour indiquer une erreur, 0 sinon.
virtual int init()=0
a deriver pour creer les objets openGL. renvoie -1 pour indiquer une erreur, 0 sinon.
const Orbiter & camera() const
renvoie l'orbiter gere par l'application.
Definition: app_camera.h:37
virtual int render()=0
a deriver pour afficher les objets. renvoie 1 pour continuer, 0 pour fermer l'application.
Transform Identity()
construit la transformation identite.
Definition: mat.cpp:187

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 :

draw(m_objet, Identity(), camera());

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() :

charger un objet

il est aussi possible de charger un fichier .obj / wavefront pour initialiser un objet Mesh :

#include "mesh.h"
#include "wavefront.h"
Mesh objet= read_mesh( /* filename */ "..." );
Mesh read_mesh(const char *filename)
charge un fichier wavefront .obj et renvoie un mesh compose de triangles non indexes....
Definition: wavefront.cpp:14

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() :

#include "mesh.h"
#include "wavefront.h"
Mesh objet= read_mesh( /* filename */ "..." );
Point pmin, pmax;
objet.bounds(pmin, pmax);
// pmin et pmax contiennent les points extremes de l'englobant de l'objet.
// regler la region de la scene observee par la camera
camera().lookat(pmin, pmax);
void bounds(Point &pmin, Point &pmax) const
renvoie min et max les coordonnees des extremites des positions des sommets de l'objet (boite engloba...
Definition: mesh.cpp:501

placer un objet dans le monde

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() :

#include "mesh.h"
#include "wavefront.h"
#include "draw.h"
#include "app_camera.h"
class Ex1 : public AppCamera
{
Ex1( ) : AppCamera(1024, 640) {}
int init( )
{
// charger un objet
m_objet= read_mesh( /* filename */ "data/cube.obj" );
// recuperer les dimensions de l'objet
Point pmin, pmax;
m_objet.bounds(pmin, pmax);
// regler la camera pour observer l'objet
camera().lookat(pmin, pmax);
}
int render( )
{
glClear(GL_COLOR_BUFFER_BIT);
Transform model= Identity();
draw(m_objet, model, camera());
}
int quit( ) { ... }
Mesh m_objet;
};
representation d'une transformation, une matrice 4x4, organisee par ligne / row major.
Definition: mat.h:21

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) :

Transform model= Translation(0, 2, 0);
draw(m_objet, model, camera());
Transform Translation(const Vector &v)
renvoie la matrice representant une translation par un vecteur.
Definition: mat.cpp:216

orienter un objet dans le monde

pour orienter / faire tourner un objet, on peut utiliser une rotation, autour de l'axe X, par exemple :

Transform model= RotationX(45); // en degres
draw(m_objet, model, camera());
Transform RotationX(const float a)
renvoie la matrice representation une rotation de angle degree autour de l'axe X.
Definition: mat.cpp:230

le résultat est sans surprise. mais comment placer et orienter un objet dans le monde ?

transformations : 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 \)...

exemple :
Transform t= Translation(0, 2, 0);
Transform r= RotationX(45); // en degres
// solution 1 : tourner le cube puis le deplacer
Transform model= t * r;
draw(m_objet, model, camera());
// + le cube a l'origine
draw(m_objet, Identity(), camera());

et l'autre solution :

Transform t= Translation(0, 2, 0);
Transform r= RotationX(45); // en degres
// solution 2 : deplacer puis tourner le cube
Transform model= r * t;
draw(m_objet, model, camera());
// + le cube a l'origine
draw(m_objet, Identity(), camera());

pourquoi ?

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

placer un objet par rapport à un autre

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.

Transform m1= Translation(1, 0, 0); // place l'objet 1
Transform m2= Translation(0, 2, 0) * RotationX(45); // place et oriente l'objet 2
// dessine l'objet 1
draw(m_objet, m1, camera());
// dessine l'objet 2 par rapport a l'objet 1
// quelle est la bonne composition ?
// solution 1
draw(m_objet, m1 * m2, camera());
// solution 2
draw(m_objet, m2 * m1, camera());

contrôler le déplacement d'un objet au clavier

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 :

if(key_state(SDLK_UP))
// avancer
if(key_state(SDLK_DOWN))
// reculer
int key_state(const SDL_Keycode key)
renvoie l'etat d'une touche du clavier. cf la doc SDL2 pour les codes.
Definition: window.cpp:42

comment representer un deplacement en avant / en arriere ? tout simplement par une translation le long du vecteur Z (0,0,1)

if(key_state(SDLK_UP))
// avancer
t= Translation(0, 0, 1);
if(key_state(SDLK_DOWN))
// reculer
t= Translation(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 :

Transform position= Identity();
render():
if(key_state(SDLK_UP))
// avancer
t= Translation(0, 0, 1);
if(key_state(SDLK_DOWN))
// reculer
t= Translation(0, 0, -1))
position= position * t;
// dessiner l'objet a sa nouvelle position
draw(objet, position, camera());

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) :

Transform position= Identity();
render():
// modifie la position de l'objet en fonction des fleches de direction
if(key_state(SDLK_UP))
m= Translation(0, 0, 0.25); // en avant
if(key_state(SDLK_DOWN))
m= Translation(0, 0, -0.25); // en arriere
if(key_state(SDLK_LEFT))
m= RotationY(4); // tourne vers la droite
if(key_state(SDLK_RIGHT))
m= RotationY(-4); // tourne vers la gauche
// composition
position= position * m;
// dessiner l'objet a sa nouvelle position
draw(objet, position, camera());
Transform RotationY(const float a)
renvoie la matrice representation une rotation de a degree autour de l'axe Y.
Definition: mat.cpp:242

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...

question bonus :

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 ?

transformations : camera

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 :

Mesh objet= { ... };
Transform view= camera().view();
Transform projection= camera().projection();
draw(objet, model, view, projection);
// meme resultat que draw(objet, model, camera());
Transform projection(const int width, const int height, const float fov)
fixe la projection reglee pour une image d'aspect width / height, et une demi ouverture de fov degres...
Definition: orbiter.cpp:47
Transform view() const
renvoie la transformation vue.
Definition: orbiter.cpp:40

placer et orienter une camera dans la scène

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 :

Mesh make_grid_camera( )
{
Mesh camera= Mesh(GL_LINES);
// pyramide de vision de la camera
camera.color(Yellow());
camera.vertex(0,0,0);
camera.vertex(-0.5, -0.5, -1);
camera.vertex(0,0,0);
camera.vertex(-0.5, 0.5, -1);
camera.vertex(0,0,0);
camera.vertex(0.5, 0.5, -1);
camera.vertex(0,0,0);
camera.vertex(0.5, -0.5, -1);
camera.vertex(-0.5, -0.5, -1);
camera.vertex(-0.5, 0.5, -1);
camera.vertex(-0.5, 0.5, -1);
camera.vertex(0.5, 0.5, -1);
camera.vertex(0.5, 0.5, -1);
camera.vertex(0.5, -0.5, -1);
camera.vertex(0.5, -0.5, -1);
camera.vertex(-0.5, -0.5, -1);
// axes XYZ
{ ... }
return camera;
}
Color Yellow()
utilitaire. renvoie une couleur jaune.
Definition: color.cpp:56

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 :

Transform t= Translation(0, 0, 4);
Transform m= r * t;
draw(objet, /* model */ position, view, projection); // cube au centre de la scene
draw(objet, /* model */ position * m, view, projection); // camera en arriere du cube

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 :

Transform t= Translation(0, 0, 4);
Transform m= r * t;
Transform v= position * m; // place la camera par rapport au cube
Transform view= Inverse(v); // matrice view de la camera
...
Transform Inverse(const Transform &m)
renvoie l'inverse de la matrice.
Definition: mat.cpp:197

Il reste une dernière étape : la matrice projection, il suffit d'utiliser Perspective() définie dans mat.h (comme les autres transformations) :

// mat.h
Transform Perspective( const float fov, const float aspect, const float znear, const float zfar );
Transform Perspective(const float fov, const float aspect, const float znear, const float zfar)
renvoie la matrice representant une transformation projection perspective.
Definition: mat.cpp:329

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...).

Transform t= Translation(0, 0, 4);
Transform m= r * t;
Transform v= position * m; // place la camera par rapport au cube
Transform view= Inverse(v); // matrice view de la camera
Transform projection(45, float(window_width()) / float(window_height()), 0.1, 40);
draw(objet, position, view, projection);
int window_height()
renvoie la hauteur de la fenetre de l'application.
Definition: window.cpp:29
int window_width()
renvoie la largeur de la fenetre de l'application.
Definition: window.cpp:25

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

et avec Lookat() ??

c'est la même chose, la fonction Lookat() définie dans mat.h construit la matrice view directement avec 3 informations :

// mat.h
Transform Lookat( const Point& from, const Point& to, const Vector& up );
Transform Lookat(const Point &from, const Point &to, const Vector &up)
renvoie la matrice representant le placement et l'orientation d'une camera pour observer le point to.
Definition: mat.cpp:369
representation d'un vecteur 3d.
Definition: vec.h:59

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

Transform view= Lookat( Point(0, 1, -4), Point(0,0,0), Vector(0,1,0) );

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...

Transform m= Inverse( Lookat( Point(0, 1, -4), Point(0,0,0), Vector(0,1,0)) );
// puis memes calculs que dans le code precedant
Transform v= position * m; // place la camera par rapport au cube
Transform view= Inverse(v); // matrice view de la camera
Transform projection(45, float(window_width()) / float(window_height()), 0.1, 40);
draw(objet, position, view, projection);

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 :

// Transform m= { ... }; // place la camera
// Transform v= position * m; // place la camera par rapport au cube
// Transform view= Inverse(v); // matrice view de la camera
// Transform view= Inverse(m) * Inverse(position); // meme resultat
// et Inverse(m) est le resultat de Lookat()...
Transform mInv= Lookat( Point(0, 1, -4), Point(0,0,0), Vector(0,1,0));
Transform view= mInv * Inverse(position);
Transform projection(45, float(window_width()) / float(window_height()), 0.1, 40);
draw(objet, position, view, projection);

le code complet est disponible, cf tuto_transformations_lookat.cpp