TD8 - Courbes et surfaces paramétriques

L'objectif de ce TP est de construire des surfaces définies à partir de courbes paramétriques. Afin de démarrer dans de bonnes conditions, téléchargez l'archive mds3d_td8.zip.

1. Grille régulière

Nous allons considérer des surfaces paramétriques $S(u,v)$ définies par deux paramètres $u$ et $v$ dans l'intervalle $[0,1]^2$. Cela défini un carreau que nous allons tracer à l'aide d'un maillage représentant, dans l'espace paramétrique, une grille régulière avec M et N sommets le long des axes u et v respectivement. Le sommet d'indice $(i,j)$ aura donc comme coordonnées : $p_{i,j}=S(u_i,v_j)$. Remarquez que la topologie du maillage ainsi que les coordonnées paramétriques $(u_i,v_j)$ des sommets sont indépendants de la surface $S$. Pour tracer une surface $S$ donnée, nous allons donc envoyer à la carte graphique le maillage d'une grille uniforme, et l'évaluation de la surface $S$ sera réalisée à la volée par le vertex shader. Cette approche a de multiples avantages : un seul maillage à stocker en mémoire, animation/déformation de la surface en temps réel, etc.

La première étape consiste donc à créer le maillage d'une grille uniforme unitaire de résolution $N \times M$. Pour ce TP, vous pouvez simplement charger le maillage grid.obj. Pour contrôler la résolution de la grille, vous pourrez, chez vous, ajouter à la classe Mesh une méthode void createMesh(int M, int N); créant une telle grille. Par défaut, vous pouvez prendre $p_{i,j}=(u_i,v_j,0)$ pour les coordonnées 3D des sommets. Afin de mieux visualiser la surface, la texture rainbow.png est appliquée à votre objet. Compilez et testez le code fourni avant de passer à l'étape suivante.

2. Cylindre

Afin de nous échauffer, nous allons tracer un cylindre défini par deux points $A$ et $B$ (les extrémités), et un rayon $r$. Pour cela, écrire sur papier la fonction $S(u,v)$. Il y a de nombreuses façons de formuler cette équation, mais afin de nous préparer pour la suite, pensez en terme d'extrusion d'un cercle le long d'un segment. Puis, implémentez dans le vertex shader une fonction vec3 cylinder(vec2 uv, vec3 A, vec3 B, float r); et testez. Vous pouvez au choix, fixer $A$, $B$, et $r$ dans le code sur shader ou les passer par des variables uniformes.
Conseil : procédez par étape, commencez par considérer un cylindre aligné sur un axe, par exemple sur l'axe X, puis passez à un axe quelconque en calculant un repère unitaire dont le premier axe est aligné avec la direction du cylindre.

3. Courbes de Bézier

Nous allons maintenant tracer une courbe de Bézier $B(u)$ définie par quatre points $B_0, B_1, B_2, B_3$ avec un peu d'épaisseur. La courbe sera tracée sous la forme d'une surface paramétrique obtenue par extrusion d'un cercle le long de la courbe. Pensez par exemple à un câble d'un certain diamètre dont la trajectoire est définie par la courbe de Bézier. Vous pouvez aussi voir ça comme la déformation du cylindre précédent pour qu'il suive la courbe.

Pour une courbe $B(u)$ 3D arbitraire, cet exercice est relativement difficile (voir plus loin). Aussi, nous allons commencer par le cas d'une simple courbe planaire (par exemple dans le plan X-Z), et sans repliement (par exemple à un X donné correspond un seul Z). Vous pouvez par exemple prendre le jeu de points : $(-1,0,2), (-0.3,0,4), (0.3,0,1), (1,0,-0.5)$. Pour cet exercice, nous allons simplement extruder un cercle défini dans le plan Y-Z le long de cette courbe. Pour cela, commencez par écrire dans le vertex shader une fonction vec3 bezier(float u, vec3 B[4]); calculant la position $B(u)$ et la fonction vec3 cylBezierYZ(vec2 uv, vec3 B[4], float r); évaluant la surface paramétrique définie précédemment.

Nous allons maintenant généraliser la fonction précédente de manière à ce que le plan support du cercle suive la trajectoire définie par la courbe. En d'autres termes, à toute valeur du paramètre $u$ correspond un cercle défini dans le plan passant par $B(u)$ et de normale $T(u)$ où $T$ est le vecteur tangent à la courbe. Nous devons en plus contrôler la rotation du cercle autour de ce vecteur tangent. Cette rotation nous est donnée par les vecteurs normales et binormales de la courbe définissant ainsi le repère de Frenet. Pour une courbe planaire, par exemple dans le plan X-T, le vecteur binormale est constant, i.e. $(0,1,0)$, et la connaissance du vecteur tangent est donc suffisante. Pour cela, étendez la fonction bezier de manière à retourner le vecteur tangent: vec3 bezier(float u, vec3 B[4], out vec3 T); et mettez à jour la fonction cylBezierYZ réalisant l'alignement avec le vecteur tangent en supposant un vecteur binormal constant $(0,1,0)$. Testez avec la courbe planaire précédente et éventuellement modifiez les points de contrôles pour créer une boucle.

Passez à une courbe 3D arbitraire en calculant le repère de Frenet complet dans une matrice 3x3 : vec3 bezier(float u, vec3 B[4], out mat3 F);. Implémentez une fonction vec3 cylBezier(vec2 uv, vec3 B[4], float r); réalisant l'alignement complet des cercles avec le repère de Frenet. Testez avec : $(-0.5,-1,-1), (1.5, 1,-0.3), (-1.5, 1, 0.3), (0.5,-1,1)$. Vous devez obtenir une sorte d'hélice.
Puis avec: $(-1,-0.5,-1), (-1, 1,-0.3), (1, -1, 0.3), (1,0.5,1)$. Que ce passe-t-il ? Revenez à l'exemple précédent.

Nous allons maintenant introduire une torsion du câble en appliquant progressivement (linéairement) une rotation du repère support au cercle le long de la courbe. Contrôlez la rotation maximale (i.e., en $u=1$) via une variable uniforme elle même contrôlée au clavier. Vous pouvez aussi tester avec une section carré en appliquant une simple transformation du cercle (par exemple en normalisant par la norme 1 ou infinie).

4. Calcul des normales (bonus)

Afin d'obtenir un éclairage correct de nos surfaces, nous devons calculez la normale en chaque sommets, par exemple en modifiant la fonction cylBezier pour retourner également le vecteur normale de la surface : vec3 cylBezier(vec2 uv, vec3 B[4], float r, out vec3 n);