Retourner au sommaire des cours
Ce tutorial est une suite du tutorial précédent. Nous allons continuer à utiliser les matrices pour mieux les comprendre au travers des transformations. Nous introduirons aussi le tutorial suivant qui abordera les indices. La notions de vertices (et leur utilisation) sera elle aussi abordée puisque nous allons passer de l'affichage d'un simple triangle à un joli cube coloré.
Notre programme
La première difficulté à appréhender nous vient de l'affichage du cube. Pour le débutant, l'affichage d'un triangle était déjà un exercice relativement compliqué. Avec l'affichage d'un cube l'exercice se corse puisqu'il faut pas moins de 12 triangles pour former un cube ! (un cube = 6 faces, une face = 2 triangles). Nous commencerons donc à réaliser l'affichage d'une face, puis de deux, et enfin le cube en entier. Nous appliquerons une transformation propre au cube pour terminer.
Regardez l'image ci-dessous, elle explicite les douze triangles qui forment le cube.

Les traits noirs représentent les faces, les traits gris les triangles. C'est ce résultat que nous devons reproduire (en plus coloré). Reprennons le code du précédent tutorial. La première étape consiste à modifier notre triangle afin de le rendre rectangle (triangle à angle droit). Les trois vertices définis ainsi :
vertices = new VertexPositionColor[3];
vertices[0].Position = new Vector3(0.5f, -0.5f, 0f);
vertices[0].Color = Color.Yellow;
vertices[1].Position = new Vector3(0, 0.5f, 0f);
vertices[1].Color = Color.Green;
vertices[2].Position = new Vector3(-0.5f, -0.5f, 0f);
vertices[2].Color = Color.Red;
deviennent :
vertices = new VertexPositionColor[3];
vertices[0].Position = new Vector3(-10f, -10f, 10f);
vertices[0].Color = Color.Green;
vertices[1].Position = new Vector3(-10f, 10f, 10f);
vertices[1].Color = Color.Red;
vertices[2].Position = new Vector3(10f, 10f, 10f);
vertices[2].Color = Color.Yellow;
On remarque que les trois points du triangle on la même profondeur (10f) ceci, afin de rendre la définition des position des vertices plus simple à imager. L'image ci-dessous résume le travail que nous venons de faire (les numéros en rouge correspondant à l'index des vertices dans le tableau vertices) :

On notera que nous avons défini les vertices dans l'ordre des aiguilles d'une montre (voir cours précédent sur le backface culling)... En outre nous sommes en train de construire un cube. Il n'est donc plus nécessaire d'annuler le backface culling les faces des triangles non visibles étant situées à l'intérieur du cube. Supprimer pour cela l'instruction :
this.graphics.GraphicsDevice.RenderState.CullMode = CullMode.None;
De la méthode répondant à l'événement de reinisitalisation du device graphique (graphics_DeviceReset).
Annulez les transformations que nous appliquions au triangle afin de le faire tourner en remplaçant dans la méthode Update l'instruction :
this.effect.Parameters["World"].SetValue(Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds));
par :
this.effect.Parameters["World"].SetValue(Matrix.Identity);
En outre, les dimensions de notre triangle sont bien plus importantes que dans le tutorial précédent. Il faut donc replacer la caméra en conséquence. Modifiez les instructions d'affectation aux matrices View et Projection de la méthode Load ainsi :
this.effect.Parameters["View"].SetValue(Matrix.CreateLookAt(new Vector3(0, 10, -50), Vector3.Zero, Vector3.Up));
this.effect.Parameters["Projection"].SetValue(Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, this.GraphicsDevice.Viewport.AspectRatio, 0.1f, 100f));
Nous nous placons pile en face du triangle que nous venons de créer à 50 unité de distance et avec une légère vue de haut de 20 unités.Si vous lancez le programme à ce stade vous obtenez :

Le premier triangle du cube est affiché. En fait aucun mérite à avoir, cela fait déjà quatre tutoriaux que nous savons faire cela. Ajoutons une difficulté en créant un nouveau triangle collé au premier de manière a former une face carrée ! Modifiez la création du tableau de vertices dans Initialize de cette manière :
vertices = new VertexPositionColor[ 6 ];
//triangle 1, face devant
vertices[0].Position = new Vector3(-10f, -10f, 10f);
vertices[0].Color = Color.Green;
vertices[1].Position = new Vector3(-10f, 10f, 10f);
vertices[1].Color = Color.Red;
vertices[2].Position = new Vector3(10f, 10f, 10f);
vertices[2].Color = Color.Yellow;
//triangle 2, face devant
vertices[3].Position = new Vector3(-10f, -10f, 10f);
vertices[3].Color = Color.Green;
vertices[4].Position = new Vector3(10f, 10f, 10f);
vertices[4].Color = Color.Red;
vertices[5].Position = new Vector3(10f, -10f, 10f);
vertices[5].Color = Color.Yellow;
Nous avons maintenant 6 vertices. Deux triangles isocèles à angle droit et donc une face. Schematiquement nous avons réalisé ceci :

