Aller au contenu

Programmation Graphique : TP séance 05

Rendu du TP sur Ametice :
Suivez les instructions pour le rendu du TP en mettant le bon numéro de séance dans le nom du répertoire.
N'oubliez pas de rendre votre travail à la fin de la séance comme indiqué, même si vous n'avez pas fini la planche de TP, puis de rendre la version finale (sans changer le nom du fichier) avant le début de la séance suivante.

Exercice 1. Roue dentée avec lumière diffuse

On se propose de reprendre le dessin de la roue dentée du TP1, cette fois-ci en OpenGL core. Toutes les facettes auront le même couleur, et seront rendues visibles par une lumière ambiante et diffuse. Le trou aura un lissage de Phong.

Dans votre nouveau répertoire pgra-TP05-NOM1-NOM2, récupérer les fichiers suivants : le Makefile du TP 04 (qui compile avec stb_image), les fichiers de GLAD glad.c, glad.h, khrplatform.h, le module de calcul matriciel vmath.h, le petit complément vmath-et.h, la librairie tout-en-un stb_image.h et le petit module C associé stb_image.c.

Enfin, récupérer l'exemple de départ fw51-light.cpp.

Vérifiez que vous pouvez compiler et exécuter l'exemple (au besoin, voir 3.1. Installation et compilation dans le CM 01).

On peut afficher les shaders par défaut avec l'option -ps (print shader), et modifier les shaders avec les options -vs vs_file et -fs fs_file.

La touche U recharge les shaders sans quitter le programme. La touche a déclenche une animation, la touche p change le type de projection, h affiche l'aide pour les réglages de la projection.

Les éléments utiles en OpenGL core pour réaliser ce TP sont expliqués dans le CM 05, en particulier dans la section 9.3. Exemple du Kite, pour le calcul des normales.

Roue 1

1.1. Paramètres d'une roue