Nous n'affichons donc plus une seule primitive (triangle) mais deux. Il faut donc aussi modifier l'instruction de dessin dans la méthode Draw :
this.graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, 2);
Le "2" à la fin de l'appel insique que nous affichons maintenant deux primitives (deux triangles). Etant donné que nous sommes en TriangleList il faut donc 6 vertices, ce que nous donnons à l'aide du tableau vertices. Pas de surprise, à l'execution nous obtenons :

Accelerons le rythme, la prochaine étape sera plus importante : une nouvelle face va être ajoutée, soit deux triangles en plus et donc 12 vertices à définir. Modifiez de nouveau la création du tableau de vertices pour passer à 12 vertices :
vertices = new VertexPositionColor[12];
//triangle 1, face devant
vertices[0].Position = new Vector3(-10f, -10f, 10f);
vertices[0].Color = Color.Green;
vertices[1].Position = new Vector3(-10f, 10f, 10f);
vertices[1].Color = Color.Red;
vertices[2].Position = new Vector3(10f, 10f, 10f);
vertices[2].Color = Color.Yellow;
//triangle 2, face devant
vertices[3].Position = new Vector3(-10f, -10f, 10f);
vertices[3].Color = Color.Green;vertices[4].Position = new Vector3(10f, 10f, 10f);
vertices[4].Color = Color.Red;
vertices[5].Position = new Vector3(10f, -10f, 10f);
vertices[5].Color = Color.Yellow;
//triangle 3, face droite
vertices[ 6 ].Position = new Vector3(10f, -10f, 10f);
vertices[ 6 ].Color = Color.Green;
vertices[7].Position = new Vector3(10f, 10f, 10f);
vertices[7].Color = Color.Red;
vertices[ 8 ].Position = new Vector3(10f, 10f, -10f);
vertices[ 8 ].Color = Color.Yellow;
//triangle 4, face droite
vertices[9].Position = new Vector3(10f, -10f, 10f);
vertices[9].Color = Color.Green;
vertices[10].Position = new Vector3(10f, 10f, -10f);
vertices[10].Color = Color.Red;
vertices[11].Position = new Vector3(10f, -10f, -10f);
vertices[11].Color = Color.Yellow;
Les deux triangles ajoutés correspondent dans l'image à :

Modifiez l'instruction de dessin ainsi :
this.graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, 4);
Nous affichons maintenant 4 triangles. Pourtant à l'affichage nous obtenons le même résultat que précédemment, que s'est il passé ? Ce problème et lié à la position de la caméra. Nous sommes pile en face des deux triangles que nous avons créé au début. Les deux nouveaux triangles ne sont donc pas visibles. Modifiez la position de la caméra (dans la méthode LoadContent) de manière a la placer légèrement plus haut et à droite :
this.effect.Parameters["View"].SetValue(Matrix.CreateLookAt(new Vector3(20, 30, 50), Vector3.Zero, Vector3.Up));
Cette fois ci à l'exécution nous obtenons :

On distingue bien les deux faces. A ce stade l'ajout de triangles pour créer un cube doit être assimilé, nous allons directement ajouter les 4 autres faces restantes (soit 8 triangles). Le tableau de vertices est désormais créé de cette manière :
//triangle 1, face devant
vertices[0].Position = new Vector3(-10f, -10f, 10f);
vertices[0].Color = Color.Green;
vertices[1].Position = new Vector3(-10f, 10f, 10f);
vertices[1].Color = Color.Red;
vertices[2].Position = new Vector3(10f, 10f, 10f);
vertices[2].Color = Color.Yellow;
//triangle 2, face devant
vertices[3].Position = new Vector3(-10f, -10f, 10f);
vertices[3].Color = Color.Green;
vertices[4].Position = new Vector3(10f, 10f, 10f);
vertices[4].Color = Color.Red;
vertices[5].Position = new Vector3(10f, -10f, 10f);
vertices[5].Color = Color.Yellow;
//triangle 3, face droite
vertices[ 6 ].Position = new Vector3(10f, -10f, 10f);
vertices[ 6 ].Color = Color.Green;
vertices[7].Position = new Vector3(10f, 10f, 10f);
vertices[7].Color = Color.Red;
vertices[ 8 ].Position = new Vector3(10f, 10f, -10f);
vertices[ 8 ].Color = Color.Yellow;
//triangle 4, face droite
vertices[9].Position = new Vector3(10f, -10f, 10f);
vertices[9].Color = Color.Green;
vertices[10].Position = new Vector3(10f, 10f, -10f);
vertices[10].Color = Color.Red;
vertices[11].Position = new Vector3(10f, -10f, -10f);
vertices[11].Color = Color.Yellow;
//triangle 5, face arrriere
vertices[12].Position = new Vector3(10f, -10f, -10f);
vertices[12].Color = Color.Green;
vertices[13].Position = new Vector3(10f, 10f, -10f);
vertices[13].Color = Color.Red;
vertices[14].Position = new Vector3(-10f, 10f, -10f);
vertices[14].Color = Color.Yellow;
//triangle 6, face arrirere
vertices[15].Position = new Vector3(10f, -10f, -10f);
vertices[15].Color = Color.Green;
vertices[16].Position = new Vector3(-10f, 10f, -10f);
vertices[16].Color = Color.Red;
vertices[17].Position = new Vector3(-10f, -10f, -10f);
vertices[17].Color = Color.Yellow;
//triangle 7, face gauche
vertices[18].Position = new Vector3(-10f, -10f, -10f);
vertices[18].Color = Color.Green;
vertices[19].Position = new Vector3(-10f, 10f, -10f);
vertices[19].Color = Color.Red;
vertices[20].Position = new Vector3(-10f, 10f, 10f);
vertices[20].Color = Color.Yellow;
//triangle 8, face gauche
vertices[21].Position = new Vector3(-10f, -10f, -10f);
vertices[21].Color = Color.Green;
vertices[22].Position = new Vector3(-10f, 10f, 10f);
vertices[22].Color = Color.Red;
vertices[23].Position = new Vector3(-10f, -10f, 10f);
vertices[23].Color = Color.Yellow;
//triangle 9, face bas
vertices[24].Position = new Vector3(-10f, 10f, 10f);
vertices[24].Color = Color.Green;
vertices[25].Position = new Vector3(-10f, 10f, -10f);
vertices[25].Color = Color.Red;
vertices[26].Position = new Vector3(10f, 10f, -10f);
vertices[26].Color = Color.Yellow;
//triangle 10, face bas
vertices[27].Position = new Vector3(-10f, 10f, 10f);
vertices[27].Color = Color.Green;
vertices[28].Position = new Vector3(10f, 10f, -10f);
vertices[28].Color = Color.Red;
vertices[29].Position = new Vector3(10f, 10f, 10f);
vertices[29].Color = Color.Yellow;
//triangle 11, face haut
vertices[30].Position = new Vector3(-10f, -10f, -10f);
vertices[30].Color = Color.Green;
vertices[31].Position = new Vector3(-10f, -10f, 10f);
vertices[31].Color = Color.Red;
vertices[32].Position = new Vector3(10f, -10f, 10f);
vertices[32].Color = Color.Yellow;
//triangle 12, face haut
vertices[33].Position = new Vector3(-10f, -10f, -10f);
vertices[33].Color = Color.Green;
vertices[34].Position = new Vector3(10f, -10f, 10f);
vertices[34].Color = Color.Red;
vertices[35].Position = new Vector3(10f, -10f, -10f);
vertices[35].Color = Color.Yellow;
L'instruction DrawUserPrimitive se présente ainsi :
this.graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, 12);
A l'exécution nous obtenons un splendide cube :