En vous inspirant de la classe Kite présente dans l'exemple, déclarer une classe RoueNor avec les données membres suivantes (identiques au TP 01), en rajoutant le préfixe m_ :

  • le nombre de dents nb_dents (dans l'exemple il y en a 10) ;
  • le rayon intérieur r_trou (la distance \(OA\) dans la figure ci-dessous) ;
  • le rayon de la roue r_roue (le rayon du grand cercle jaune qui passe entre \(C\) et \(D\)) ;
  • la hauteur des dents h_dent (la différence de rayons entre le cercle rose qui passe par \(D\) et \(E\), et le cercle rose qui passe par \(B\), \(C\), \(F\)) ;
  • la couleur (coul_r, coul_v, coul_b) ;
  • l'épaisseur ep_roue (en \(z\)).

1.2. Mémorisation des faces

Le but est de tout mémoriser dans un seul VBO, avec pour chaque sommet : sa position, sa couleur et la normale au triangle dont il fait partie.

Dans le VBO il y aura de quoi dessiner les GL_TRIANGLE_STRIP suivants :

RoueNor 1

  • la face de devant,
  • la face de derrière,
  • le pourtour du trou avec lissage de Phong,
  • le pourtour des dents (1 triangle strip par plan à cause des normales).

Le dessin du côté d'une dent avec un triangle strip comme indiqué sur la figure nécessite l'introduction des sommets I = (A+H)/2 et J = (H+G)/2 (cliquer sur la figure pour l'agrandir).

Pour mémoriser la face de devant, on mémorise dans le VBO les sommets dans l'ordre :

  • pour la première dent : A,B,I,C,H,D,J,E ;
  • puis pour chaque dent suivante, les sommets dans le même ordre avec une rotation (des sommets de la première dent) ;
  • et à la fin on rajoute encore A,B pour "fermer" le triangle strip.

On ne mémorise donc pas les sommets G,F d'une dent car ils correspondent aux sommets A,B de la dent suivante. Tous ces sommets auront la même couleur et la même normale (ils sont coplanaires sur le plan z = +ep_roue/2). Astuce : comme la face est dans le plan \(Oxy\), on peut directement prendre comme normale \((0,0,1)\).

L'important pour que les triangles soient bien orientés (la face avant vers l'extérieur et la face arrière vers l'intérieur de la roue) est que le premier triangle dans le triangle strip soit anti-clockwise, c'est-à-dire dans l'ordre trigonométrique ; c'est bien le cas ici. L'orientation de la face permet de tester gl_FrontFacing dans les fragment shader, et de correctement calculer la normale au triangle pour l'éclairage.

Pour la face à l'arrière de la roue, attention il faudra que les triangles (et les normales) soient eux aussi bien orientés, de manière à ce que si l'on retourne la roue (avec une rotation de la roue de 180° autour de l'axe des \(y\)), cette face soit correctement éclairée. Le plus simple est de faire une symétrie centrale, c'est-à-dire de reprendre la boucle complète de mémorisation de la première face en inversant les signes des coordonnées des positions et des normales.

Avant de vous lancer dans le code, lisez la section suivante !

1.3. Un peu d'aide

Pour faciliter l'écriture voici quelques suggestions ; dans la classe Roue déclarer :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    struct Polar  { double a, r; };
    struct Posit2 { double x, y; };

    std::vector<Polar> m_sommets_pol;
    double m_angle_dent;

    void init_sommets_pol() 
    {
        m_angle_dent = 2*M_PI / m_nb_dents;
        float rI = ...;  // voir ci-dessous

        m_sommets_pol = {
            {                0, m_r_trou              },  // A 0
            {                0, m_r_roue - m_h_dent/2 },  // B 1
            {   m_angle_dent/4, m_r_roue - m_h_dent/2 },  // C 2
            ...
            { 3*m_angle_dent/4, rI                    }   // J 9
        };
    }

    Posit2 get_sommet_pos (int num_som, int num_dent)
    {
        Polar& p = m_sommets_pol[num_som];
        return Posit2 { p.r * cos(p.a + num_dent*m_angle_dent),
                        p.r * sin(p.a + num_dent*m_angle_dent) };
    }

Avec get_sommet_pos vous pourrez directement obtenir les coordonnées d'un sommet à partir de son numéro num_som et du numéro de sa dent num_dent.

Le calcul du rayon de I et de J est obtenu avec un peu de trigonométrie, complétez les lignes 10,11 par :

10
11
        double rI = m_r_trou * (cos(0) + cos(m_angle_dent/2))
                             /    (2.0 * cos(m_angle_dent/4));

1.4. Mémorisation des pourtours

Roue 6

Le pourtour du trou sera rendu avec un lissage de Phong ; cela permet de mémoriser tous les sommets en un seul triangle strip. Pour rajouter les sommets, couleur et normales du pourtour du trou dans le VBO, il suffit de faire comme dans la section précédente :

  • pour la première dent, on insère : A',A,H',H ;
  • puis pour chaque dent suivante, on insère les sommets dans le même ordre avec une rotation (des sommets de la première dent) ;
  • et à la fin on rajoute encore A',A pour "fermer" le triangle strip.

Les normales doivent "pointer" vers l'axe Oz, il suffit donc pour un sommet \((x,y,z)\) de prendre comme normale \((-x,-y,0)\).

Enfin pour le pourtour des dents, il va falloir insérer 4 triangle strip par dent, chacun avec la normale correspondant à son plan : BB'CC', CC'DD', DD'EE', EE'FF'. En effet le pourtour des dents ne doit pas être lissé.

1.5. Affichage

Pour afficher la roue dans la méthode draw, il faut appeler glDrawArrays (GL_TRIANGLE_STRIP, start_index, vertex_number) pour chaque triangle strip, ce qui implique de bien calculer les paramètres start_index et vertex_number en fonction de nb_dents.

Rajouter un paramètre flag_fill à la méthode draw de manière à pouvoir choisir entre des faces remplies et un dessin "fil de fer" (touche L) grâce à la fonction glPolygonMode (voir TP 02).

Dans l'application, incrémenter un angle m_alpha avec la touche espace, et tourner la scène dans displayGL avec une rotation autour de l'axe des \(z\).

Exercice 2. Sphère avec lumière spéculaire

On se propose de générer récursivement une sphère triangulée, avec ou sans lissage de Phong, avec lumière ambiante et diffuse, avec ou sans lumière spéculaire.

Dans le même répertoire que pour l'exercice 1, récupérer l'exemple de départ fw53-specular.cpp.

2.1. Paramètres d'une sphère

Sphère 2

En vous inspirant de la classe Kite présente dans l'exemple, déclarer une classe Sphere avec les données membres suivantes, en rajoutant le préfixe m_ :

  • le rayon rayon de la sphère, par défaut 0.5 ;
  • le nombre d'étapes nb_etapes du découpage récursif ;
  • la couleur (coul_r, coul_v, coul_b) ;
  • le booléen flag_lissage.

2.2. Découpage récursif

On partage au départ la sphère en 8 grands triangles (sphériques), comme dans la figure, en veillant à les orienter dans le sens trigonométrique (le devant vers l'extérieur de la sphère). Sur chacun des 8 triangles on appellera la fonction de découpage récursive decouper_triangle :

La fonction decouper_triangle prend en entrée 3 sommets \(A\), \(B\), \(C\), un niveau de récursion n (0 au premier appel), et par référence le vector de GLfloat qu'on passera à la fin au VBO.

Sphère 4

La fonction calcule les points \(D\), \(E\), \(F\) comme sur la figure, en utilisant le fait que

\[\vec{OD} = R\,\frac{\vec{OA}+\vec{OB}}{||\vec{OA}+\vec{OB}||}\]

\(R\) = m_rayon ; on fait de même pour \(E\) et \(F\).

La fonction effectue ensuite 4 appels récursifs pour le niveau n+1, sur les sommets dans l'ordre : \(A,D,F\) ; \(D,B,E\) ; \(E,C,F\) ; \(D,E,F\).

Au début de la fonction on place le cas terminal : si n vaut nb_etapes alors on mémorise pour les 3 sommets ; leur position, leur couleur et leur normale, tout ceci dans le vector de GLfloat, puis on revient. (Remarque : il ne faut surtout pas mémoriser les sommets dans les étapes intermédiaires).

On donnera la même couleur à tous les sommets. Quant aux normales :

  • si le lissage est désactivé (m_flag_lissage est false), on mémorisera pour chaque sommet d'un triangle, la normale calculée du triangle (produit vectoriel de deux vecteurs) ;

  • si le lissage est activé, on donne en chaque sommet \(P\) sa normale à la surface de la sphère, qui est par définition \(\vec{OP}\) (à un facteur d'échelle près, qui n'a pas d'importance puisque le fragment shader normalisera les coordonnées).

2.3. Affichage

Pour l'affichage des triangles dans la méthode draw, appeler glDrawArrays avec GL_TRIANGLES, en lui fournissant le nombre de sommets générés.

Remarque : à l'étape 0 il y a 8 triangles, à l'étape 1 il y a \(8*4^1\) triangles, etc, donc à l'étape \(k\) il y a \(8*4^k\) triangles.

Rajouter un paramètre flag_fill à la méthode draw de manière à pouvoir choisir entre des faces remplies et un dessin "fil de fer" (touche L) grâce à la fonction glPolygonMode (voir TP 02).

Dans MyApp, instancier 2 sphères, l'une lissée et l'autre non, de manière à passer de l'une à l'autre à l'aide de la touche o comme dans l'exemple de départ.

Pour pouvoir activer ou désactiver la lumière spéculaire, gérer un flag avec la touche s, passer le flag en uniform au fragment shader par défaut, et modifier la somme des composantes de lumières en conséquence.

Tests supplémentaires : dans le fragment shader, utiliser uniform vec4 mousePos pour déplacer la source de lumière (voir 7.3.2. Position de la souris).