Pour profiter des formes ce cube nous allons le soumettre à toutes les transformations existantes. Il va être déplacé, tourné et redimentionné. Là où nous allons faire fort, c'est que nous le déplacerons et le redimentionnerons à l'aide du clavier !
Gestion des entrées clavier
Nous ferons un cours complet sur la gestion des entrées clavier, gamepad ou autre plus tard. pour l'heure il s'agit juste de s'amuser un peu pour se récompenser de l'effort fourni jusqu'ici. Il nous tout d'abord faut créer deux variables de type Vector3 afin de sauvegarder la taille courante (changement de taille en X, Y ou Z) et la position (déplacement sur X, Y ou Z) du cube.
Vector3 position = Vector3.Zero;
Vector3 size = Vector3.One;
La méthode Update a été réécrite afin d'accueillir de nouvelles instructions.
if (Keyboard.GetState()[Keys.Up] == KeyState.Down)
position += Vector3.Up;
if (Keyboard.GetState()[Keys.Down] == KeyState.Down)
position += Vector3.Down;
if (Keyboard.GetState()[Keys.Left] == KeyState.Down)
position += Vector3.Left;
if (Keyboard.GetState()[Keys.Right] == KeyState.Down)
position += Vector3.Right;
if (Keyboard.GetState()[Keys.PageUp] == KeyState.Down)
size += new Vector3(0.1f, 0.1f, 0.1f);
if (Keyboard.GetState()[Keys.PageDown] == KeyState.Down)
size -= new Vector3(0.1f, 0.1f, 0.1f);
La propriété Keyboard.GetState() renvoie l'état du clavier courant. Il s'agit d'une collection de touches clavier définies dans le type Keys. Ici nous verifions l'état des touches Haut (Up), Bas (Down), Gauche, Droite, Page Haut, Page Bas. Deux états peuvent exiter, soit la touche est appuyée (KeyState.Down), soit elle est relachée (KeyState.Up). Ne reste plus en fonction de l'état de chacun de ces touches à mettre à jour nos deux variables. Si la touche est haut, nous faisons évoluer Y (la distance), si c'est gauche ou droite c'est X (deplacement horizontal). Nous n'avons pas défini de touches pour les déplacement verticaux (Z). Mais essayez comme exercice de créer ce déplacement à l'aide des touches + et - (Keys.Add et Keys.Subtract). Enfin pour page haut et page bas nous faisons évoluer la taille du cube de 0.1 unité. Essayez là encore comme exercice de ne faire évoluer qu'une seule des composantes X, Y ou Z afin d'étudier le résultat. Vector3.Up/.Down.Left/.Right correspondent à des contants qui nous évitent d'écrire des "valeurs" dans notre code. Vector3.Up correspond ainsi à la valeur "new Vector3(0, 1, 0)". Le code devient ainsi plus lisible. Les autres constantes suivent le même principe.
En ce qui concerne la rotation du code, la transformation est réalisée là aussi à l'intérieur de la méthode Update :
float fAngle = (float)gameTime.TotalGameTime.TotalSeconds;
//la transformation en elle mme
Matrix world = Matrix.CreateRotationY(fAngle) * Matrix.CreateRotationX(fAngle)
* Matrix.CreateScale(size)
* Matrix.CreateTranslation(position);
this.effect.Parameters["World"].SetValue(world);
Nous nous basons ici sur le temps écoulé depuis le lancement de l'application. La valeur est utilisé pour réaliser des rotation sur Y et X (les rotations étant cycliques prendre le temps écoulé est une façon de faire parfaite !). Celles-ci seront constantes quelque soit la vitesse de la machine qui fait fonctionner l'application. Les transformations (rotation Y, rotation X, Scale, Translation) sont multipliées ensemble pour obtenir une transformation globale qui les intègres toutes. Le resultat est donnée à la matrice World du fichier effet. Ce fichier effet étant utilisé pour afficher le cube, la transformation s'appliquera donc au cube.
Au final nous obtenons :

Vous pouvez de même tester la distance de vision de la caméra en eloignant le cube, au bout d'un moment il disparait dépassant la distance du far plane.
Conclusion
Nous avons vraiment progressé dans notre maitrise des Vertices et des transformations. Nous avons vu que pour faire un simple cube il faut pas moins de 36 vertices ! Cela peut faire peur si on imagine le nombre qu'il faut pour créer des modèles détaillés comme les arbres, le corps humain ou autres ... C'est l'objet de notre prochain tutorial : nous verrons une méthode pour réduire au maximum le nombre de vertices d'un modèle et optimiser leur utilisation.
Pour ce qui est des transformations, nous aurons encore l'occasion de les découvrir lorsque nous aboderons les lumières. Nous allons alors créer une reproduction de l'univers et des planètes du système solaire. Une bonne occasion et un excellent exercice pour éprouver ses connaissances en géométrie 3D et dans les calculs matriciels...
Vous pouvez télécharger le sample ici.
A bientôt sur ce Blog !
Valentin Billotte
Retourner au sommaire des cours