Annexe : Format de Vertices

Les vertices, en 3D, on connait leur importance : c’est eux qui forment les modèles 3D affichés à l’écran. Chacun d’eux apportent au modèle auquel ils appartiennent un ensemble d’informations qui peuvent être au choix :



  • Une ou plusieurs coordonnées de textures

  • Une couleur

  • Une position

  • Une normale

Un vertex en C# avec Xna (mais aussi avec Managed Direct) est représenté par une structure. C’est cette structure qui va définir l’ensemble des informations associées au vertex. Si vous regardez les types custom déjà présents dans le framework Xna comme

Xbox 360 : un mois de janvier record !

(source Reuters/Le monde) Microsoft a vendu 294.000 exemplaires de sa console de jeu video Xbox 360 en janvier. Les ventes de janvier représentent une hausse de près de 18% par rapport aux 250.000 unités écoulées lors du même mois de l’année dernière.


C’est vraiment le moment de se mettre au développement de jeu avec XNA non ? [:P]

XNA Tutorial 8 : Les textures


< en construction / under construction>


Retourner au sommaire des cours  


Notre premier chapitre consacré aux textures : un vaste, un très vaste sujet et au combien important dans le développement 3D. Le texturing est sans aucun doute l’élément le plus important pour donner du réalisme et de l’interet à un projet 3D. Nul doute qu’après cet apprentissage, nos petites productions n’auront plus rien à voir avec ce que nous faisions jusque là !


Nous suivrons trois phases dans cet apprentissage :



  • Faire connaissance avec le texturing en apprennant les notions de base.

  • Faire connaissance avec le blending de filtrage, de mipmapping, de blending et de multitexturing.

  • Opérer sur les textures.

 


Texture ?

 

Aucun rapport avec la définition du dictionnaire. La texture est une image qui est plaquée sur un triangle. Il s’agit donc d’un objet à deux dimensions. Nous pourrions la comparer à une surface collée sur un objet 3D pour lui donner un habillage réaliste. XNA comme DirectX autorise toute sorte de taille de texture. En interne il les découpera en images carrés dont le nombre de pixels est une puissance de 2 (16,32,256, …). Dans tous les cas il faut toujours se rappeler que pour la majorité des matériels installés sur les machines, les textures les plus rapides à manipuler font une taille de 256 par 256 et plus la taille de la texture est importante, plus il faudra de temps GPU pour les manipuler.

 

Si les données affichées à l’écran sont nommées pixels, les données d’une texture sont nommée texels (pour “texture element”).

 

DirectX dans le chargement s’avère très polyvalent et peut lire un grand nombre de type de fichiers images, à savoir PNG, JPG, BMP et TGA.


Dans tous les cas le format d’image n’influence rien pour les performances de jeu. Chaque image étant traduite dans un format interne à Direct3D lors de son chargement. Notons enfin que les textures peuvent être générée à la volée à partir d’un accès mémoire.


Coordonnées de texture


Voilà une notion qui peut étonner au premier abord : Une coordonnée de texture permet de spécifier une position dans la texture. Dans la mesure où, dans une texture, l’espace est en 2D, seules deux valeurs sont nécessaire pour spécifier une position : U et V. U représente l’abscisse, à savoir le nombre d’unités à parcourir horizontalement et V qui représente le nombre d’unités verticales (ordonnées) à parcourir. Leurs valeurs oscillent entre 0 et 1 (des valeurs supérieure peuvent être données pour répéter une texture, nous verrons cela plus tard). Le point supérieur gauche de la texture est donc le (0,0) et le point inférieur droit (1,1) pour (U, V). Le schéma ci-dessous explicite tout cela avec la texture que nous allons employer dans notre premier exemple.



Les coordonnées de textures.


 


Placage de textures
 


Les coordonnées de textures sont donc une notion simple qui s’apparente aux bases mathématiques de la géométrie dans l’espace mathématique. Toutes les coordonnées (UV) pour le placage sur une forme 3D seront associées à chaque vertex qui la compose.


Pour plus de simplicité nous allons mettre de coté le cube qui servait à tous nos exemples depuis plusieurs cours pour revenir au carré utilisé dans le tutoriel 3. Dans ce programme un carré est formé de 4 vertices associés à une couleur et une position X, Y, Z comme ci-dessous.


Un programme simple déjà vu qui affiche un carré.
Un programme simple déjà vu qui affiche un carré.


Associons maintenant un troisième type de valeur : les coordonnées de texture nommée ici tu et tv. Nous allons afficher sur ce carré notre texture en entier. Le point haut gauche aura donc la coordonnée de texture (0,0), le point haut droit (1,0) le point bas gauche (0,1) et enfin le point bas droite (1,1). Simple donc. Nous aurons :


La texture entière occupe la surface du carré.
La texture entière occupe la surface du carré.


Essayons maintenant de n’afficher que la moitié haut droite de la texture, à savoir la partie :


 


Nous aurons donc pour caractéristiques des 4 vertices :



Essayons pour continuer de n’afficher que la moitié basse de la texture :



Les caractéristiques seront :



Autre exemple, affichons le centre du carré à savoir :



nous aurons :



Finissons sur une tâche plus compliquée (rassurez vous seulement d’un chouïa).  Nous n’allons pas prendre une portion de la texture de manière a avoir des coordonnées “ronde” (à savoir 0.5 ou 1) mais une portion complétement aléatoire. Il va donc nous falloir calculer la valeur exacte de la texture, prendre le pourcentage et reporter la valeur entre 0 et 1. Mettons au hasard le point haut gauche de la source au texel (12,16). Pour une texture de (64,64), converti en pourcentage nous avons : 18.75% en abscisse (12/64*100) et 25% en ordonnée soit donc en coordonnées de texture 0.1875 pour tu et 0.25 pour tv. Faisons de même pour les 3 autres coins. Au final nous avons :



Terminons enfin sur la répétition de textures. Jusqu’à présent les valeurs de tu et tv étaient comprises entre 0 et 1. Que ce passe t’il si ces valeurs dépassent 1 ? XNA, lors du mappage va répéter la texture suivant le pourcentage donné par les coordonnées ;  Si tu vaut 1.5 la texture sera copiée entièrement avec la moitié d’une texture sur l’abscisse. Si tv vaut 3, alors sur l’ordonnée nous aurons trois textures copiées. Le dernier schéma de ce point illustre cela très bien :



Notons aussi que les valeurs négatives données à tu et tv inverseront le mappage de la texture. XNA permet d’autres interprétations dans le cas ou tu et tv dépassent la valeur 1 (nous verrons cela plus loin dans ce chapitre).


Passons maintenant aux choses concrètes.


Textures et XNA


Préparer notre projet à l’utilisation de textures… 


Jusqu’à présent, la seule manière que nous avions pour ajouter des couleurs à un scène 3D étant de créer autant de vertices que de couleurs associées. Nous allons utiliser maintenant un moyen bien plus puissant et efficace en associant chaque vertices à une coordonnée de texture comme vu dans le point précédent. Nous reprendrons un projet relativement similaire à celui utilisé pour le chapitre 4 consacré aux matrices. Revenez quelques instants sur le code qu’il contient avant de continuer plus en avant. Notre but sur ce cours est d’afficher un carré à l’écran et, en utilisant un format de vertice adapté, y plaquer une texture.


La première étape consiste à définir 6 vertices pour les deux triangles formant le carré. Sachant que nous n’utilisons plus de couleur nous ne pouvons plus utiliser le type VertexPositionColor. Nous utiliserons à la place un nouveau type de vertex : VertexPositionTexture. *



Remarque : Nous avons déjà abordé les structure de format de vertices comme VertexPositionTexture ou comme VertexPositionColor. Ces formats permettent d’associer à un vertex différentes informations (comme la position, la couleur, les coordonnées de  texture …). Vous vous demandez peut être comment XNA peut il savoir quel type de données (couleur, texture, …) est associé aux vertices qu’on lui donne ? Il utilise tout simple le contenu de la propriété que chaque structure de format doit posséder. Pour plus d’information à ce sujet et pour créer vos propres formats rendez vous ici. 


Changez la déclaration du tableau de vertices ainsi :


VertexPositionTexture[] vertices;


Vient maintenant la création de chacun des vertices :

vertices = new VertexPositionTexture[6]; //triangle 1, face devantvertices[0].Position = new Vector3(-10f, 0f, -10f);vertices[0].TextureCoordinate = new Vector2(0f, 0f);vertices[1].Position = new Vector3(-10f, 0f, 10f);vertices[1].TextureCoordinate = new Vector2(0f, 1f);vertices[2].Position = new Vector3(10f, 0f, 10f);vertices[2].TextureCoordinate = new Vector2(1f, 1f);//triangle 2, face devantvertices[3].Position = new Vector3(-10f, 0f, -10f);vertices[3].TextureCoordinate = new Vector2(0f, 0f);vertices[4].Position = new Vector3(10f, 0f, 10f);vertices[4].TextureCoordinate = new Vector2(1f, 1f);vertices[5].Position = new Vector3(10f, 0f, -10f);vertices[5].TextureCoordinate = new Vector2(1f, 0f); 

On remarque ici une première différence avec nos programmes précédents : plus de couleur mais une coordonnée de texture associée à chaque vertex. Le vertex 0 se trouve en haut à gauche du carré. La coordonnée (U,V) associée est donc (0, 0). On ancrera donc la partie supérieure gauche de la texture sur ce vertex. Le second vertex se trouve en haut à droite sa coordonnée est donc  (0, 0). La partie supérieure droite va être ancrée sur lui. Ainsi de suite pour les 4 autres vertices. C’est exactement ce que nous avons vu au tout début de ce chapitre.


Nous devons aussi spécifier à notre effet que nous ne manipulons plus des vertices colorés, mais texturés. Pour cela modifiez la ligne :

this.effect.CurrentTechnique = effect.Techniques[“Colored”];

en


this.effect.CurrentTechnique = effect.Techniques[“Textured”];


Il a été vu précédemment que le device où est affiché notre jeu (la carte graphique principale donc) a besoin de connaître le type de vertices utilisé (c’est à dire leur format). Il est donc nécessaire dans la méthode de dessin de modifier la ligne :

this.graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(this.graphics.GraphicsDevice, VertexPositionColor.VertexElements);

en

this.graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(this.graphics.GraphicsDevice, VertexPositionTexture.VertexElements);

A ce stade, l’application, à l’exécution affiche un magnifique carré … noir ! Tout est normal : nous n’avons pas encore utilisé de texture et nous n’utilisons plus de couleur.


Ajout de textures au projet… 


Nous utiliserons la texture prise en exemple précédemment :


Notre texture


Celle-ci, nommée “texture.jpg” se trouve à la racine du répertoire HuitiemeChapitre. Ajoutez là en cliquant droit sur le nom du projet et en selectionnant “ajouter un élément existant” :


Cliquez droit sur le projet et selectionnez "Ajouter un élément existant" 


Dans la fenetre qui s’ouvre, choisissez “Content Pipeline” dans la combobox de choix de type de contenu et selectionnez le fichier texture.jpg.


 


Après validation 


 

Déclarez un object de type Texture2D dans la classe Game1

Texture2D texture;


 


C’est cet objet qui va nous permettre de manipuler les textures dans notre application. Rendez vous maintenant dans la méthode LoadGraphicsContent : Il nous faut y charger la texture mais aussi spécifier à notre effet que nous associons cette texture à l’affichage des vertices. Nous procédons comme ceci :


 

protected override void LoadGraphicsContent(bool loadAllContent){    if (loadAllContent)    {        // TODO: Load any ResourceManagementMode.Automatic content        texture = content.Load<Texture2D>(“texture”);        effect.Parameters[“xTexture”].SetValue(texture);    }     // TODO: Load any ResourceManagementMode.Manual content

}


 


 


L’objet content de type ContentManager s’occupe des ressources du jeu en évitant au développeur d’avoir à se soucier de leur gestion. La méthode Load, générique est son seul membre réellement interessant. Elle renvoie un objet du type spécifié en généricité à partir du path ressource donné. Ici :


 


texture = content.Load<Texture2D>(“texture”);


 


Nous indiquons la spécificité sur le type Texture2D et donnons en path la string “texture”. Nous ne donnons pas l’extension “.jpg” car la méthode Load prend le nom AssetName. Pour voir sa valeur, selectionnez la texture dans le projet et appuyez sur F4 (ou cliquez droit sur ce fichier et faites Propriété). Dans la fenêtre d’outil qui s’ouvre le champs AssetName contient la string “texture”. Vous pouvez toujorus changer cette valeur pour donner un nom explicite, mais faites attention à bien vous y retrouver lorsque vous avez plusierus centaines de fichiers ressources !


Si vous lancez l’application vous obtenez maintenant :


 



 


Voila nous avons terminé ! Comme exercice revenez sur le premier point de ce chapitre où l’ont voit différentes manière de texturer un carré. Appliquez ces différentes possibilités à votre programme pour les tester.


 


 


telecharger Vous pouvez télécharger le sample, les deux moteurs et les exercices ici.   


 


 


Ajout de textures et de couleurs au projet… 


 


Avant de passer aux choses sérieuses, nous allons tenter d’associer trois données à chaque vertex :




  • La position bien évidemment


  • La texture


  • Une couleur

Nous utiliserons pour cela la structure de format de vertex VertexPositionColorTexture en lieu et place de VertexPositionTexture.


La déclaration des vertices sera modifiée de manière a spécifier le champs color comme auparavant :


 


 

vertices = new VertexPositionColorTexture[6]; //triangle 1, face devantvertices[0].Position = new Vector3(-10f, 0f, -10f);vertices[0].TextureCoordinate = new Vector2(0f, 0f);vertices[0].Color = Color.Green;vertices[1].Position = new Vector3(-10f, 0f, 10f);vertices[1].TextureCoordinate = new Vector2(0f, 1f);vertices[1].Color = Color.Red;vertices[2].Position = new Vector3(10f, 0f, 10f);vertices[2].TextureCoordinate = new Vector2(1f, 1f);vertices[2].Color = Color.Yellow;//triangle 2, face devantvertices[3].Position = new Vector3(-10f, 0f, -10f);vertices[3].TextureCoordinate = new Vector2(0f, 0f);vertices[3].Color = Color.Green;vertices[4].Position = new Vector3(10f, 0f, 10f);vertices[4].TextureCoordinate = new Vector2(1f, 1f);vertices[4].Color = Color.Red;vertices[5].Position = new Vector3(10f, 0f, -10f);vertices[5].TextureCoordinate = new Vector2(1f, 0f);

vertices[5].Color = Color.Yellow;


 


Faites bien attention à remplacer partout VertexPositionTexture.


Si vous lancez cette nouvelle version du programme, vous obtenez la même sortie qu’auparavant : et pour cause, notre fichier effet ne permet pas de gérer les vertices qui possèdent à la fois une coordonnée de texture et une couleur. Deux possibilités face à ce problème : chercher un fichier effet qui permet cela, ou utiliser celui de base inclu dans le framework. C’est cette seconde option que nous utiliserons pour sa simplicité.


La classe BasicEffect est une classe qui encapsule en interne un fichier effet (.fx) pratiquement identique à celui que nous utilisions jusque là. Elle offre une interface objet simple et intuitive pour paramétrer la façon dont les vertices sont affichés à l’écran.


 



Remarque : Pourquoi ne pas avoir directement utilisé cette classe au tout début de notre apprentissage ? Pour des raisons de compréhension : lorsqu’on travaille à trop haut niveau les API nous cachent se qui se passe réellement dans les couches basses des fonctionnalités utilisées. On se trouve souvent ainsi confrontés à des bugs incompréhensibles faute de connaissances techniques… Ce qui a été appris jusqu’ici sera très utile lorsque nous aborderons le chapitre sur les effets.


 


Supprimez toutes les références vers la classe Effect et son instance effect du programme. Ajoutez la déclaration suivante dans la classe Game1 :


 


 

BasicEffect effect;

 


 


Dans la méthode Initialize ajoutez le code suivant :


 

effect = new BasicEffect(this.graphics.GraphicsDevice, null);
 effect.VertexColorEnabled = true;
effect.TextureEnabled = true;
effect.View = viewMatrix;
effect.Projection = projectionMatrix;
effect.World = Matrix.Identity;


 


Nous pouvons en convenir : le code est réellement plus simple ; Ecrire :


 

effect.View = viewMatrix;
effect.Projection = projectionMatrix;
effect.World = Matrix.Identity;


 


est plus lisible que  


 

effect.Parameters[“xView”].SetValue(viewMatrix);
effect.Parameters[“xProjection”].SetValue(projectionMatrix);
effect.Parameters[“xWorld”].SetValue(Matrix.Identity);


 


Les deux premières instructions indiquent que les vertices affichés à l’écran gèrent à la fois la couleur et les textures :


 

effect.VertexColorEnabled = true;
effect.TextureEnabled = true;


 


Reste a affecter la dite texture dans la méthode LoadGraphicsContent de cette manière :


 

effect.Texture = texture; 

 


Si vous lancez maintenant l’application le résultat est meilleur :


 


 



 


La couleur de chaque texel de la texture a été “aditionnée” à la couleur du pixel du carré issue de la couleur du vertex sous jacent.


 


Pour la suite de nos applications nous utiliserons la classe BasicEffect.


 


 


telecharger Vous pouvez télécharger le sample, les deux moteurs et les exercices ici.   


 


 


Passons aux choses sérieuses…


Nous allons maintenant modifier la classe Cube du chapitre précédent afin de lui faire afficher une texture. Il lui sera aussi ajouté un objet de type BasicEffect afin de mettre la gestion de l’affichage des vertices au niveau du cube et non au niveau de tous les cubes. De même, pour changer un peu, une nouvelle texture (qui devrait rappeller de bon souvenirs) sera utilisée:



La classe Cube va contenir deux nouveaux champs :

private Texture2D _texture;

private BasicEffect _effect;


et deux nouvelles propriétés :

/// <summary>/// <para>Gets the cube’s effect.</para>/// </summary>public BasicEffect Effect{    get    {        return this._effect;    }} /// <summary>/// <para>Gets or sets the cube’s texture.</para>/// </summary>public Texture2D Texture{    get    {        return this._texture;    }    set    {        this._texture = value;        this._effect.Texture = value;    }

}


Une méthode InitializeEffect est ajoutée pour initialiser notre effet :

private void InitializeEffect()
{
    this._effect = new BasicEffect(this._device, null);
    this._effect.VertexColorEnabled = true;
    this._effect.TextureEnabled = true;

}


La méthode Render est modifiée en conséquence :

public void Render(){    this._effect.Begin();     foreach (EffectPass pass in this._effect.CurrentTechnique.Passes)    {        pass.Begin();        this._device.Vertices[0].SetSource(this._vertexBuffer, 0, VertexPositionColorTexture.SizeInBytes);        this._device.Indices = this._indexBuffer;        this._device.VertexDeclaration = new VertexDeclaration(this._device, VertexPositionColorTexture.VertexElements);        this._device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 24, 0, 12);        pass.End();    }     this._effect.End();

}


Le code de la classe est maintenant largement simplifié. Il ne nous reste plus qu’à modifier la déclarations des 8 vertices du cube pour leur donner à chacun, une coordonnée de texture. En fait il y’a ici un problème. Nous avons 8 vertices pour 6 faces et donc 12 triangles. Donc chaque vertex du cube est utilisé pour plusieurs faces. L’image suivant illustre le problème :


Le vertex rouge est utilisé par plusieurs face
Le vertex rouge est utilisé par plusieurs face


Si on prend le vertex surmonté d’un point rouge, on remarque que celui-ci appartient à trois faces :



  • La rouge pâle

  • La jaune pâle

  • La bleue pâle

Comment donner à ce vertex une coordonnée de texture dans ces conditions ? Pour la face rouge, ce vertex se trouve en bas à droite il aurait donc la coordonnée (1,1), or pour la face jaune il est en haut à droite et possède donc l’emplacement (1,0) sans parler de la face bleue dans laquelle il a la coordonnée (0,0) soit haut gauche. En fait chaque point du cube (au total 8 points) appartiennent à 3 faces. Il est donc nécessaire de créer non pas 8 vertices pour un cube mais 24 : chaque face possèdera ses propres vertices non partagés avec les autres faces, nous avons 6 faces de 4 vertices donc 24 vertices. Il ne s’agit pas ici d’une regression ; nous ne revenons pas au cube du tutoriel 5 qui possédait 36 vertices. La méthode InitializeVertices se présente désormais ainsi :

private void InitializeVertices(){    VertexPositionColorTexture[] vertices = new VertexPositionColorTexture[24];     //face devant    vertices[0].Position = new Vector3(0f, 0f, 0f);    vertices[0].Color = this.Color;    vertices[0].TextureCoordinate = new Vector2(0, 1);    vertices[1].Position = new Vector3(0f, 0f, 1f);    vertices[1].Color = this.Color;    vertices[1].TextureCoordinate = new Vector2(0, 0);    vertices[2].Position = new Vector3(1f, 0f, 1f);    vertices[2].Color = this.Color;    vertices[2].TextureCoordinate = new Vector2(1, 0);    vertices[3].Position = new Vector3(1f, 0f, 0f);    vertices[3].Color = this.Color;    vertices[3].TextureCoordinate = new Vector2(1, 1);     //face derrière    vertices[4].Position = new Vector3(1f, 1f, 1f);    vertices[4].Color = this.Color;    vertices[4].TextureCoordinate = new Vector2(0, 0);    vertices[5].Position = new Vector3(1f, 1f, 0f);    vertices[5].Color = this.Color;    vertices[5].TextureCoordinate = new Vector2(0, 1);    vertices[6].Position = new Vector3(0f, 1f, 0f);//    vertices[6].Color = this.Color;    vertices[6].TextureCoordinate = new Vector2(1, 1);    vertices[7].Position = new Vector3(0f, 1f, 1f);    vertices[7].Color = this.Color;    vertices[7].TextureCoordinate = new Vector2(1, 0);     //face gauche    vertices[8].Position = new Vector3(0f, 1f, 0f);//    vertices[8].Color = this.Color;    vertices[8].TextureCoordinate = new Vector2(0, 1);    vertices[9].Position = new Vector3(0f, 1f, 1f);    vertices[9].Color = this.Color;    vertices[9].TextureCoordinate = new Vector2(0, 0);    vertices[10].Position = new Vector3(0f, 0f, 1f);    vertices[10].Color = this.Color;    vertices[10].TextureCoordinate = new Vector2(1, 0);    vertices[11].Position = new Vector3(0f, 0f, 0f);    vertices[11].Color = this.Color;    vertices[11].TextureCoordinate = new Vector2(1, 1);     //face droite    vertices[12].Position = new Vector3(1f, 0f, 0f);    vertices[12].Color = this.Color;    vertices[12].TextureCoordinate = new Vector2(0, 1);    vertices[13].Position = new Vector3(1f, 0f, 1f);    vertices[13].Color = this.Color;    vertices[13].TextureCoordinate = new Vector2(0, 0);    vertices[14].Position = new Vector3(1f, 1f, 1f);//    vertices[14].Color = this.Color;    vertices[14].TextureCoordinate = new Vector2(1, 0);    vertices[15].Position = new Vector3(1f, 1f, 0f);    vertices[15].Color = this.Color;    vertices[15].TextureCoordinate = new Vector2(1, 1);     //face haut    vertices[16].Position = new Vector3(0f, 0f, 1f);//    vertices[16].Color = this.Color;    vertices[16].TextureCoordinate = new Vector2(0, 1);    vertices[17].Position = new Vector3(0f, 1f, 1f);    vertices[17].Color = this.Color;    vertices[17].TextureCoordinate = new Vector2(0, 0);    vertices[18].Position = new Vector3(1f, 1f, 1f);    vertices[18].Color = this.Color;    vertices[18].TextureCoordinate = new Vector2(1, 0);    vertices[19].Position = new Vector3(1f, 0f, 1f);    vertices[19].Color = this.Color;    vertices[19].TextureCoordinate = new Vector2(1, 1);     //face bas    vertices[20].Position = new Vector3(1f, 0f, 0f);    vertices[20].Color = this.Color;    vertices[20].TextureCoordinate = new Vector2(0, 1);    vertices[21].Position = new Vector3(1f, 1f, 0f);    vertices[21].Color = this.Color;    vertices[21].TextureCoordinate = new Vector2(0, 0);    vertices[22].Position = new Vector3(0f, 1f, 0f);//    vertices[22].Color = this.Color;    vertices[22].TextureCoordinate = new Vector2(1, 0);    vertices[23].Position = new Vector3(0f, 0f, 0f);    vertices[23].Color = this.Color;    vertices[23].TextureCoordinate = new Vector2(1, 1);     if (this.IsObjectOriginInCubeCenter)        for (int i = 0; i < 24; i++)            vertices[i].Position += new Vector3(-0.5f, -0.5f, -0.5f);     this._vertexBuffer = new VertexBuffer(    this._device,    typeof(VertexPositionColorTexture),    24,    ResourceUsage.WriteOnly,    ResourceManagementMode.Automatic);     this._vertexBuffer.SetData(vertices);} 

A noter la présence du bloc :

    if (this.IsObjectOriginInCubeCenter)
        for(int i = 0 ; i < 24 ; i++)
            vertices[i].Position += new Vector3(-0.5f, -0.5f, -0.5f);

J’ai rajouté sur demande de certains d’entre vous une propriétée nommée qui permet de préciser si le centre du cube doit coincider avec l’origine du repère de l’objet (pour plus d’information allez voir ici). L’origine se trouve par défaut sur le vertice 0, pour mettre l’origine au centre du cube il me suffit de déplacer tous les vertices de 0.5f unités car le cube a une taille de 1. Modifiez la méthode d’affichage pour indiquer que vous gérez non plus 8 vertices mais 24. Au lancement vous obtenez


On aurait presque envie de donner un coup de tête à la mario :)


(je reviendrais dans une annexe sur la manière de faire des mouvements amples et fluides pour ses objets dans une annexe prochainement).


 


 


telecharger Vous pouvez télécharger le sample, les deux moteurs et les exercices ici.   


 


 


A ce stade de nos connaissances nous pouvons …


L’information qui suit est à prendre avec des pincette mais elle donne toutefois une idée assez précise du niveau de nos connaissances à ce stade de notre apprentissage. Une partie de ceux qui lisent ces lignes ont sans doute déjà joué à l’un des épisode de la série à succès Tomb Raider (l’auteur de cours XNA a pour sa part joué aux version 1, 2, 3, 4). Nous sommes maintenant capable de réaliser des mondes tous à fait similaires à celui présenté par la capture d’écran ci-dessous :



Regardez attentivement cette image. Vous verrez qu’en fait, ce monde n’est constitué que de formes cubiques comme nous savons très bien les faire maintenant : des cubes, des cubes coupés sur leur diagonale, des cubes coupés en leur milieu etc. C’est là toute l’ingéniosité et toute la puissance du moteur de Core/Eidos : reproduire n’importe quelle architecture/géographique à l’aide de ces formes. Les mouvements de Lara sont d’ailleur soumi à une seule unité de mesure : la taille d’un cube. Ce principe assez proche des jeux de plateforme 2D permet de gérer les collisions et les déplacements très facilement et de créer des formes à l’écran simplement. Core/Eidos peut donc créer des mondes et développer un moteur beaucoup plus rapidement que des moteurs complexes comme celui de World Of Warcraft (que nous reproduirons à la fin de cet apprentissage).


Si le temps nous le permet nous essayerons de reproduire un monde à base de cubes.


 


Les filtres


Introduction 


Tous ceux qui ont travaillé avec des programmes d’édition d’images comme paint shop pro ou photoshop sont familiers des effets de pixellisation. On obtient cet effet désagréable lorsqu’on réduit la taille d’une image selon un rapport impair. Résultat : de nombreux détails sont perdus et la qualité de l’image s’en ressent. Pour éviter cela, les bons programmes d’édition d’images proposent différentes techniques de réduction ou d’augmentation de taille d’image aussi nommées filtrage pour réduire au minimum les pertes dues au changement de taille. XNA aussi.


Pour mieux comprendre, voici un exemple concrêt : Quand XNA affiche une primitive, il plaque un modèle 3-D sur un écran 2-D. Si la primitive possède une texture, XNA doit utiliser cette dernière afin de créer une couleur pour chaque Pixel de ce modèle en utiliser une valeur de couleur de la texture. C’est ce processus qu’on appelle filtrage de texture.


Quand une opération de filtre de texture est effectuée, la texture utilisée est “magnifiée” (agrandie) ou “minifiée” (réduite). En d’autres termes, elle est plaquée sur une primitive qui est plus grande ou plus petite que sa propre taille. La magnification d’une texture peut avoir comme conséquence l’utilisation d’un seul texel (point de couleur dans une texture) pour colorer un grand nombre de Pixel à l’écran. L’image résultante peut être volumineuse et pixelisée. La Minification d’une texture au contraire signifie souvent qu’un Pixel simple se substitue à plusieurs texels. L’image résultante peut être trouble . Pour résoudre ces problèmes, se mélanger des couleurs de texel doit être effectué pour arriver à une couleur pour le Pixel.


XNA tout comme DirectX fournit plusieurs types de filtrage de texture (lineaire, anisotropic, gaussien …) et un système de mipmapping (notion sur laquelle nous reviendrons plus loin). Si vous ne choisissez aucun filtre, XNA emploie une technique appelée nearestpoint sampling (en gros : choix du texel le plus proche).


Chaque type de filtrage de texture a des avantages et des inconvénients. Par exemple, le filtrage linéaire de texture peut produire des bords déchiquetés ou un aspect volumineux dans l’image finale. Cependant, c’est un filtre très simple et rapide à mettre en place à faible coùt de mémoire. Le filtrage anisotrope demande plus de temps processeur et beaoucp plus de mémoire.


L’exemple suivant dans le jeu Might and Magic le montre bien :


— La maison vue de loin est belle …
La maison vu de loin est belle …


— … mais quand on s’approche … tout est pixellisé et … hideux 
… mais quand on s’approche … tout est pixellisé et … hideux le jeu à l’époque, manquait d’un bon filtre de texture…


Might and Magic dans sa version 6 (NDA : la meilleure version) n’était pas très avancé graphiquement parlant. Le jeu manquait en autres choses d’un bon filtrage de texture. La première image montre un batiment dans la ville de Sorpigal. Vu de loin, la texture qui est plaquée sur ce batiment semble normale, mais comme le montre la seconde image, si l’on s’approche, un filtre nearest point sampling est effectué pour créer une magnification : on agrandit énormément la texture. En ressort alors un très désagréable effet de pixelisation. Dans la première image, la surface visible du batiment est d’environ 300*200. On s’approche donc de la taille de cette texture : l’affichage est donc optimal. Dans la seconde image la surface d’affichage pour la texture passe a 450*350 et ce pour n’afficher que 8% de la texture ! L’utilisation d’un bon filtrage permet d’éviter cet effet pixélisé horrible.


Et Xna dans tout ça ?


XNA offre au développeurs 5 filtrages (tous énuméré dans le type TextureFilter) :

 























Membre


Description


GaussianQuad


Un filtre 4-sample Gaussien pour la magnification ou la minification.


PyramidalQuad


Un filtre 4-sample pour la magnification ou la minification.


Anisotropic


Un filtre de texture anisotrope utilisé pour la magnification ou la minification. Les filtres anisotrope compensent la distorsion représentent par  l’angle  du polygone à mapper et la position de la caméra.


Linear


Un filtre de texture anisotrope utilisé pour la magnification ou la minification. Une moyenne pondérée de 2×2 texels autour du texel concerné est utilisé.


Point


Un filtre de texture point utilisé pour la magnification ou la minification. Le texel avec les coordonnées les plus proches du pixel voulu est utilisé.


None


Aucun filtre.


 


C’est la propriété SamplerStates de l’objet GraphicsDevice qui va nous permettre de spécifier le filtrage à appliquer en minification et/ou en magnification.
Si nous voulons appliquer un filtre anisotrope en mignification et lineaire en magnignification nous ferons :

this.graphics.GraphicsDevice.SamplerStates[0].MinFilter = TextureFilter.Anisotropic;
this.graphics.GraphicsDevice.SamplerStates[0].MagFilter = TextureFilter.Linear;


…pas très compliqué. Nous utilisons ici la propriété SamplerStates comme un tableau tout simplement parcequ’il est possible de plaquer plusieurs textures sur un même polygone (jusqu’à 8 textures). Nous appliquons ici nos filtres MinFilter et MagFilter sur la première texture (0). La classe SamplerState possède de nombreux membres pour la gestion de l’affichage des textures à l’écran. Nous les verrons tous dans ce chapitre.


Un exemple concrêt


Rien de mieux pour bien comprendre l’effet d’un filtre à l’écran que de développer un exemple. Nous reprendrons le dernier projet pour y effectuer quelques menues modifications. Voici ce que nous allons faire : un cube va être affiché en gros plan. Il tournera sur lui-même a vitesse très réduite de sorte de bien mettre en évidence les différents filtres que nous appliquerons. Les filtres seront selectionnés au clavier : la touche ‘A’ pour anisotope, ‘L’ pour linéaire, ‘P’ pour point, ‘G’ pour Gaussian, ‘Q’ pour Pyramidal, ‘N’ pour aucun. Si la touche Shift est appuyée en même temps, le filtre s’applique en magnification, sinon, en minification.


En fait le sample sera rapide à écrire, nous n’allons ajouter qu’une vingtaine d’instructions. Première étape, la classe Cube. Ajout de deux membres et deux propriétés associées pour sauvegarder le filtre en magnification et minification :

        private TextureFilter _minfilter;
        private TextureFilter _magfilter;

        public TextureFilter MagTextureFilter
        {
            get
            {
                return this._magfilter;
            }
            set
            {
                this._magfilter = value;
            }
        }
 
        public TextureFilter MinTextureFilter
        {
            get
            {
                return this._minfilter;
            }
            set
            {
                this._minfilter = value;
            }

        }


Seconde étape : Application des filtres dans la méthode Render :

/// <summary>/// <para>Render the cube on the device.</para>/// </summary>public void Render(){    this._effect.Begin();     foreach (EffectPass pass in this._effect.CurrentTechnique.Passes)    {        pass.Begin();         this._device.SamplerStates[0].MinFilter = _minfilter;        this._device.SamplerStates[0].MagFilter = _magfilter;         this._device.Textures[0] = this.Texture;        this._device.Vertices[0].SetSource(this._vertexBuffer, 0, VertexPositionColorTexture.SizeInBytes);        this._device.Indices = this._indexBuffer;        this._device.VertexDeclaration = new VertexDeclaration(this._device, VertexPositionColorTexture.VertexElements);        this._device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 24, 0, 12);        pass.End();    }     this._effect.End();

}


C’est terminé ! La classe Game1 a bien entendu été modifiée pour pouvoir affecter au cube le filtre voulu suivant la touche clavier appuyée. A l’exécution, l‘application se présente ainsi :



En jouant avec les touches a, A, l, L, p, P, g, G, q, Q, n ou N on peut facilement voir les différences de qualité entre tous les filtres suivant les angles de vue et la distance du cube. Les deux images suivants explicite cela. En mode point, le cube en courte distance donne l’affichage pixélisé suivant :


Mode point


Le zoom sur la face du cube en mode point montre bien l’effet de pixalisation. Maintenant la même face en anisotropie :


La même face en anisotropie


donne un bien meilleur résultat visual. 


Cet exemple n’est qu’une première approche des filtres. Il met en évidence l’importance qu’ils revettent pour le design et l’aspect de nos productions. C’est aussi un élément important à prendre en compte si l’on veut rendre son jeu accessible à tous, un filtre gourmant comme l’anisotrope doit être un choix et non une obligation.


Pour cela vous devez toujours vérifier avant toute chose que la machine sur laquelle tourne votre application supporte la technologie ou la fonctionnalité que vous voulez utiliser. D’une manière générale faites donc toujours des tests par rapport à la propriété GraphicsDeviceCapabilities de l’objet GraphicsDevice. Cette propriété de type GraphicsDeviceCapabilities contient un ensemble de booléean et d’énumération indiquant toutes les fonctionnalités et technologies supportés par le matériel en rapport avec le device courant. Pour savoir si l’anisotrop est supporté, faites un test sur :

this.graphics.GraphicsDevice.GraphicsDeviceCapabilities .TextureFilterCapabilities.SupportsMagnifyAnisotropic

this.graphics.GraphicsDevice.GraphicsDeviceCapabilities.TextureFilterCapabilities.


pour la magnification et sur :

this.graphics.GraphicsDevice.GraphicsDeviceCapabilities .TextureFilterCapabilities.SupportsMinifyAnisotropic

pour la minification.



Remarque : une annexe sera écrite prochainement et consacrée uniquement à l’importance des tests sur les capacités du matériel avant l’exécution.


De même (mais là nous voyons vraiment loin) il est préférable de proproser les fonctionnalités poussées comme ce filtre dans un menu d’option.


 


Le mip mapping



 


Mip vient du latin Multum in parvo, qui signifie « beaucoup de choses dans un petit endroit ».


 


 Principe


 Le mipmapping est une technique visant à améliorer l’affichage des textures sur les polygones en prennant en compte la distance comprise entre le dit polygone et la position de la caméra. Le niveau de détail des textures est adapté à la distance de l’objet. Ainsi, un objet proche de la caméra affichera des textures en haute résolution tandis qu’un objet situé au loin sera plaqué avec une texture de petite taille.


Chaque texture utilisée dans un processus de mip mapping est appellé “niveau de MIP map” ou LOD (pour Level Of Detail). Elles sont choisies suivant la distance à la caméra. La technique du MIP mapping consiste à envoyer au GPU des textures de résolutions décroissantes qui seront utilisées à la place de la texture originale. Le choix de la texture ayant la taille la plus adaptée s’effectue selon la distance du point de vue de l’objet texturé et le niveau de détails nécessaire. Le GPU n’a alors plus qu’à appliquer les bonnes textures sur les bons objets suivant leur éloignement, réadaptant la texture chaque fois que l’objet se rapproche. La texture utilisée lors du rendu sera alors celle dont la résolution est la plus proche de celle de l’objet sur l’image projetée. Chaque niveau doit avoir pour hauteur et largeur la moitié de la hauteur et largeur du niveau précédent. Le dernier niveau a bien évidemment au moins un coté égal à 1.


Par exemple, d’une image d’une taille de 256×256 pixels seront produits les mêmes images aux résolutions de 128×128 pixels, 64×64, 32×32, 16×16, 8×8, 4×4, 2×2 et 1×1. Si la taille de l’objet sur l’image projetée à l’écran est de 30×30 pixels, la texture utilisée sera alors celle de résolution 32×32 pixels.


Le filtrage joue ici un rôle primodial en évitant de voir les “sauts” lors du passage d’une texture à l’autre, grâce à une transition progressive.


 Quel est l’interet de du mip mapping ? Si cette technique est gourmande en mémoire vidéo (elle consomme un tiers de mémoire en plus), elle est plus rapide : La diminution du nombre de texels à traiter et des opérations de filtrages en temps réel de la texture permettent un rendu plus rapide de l’image. De plus le choix des différents niveaux de mipmap peut être laissé à l’apprecciation du développeur : Souvent laisser votre machine opérer pour voir les filtres peut donner un résultat qui s’avère desastreux et ce, même avec les meilleures techniques de filtrage. Prennez l’exemple de l’image suivante qui pourrait se trouver dans un de vos jeu :



Le message qu’elle véhicule est clair : tirer sur la cible fait perdre le joueur. Si nous laissons la machine gérer le mip mapping et la réduction de la texture nous pouvons obtenir le résultat suivant



C’est cette texture qui pourrait etre plaqué sur une forme 3D se trouvant loin du joueur. Celui-ci ne voyant pas le message tirera à coup sur. En fait l’algorythme de réduction d’image ne se préoccupe pas de l’information que l’image porte dans le mesure où il ne peut la quantifier. Il ne prend donc pas en compte l’importance de laisser le texte visible après une réduction n’est donc pas pris en compte dans le calcul du filtre. Si ce n’est pas la machine qui effectue cette tâche mais une personne, celle-ci peut réduire l’image de manière intelligente en gardant l’information visible au maximum :



Paramétrage du Mipmapping


 Deux paramètres peuvent intervenir dans l’affichage d’une texture mipmappée : Le MaxAnisotropy et le MipMapLevelOfDetailBias.


MaxAnisotropy


L’anysotropie est un filtre intelligent et assez gourmant en ressource (bien qu’il soit souvent intégré au hardware des cartes graphiques). Il se base sur l’aspect graphique de l’objet sur lequel on plaque la texture. Les texels ne sont pas tirés d’une forme symétrique, mais plutôt en utilisant un motif irrégulier ajusté à la perspective (on parle de figure anisotropique). Le nombre de texels pris en compte pour ce filtrage détermine la qualité de celui-ci. Avec le filtrage anisotropique 1x, on prend 8 texels. Le niveau maximum est 16x, les plus courants sont 2x (16 texels), 4x (32 texels), 8x (64 texels) et 16x (128 texels). Plus la valeur de MaxAnisotropy est importante, et plus le niveau de filtrage est élevé, et forcement, plus le temps de calcul augmente (et ceci de manière exponentielle…).


MipMapLevelOfDetailBias


Il arrive que pour la beauté du jeu il faille garder un LOD plus longtemps que prévu en retardant le passage à un LOD inférieur lorsque la caméra s’éloigne par exemple. C’est le LOD bias qui permet de réaliser cette opération. Si une surface s’éloigne de l’utilisateur, augmenter le LOD bias revient à retarder l’utilisation du LOD inférieur. Au contraire, en diminuant sa valeur, on passe plus rapidement aux LOD inférieurs.


 


 Après la théorie, la pratique !


 Il est temps de passer aux choses sérieuses. Le sample que nous allons réaliser ici aura pour but de mettre en évidence le mipmaping et les impacts des différents filtres. Nous allons réaliser pout cela un damier de 64 cases sur 64 cases. Chaque case sera associée à une texture “mipmappée”. La camera pourra se déplacer sur ce damier et nous pourrons ainsi voir, en se déplaçant l’évolution des textures sur les différentes cases. Nous aurons trois types de textures : des textures de couleur qui permettrons d’apprecier facilement le passage entre deux LOD, des textures représentant un sol pavé pour voir en réel ce que nos choix produisent, une texture damier pour apprécier la qualité des filtres.


La première étape consiste donc à créer ce qui va être notre premier terrain de jeu. A ce stade de nos connaissances il y a ici aucune difficulté : ayant déjà développé des applications affichants un carré, nous sommes largement en mesure de créer un monde constitué de 64 carrés sur 64 carrés. Ouvrez le projet nommé “TroisiemeProjetMipmap”. Une nouvelle classe Region fait son apparition. Elle sera chargé de la création et de l’affichage d’un terrain. Elle possède un seul vertexbuffer et un seul indexbuffer. Le vertexbuffer contiendra les 64*64 cases du terrain, sachant que chaque case possède 4 vertices. La méthode InitializeVertices se présente comme suit :

private void InitializeVertices(){    float caseSize = 16;    VertexPositionColorTexture[] vertices = new VertexPositionColorTexture[_numberOfVertices];     int index = 0;    for (int j = 0; j < this._depth; j++)    {        for (int i = 0; i < this._width; i++)        {            vertices[index].Position = new Vector3(i * caseSize, j * caseSize, 0f);            vertices[index].Color = this.Color;            vertices[index].TextureCoordinate = new Vector2(0, 0);             vertices[index +1 ].Position = new Vector3(i * caseSize + caseSize, j * caseSize, 0f);            vertices[index +1 ].Color = this.Color;            vertices[index +1 ].TextureCoordinate = new Vector2(1, 0);             vertices[index + 2].Position = new Vector3(i * caseSize + caseSize, j * caseSize – caseSize, 0f);            vertices[index + 2].Color = this.Color;            vertices[index + 2].TextureCoordinate = new Vector2(1, 1);             vertices[index + 3].Position = new Vector3(i * caseSize, j * caseSize – caseSize, 0f);            vertices[index + 3].Color = this.Color;            vertices[index + 3].TextureCoordinate = new Vector2(0, 1);             index+=4;        }    }     this._vertexBuffer = new VertexBuffer(    this._device,    typeof(VertexPositionColorTexture),    _numberOfVertices,    ResourceUsage.WriteOnly,    ResourceManagementMode.Automatic);     this._vertexBuffer.SetData(vertices);

}


Les membres depth et width représentent la profondeur et la largeur du monde (ils possèdent donc pour valeur 64 chacun donc). La double boucle for parcours le monde de jeu sur chaque ligne et chaque colonne en créant un carré à chaque fois. La valeur caseSize représente la taille d’un carré. Rien de compliqué ici. La méthode InitializeIndices est la suivante :

private void InitializeIndices(){    short[] indices = new short[_numberOfIndices];     short index = 0;    short indexCase = 0;    for (int j = 0; j < this._depth; j++)    {        for (int i = 0; i < this._width; i++)        {            indices[index] = indexCase;            indices[index + 1] = (short)(indexCase + 1);            indices[index + 2] = (short)(indexCase + 3);            indices[index + 3] = (short)(indexCase + 1);            indices[index + 4] = (short)(indexCase + 2);            indices[index + 5] = (short)(indexCase + 3);            index += 6;            indexCase += 4;        }    }     this._indexBuffer = new IndexBuffer(        this._device,        typeof(short),        _numberOfIndices,        ResourceUsage.WriteOnly,        ResourceManagementMode.Automatic);     this._indexBuffer.SetData(indices);

}


Nous spécifions 6 indices pour chaque carrés (une case équivaut à deux triangles de 3 points). Rien de vraiment compliqué là encore. Terminons sur cette classe en ajoutant qu’une propriété TextureMipMapFilter a été ajoutée afin de pouvoir spécifier un filtre mipmap depuis la classe Game1.



Chaque texture utilisée sera un mipmap composé de 6 LODs (256*256, 128, 60, 32, 16 et enfin 8). De la même façons que le premier projet de ce chapitre nous allons incorporer les LODs de chaque texture dans le projet. La texture colorée possédera les LOD suivants :



chaque couleur permettra de bien discerner les niveaux de mip map utilisé. La texture de sol utilisera les LOD suivants:


 


Chaque LOD a été placé dans un répertoire du projet nommé MipMapTextures de la même façon que pour le premier projet de ce chapitre. C’est la classe Game1 qui va former les trois textures composée de 6 LODs. Dans celle-ci se trouvent trois méthodes similaires nommées GenerateMipmap, GenerateMipmapTexture et GenerateMipmapCase respectivement pour la texture colorée, de sol et de damier. A noter qu’on aurait pu ici factoriser le code en une seule méthode, mais pour des raisons de compréhension, cela n’a pas été fait. Ces trois méthodes étant similaires, nous n’en verrons qu’une :

private void GenerateMipmap(){    Texture2D mainTexture = new Texture2D(this.graphics.GraphicsDevice,        256, 256,        6,        ResourceUsage.Dynamic,        SurfaceFormat.Color, //std 32 bits mode        ResourceManagementMode.Manual);      Texture2D texture256 = content.Load<Texture2D>(“MipmapTextures\\256”);    Texture2D texture128 = content.Load<Texture2D>(“MipmapTextures\\128”);    Texture2D texture064 = content.Load<Texture2D>(“MipmapTextures\\64”);    Texture2D texture032 = content.Load<Texture2D>(“MipmapTextures\\32”);    Texture2D texture016 = content.Load<Texture2D>(“MipmapTextures\\16”);    Texture2D texture008 = content.Load<Texture2D>(“MipmapTextures\\8”);     uint[] array = new uint[256 * 256];    texture256.GetData<uint>(array);    mainTexture.SetData<uint>(0, null, array, 0, array.Length, SetDataOptions.None);     array = new uint[128 * 128];    texture128.GetData<uint>(array);    mainTexture.SetData<uint>(1, null, array, 0, array.Length, SetDataOptions.None);     array = new uint[64 * 64];    texture064.GetData<uint>(array);    mainTexture.SetData<uint>(2, null, array, 0, array.Length, SetDataOptions.None);     array = new uint[32 * 32];    texture032.GetData<uint>(array);    mainTexture.SetData<uint>(3, null, array, 0, array.Length, SetDataOptions.None);     array = new uint[16 * 16];    texture016.GetData<uint>(array);    mainTexture.SetData<uint>(4, null, array, 0, array.Length, SetDataOptions.None);     array = new uint[8 * 8];    texture008.GetData<uint>(array);    mainTexture.SetData<uint>(5, null, array, 0, array.Length, SetDataOptions.None);     _sampleMipmapColor = mainTexture;} 


Cette méthode charge dans une variable locale nommée 6 LOD. Si quelque chose vous semble obscur de les explications suivent, reportez vous à la présentation du premier projet au tout début de ce chapitre. La première instruction

    Texture2D mainTexture = new Texture2D(this.graphics.GraphicsDevice,
        256, 256,
        6,
        ResourceUsage.Dynamic,
        SurfaceFormat.Color, //std 32 bits mode
        ResourceManagementMode.Manual);

créé la texture qui va contenir les LOD. Nous n’utilisons pas la classe Content pour la créer et nous aurons donc à la gérer nous même (nous reviendrons sur ce point plus loin). Une référence vers le device est bien évidemment donnée ainsi qu’une taille de 256*256. C’est à dire la taille du premier Level Of Detail. La valeur 6 représente le nombre de LOD que nous utilisonsResourceUsage.Dynamic indique que nous laissons le driver décider ou placer le buffer contenant les données de la texture. En général si il y’a la place, celui-ci est placé dans la mémoire AGP, sinon dans la mémoire vidéo, sinon en dernier cas dans la RAM. Un bon jeu sais se référer à la taille mémoire pour placer les textures les plus utilisées en AGP et les autres moins importantes dans la RAM afin d’optimiser les affichages (nous vérrons celà dans la quatrième partie de cet apprentissage). Le paramètre SurfaceFormat.Color indique simplement que la texture aura un format de type 32 bits standard (ARGB : 8bits pour l’alpha, 8 pour la composante Rouge, 8 pour le vert et 8 pour le bleu, soit 256 variations par composantes). Enfin ResourceManagementMode.Manual précise que nous gérons nous même la vie de la texture (a nous de la recréer lorsqu’elle est “perdue”. Vient ensuite le chargement des 6 LODs : 

    Texture2D texture256 = content.Load<Texture2D>(“MipmapTextures\\256”);
    Texture2D texture128 = content.Load<Texture2D>(“MipmapTextures\\128”);
    Texture2D texture064 = content.Load<Texture2D>(“MipmapTextures\\64”);
    Texture2D texture032 = content.Load<Texture2D>(“MipmapTextures\\32”);
    Texture2D texture016 = content.Load<Texture2D>(“MipmapTextures\\16”);
    Texture2D texture008 = content.Load<Texture2D>(“MipmapTextures\\8”);

Rien de compliqué ici. La valeur “256” est le nom Asset de la texture et “MipmapTextures” le path vers celle-ci (voir premier projet au début de ce chapitre). Les instructions qui suivent créént le Mipmap. Mais qu’est ce qu’un Mipmap sur le plan technique ? Il s’agit (pour parler simplement) d’une sorte de liste chainée. Chaque texture à l’intérieur possède pour taille la moitiée de son prédécesseur et est deux fois plus grande que son successeur directe. La première texture de cette liste à la taille la plus importante, la dernière la plus petite. Cette liste va être “ratachée” à une texture qui va faire office de “pointeur”. Suivant le degré de détail voulue, la texture va pointer sur telle ou telle élément de la liste. Créer cette structure en Xna/C# est relativement aisé. Il nous suffit d’associer une texture (notre pointeur) à un ensemble de buffers correspondant aux différents LODs. C’est les méthodes GetData et SetData qui vont nous permettre cela. Prenons exemple du chargement du buffer correspondant au troisième LOD :

    array = new uint[64 * 64];
    texture064.GetData<uint>(array);
    mainTexture.SetData<uint>(2, null, array, 0, array.Length, SetDataOptions.None);

 Première instruction, nous créeons un array de uint (entier non signés) qui va faire office de buffer. La seconde instruction charge ce buffer à partir des données de la texture ayant pour taille 64 par 64. Enfin, la troisième instruction associe ce buffer au troisième LOD (index 2) de la texture “pointeur” (mainTexture). Le premier paramètre de  correspond au niveau de LOD voulu, le second correspond à un objet de type Rectangle pour indiquer la zone du buffer que nous remplissons (ici null donc nous chargeons tout le buffer). Nous passons ensuite le buffer lui-même, indiquons à partir de quel index nous le lisons et sa taille. Le dernier paramètre de type SetDataOptions spécifie la manière dont on écrase le buffer déjà associé à ce LOD (si il y’en a un) : supprimé, comblé ou surchargé (soit respectivement SetDataOptions.Discard, SetDataOptions.NoOverwrite, SetDataOptions.None).


La partie LOD de notre programme est terminée ! Ne reste plus qu’à gérer le déplacement sur le terrain de jeu et la modification des différents filtres à l’aide des touches clavier. Pour cela, une simple analyse de la méthode Update vous suffira à ce stade de cos connaissances. Avant de lancer le programme, jetez juste un oeil à la méthode LoadGraphicsContent :

/// <summary>/// Load your graphics content.  If loadAllContent is true, you should/// load content from both ResourceManagementMode pools.  Otherwise, just/// load ResourceManagementMode.Manual content./// </summary>/// <param name=”loadAllContent”>Which type of content to load.</param>protected override void LoadGraphicsContent(bool loadAllContent){    if (loadAllContent)    {    }     // TODO: Load any ResourceManagementMode.Manual content    this.GenerateMipmapTexture();    this.GenerateMipmap();    this.GenerateMipmapCase();    this._terrain.Texture = _sampleMipmapColor;

}


Nous y plaçons le chargement de nos textures. Dans la mesure ou les 3 mipmaps sont chargés à l’aide de l’option ResourceManagementMode.Manual ce chargement se fait à l’extérieur du bloc conditionnelif (loadAllContent)“.


Il y’a deux types d’appel à cette méthode dans la vie d’une application Xna : le premier appel lors du lancement de l’application passe à cette méthode la valeur trueDe ce fait toutes les ressources sont chargés à la fois dans le bloc if et à l’extérieur. Le second appel intervient lorsqu’il y’a perte de ressources (après un redimentionnement ou une  réduction de la fenêtre … tout ce qui peut obliger le device graphique à s’occuper d’une autre application au détriment de la notre), à ce moment là, Xna passe à la méthode falseSeules les ressources en dehors du bloc conditionnel sont donc chargés : à ce stade les ressources ResourceManagementMode.Automatic ont déjà été rechargée automatiquement.Dans la même sens nous devons gérer nous même la libération de ces ressources dans la méthode UnloadGraphicsContent :

/// <summary>/// Unload your graphics content.  If unloadAllContent is true, you should/// unload content from both ResourceManagementMode pools.  Otherwise, just/// unload ResourceManagementMode.Manual content.  Manual content will get/// Disposed by the GraphicsDevice during a Reset./// </summary>/// <param name=”unloadAllContent”>Which type of content to unload.</param>protected override void UnloadGraphicsContent(bool unloadAllContent){    if (unloadAllContent == true)    {        content.Unload();    }    this._sampleMipmapAuto.Dispose();    this._sampleMipmapColor.Dispose();    this._sampleMipmapTexture.Dispose();

}



là encore à l’extérieur du bloc conditionnel.


Pour preuve déplacez les quatres instructions de la méthode de chargement dans le bloc if. Lancez le programme et agrandissez la fenêtre : vous obtiendez une belle erreur spécifiant que le programme essaye d’accéder à une texture supprimée. Et pour cause au redimentionnement de la fenêtre les textures sont perdues et sont donc libérée de la mémoire. Lorsque le programme reprend la main il essaye d’utiliser ces textures pour l’affichage et plante forcement.


 


L’application


Les touches de l’application sont les suivantes :  la touche ‘A’ pour anisotope, ‘L’ pour linéaire, ‘P’ pour point, ‘G’ pour Gaussian, ‘Q’ pour Pyramidal, ‘N’ pour aucun. Si la touche Shift est appuyée en même temps, le filtre s’applique en magnification, sinon, en minification. Si la touche Alt est appuyée, le filtre s’applique au Mipmap. Les touches fléchées permettent de se déplacer dans le monde. Les touches Page Haut et Page Bas permettent de prendre de l’altitude et de redescendre. Enfin les touche + et – augmentent le paramètre MaxAnisotropy (voir plus haut). Les mêmes touches avec Shift appuyé modifient le LodBias (voir plus haut). La touche espace ajoute une grille qui permet de bien discerner les cases du terrain de jeu (en fait le terrain est affiché une deuxième fois en mode WireFrame). Enfin les touches F1, F2, F3 affichent respectivement les texture de couleur, sol, damier.


Au lancement de l’application, vous obtenez l’affichage suivant : 



Magnifiquement … rouge, et pour cause : le mipmap n’est pas activé (None). Dans ce cas, le programme n’utilise que le LOD 0 qui correspond à la texture 256*256 qui est rouge. Appuyez sut les touches Alt et A pou lancer le mipmapping avec le filtre anisotropique. L’écran se change en :



Les différentes strates que vous voyez là correspondent aux 6 textures de LOD que nous avons chargée. On remarque parfaitement que suivant la distance à la caméra, les différentes cases du terrain de jeu affichent une texture adaptée à leur taille à l’écran :


 


Maintenant en maintenant la touche Shift appuyée appuyez sur la touche + (Add). Ceci a pour conséquence d’augmenter le Bias et donc le passage au LOD suivant :



Shift avec la touche – (Substract) réduit au contraire le Bias et prolonge la vie du LOD 0 :



Le filtre mipmap que nous avons utilisé ici (Anisotropique) est sans doute le meilleur. Le passage d’un Lod à un autre se fait de manière progressive et douce. Prennez un chouïa d’altitude à l’aide de la touche Page Haut. Si vous affichez la grille à l’aide de la touche Espace vous rendrez compte que cette transition ne dépend absolument pas de la case mais bien de la distance du “point” dans l’espace qui recoit la texture à la caméra :



Pour comprendre la puissance du filtre anisotropique, changez le filtre en filtre Point en appuyant sur les touches Alt + P. L’affichage qui apparait alors est rédibitoire … :



Passons à un autre type de texture, la texture sol. Appuyez sur la touche F2 et annulez tout mipmaping à l’aide de la combinaisons Alt +  N. Vous obtenez l’affichage suivant :



Aucun mipmapping et aucun filtre en mignification donne un résultat plus que décevant. Améliorons tout cela : Ajoutez un mipmapping avec filtre point (combinaison Alt + P) :



L’affichage est meilleur mais comme le montrent les deux lignes rouges nous voyons parfaitement les lignes de transition entre un Lod et un autre (avec la texture de couleur c’est encore plus flagrant). Et pour cause : le filtre point pour le mipmapping est des plus simples : lorsqu’il y’a besoin de faire une transition vers un autre LOD il utilise directement les texels de la texture voulue sans essayer de rendre la transition progressive. Si vous vous déplacez à l’aide des touches fléchées vous remarquez encore mieux cette transition. Nous pouvons toujours appliquer un filtre lineaire en mignification (touche L) :


 


mieux, mais la transition est toujours visible… Si on applique maintenant un filtre linaire pour le mipmapping (Alt + L), le rendu est quasi parfait :



On parle dans le cas du mipmapping avec filtre linaire de “filtrage trilineaire”. Ce filtrage effectue une moyenne progressive entre les texels des textures concernées par la transitions d’un LOD à un autre. Même si vous vous déplacez, vous ne verrez pas la transition d’un LOD à un autre. A noter que le filtre trilinaire ou le filtre anisotropique donnent pratiquement le même résultat dans la mesure où nous affichons des formes relativement “droites”.


Revenez à un affichage sans filtre (touche N et shift N) et sans mipmapping (touche Alt + N). Affichez la texture Damier avec F3 :



On voit que l’affichage n’arrive absolument pas a gérer les lignes horizontales et verticales de la texture. Combien même vous affichez un filtre Linaire en mignification et magnification on garde un affichage desastreux.


 


mais si vous appliquez un filtre mipmapping en anisotropie l’affichage est parfait :


 


Nous en avons terminé avec le mipmapping. Un point très important pour la fluidité et la beauté de nos applications 3D. Continuez à vous familiariser avec cette application qui met parfaitement en évidence les filtres de mignification et magnification et les filtres de mipmapping.


Conclusion


 


 


 


 


 


telecharger Vous pouvez télécharger le sample, les deux moteurs et les exercices ici.   


 


 


Retourner au sommaire des cours 


 


</ en construction / under construction>


 

Annexe : Transformations et Matrices

Retourner au sommaire des cours  


Les transformations dans l’espace représentent le traitement clé de voute dans toute application 3D. C’est par les transformations que les objets prennent vie. Une transformation pour parler simplement est une suite de multiplication de matrices. Généralement on distingue 3 types de matrices pour travailler sur un modèle 3D avant son affichage à l’écran : les matrices de rotation, les matrices de redimentionnement, les matrices de translation. Lorsqu’on veut déplacer un objet de le faire tourner il suffit donc de réaliser des traitements du type :


 matriceTransformation = matriceRotation * matriceTranslation


Ici nous réalisons une rotation puis une translation. L’objet représente le résultat de cette transformation et peut être vu comme un condensé unique des deux matrices. C’est cette matrice qui sera affectée à la matrice monde (ou World) pour placer l’objet à l’écran avant son affichage. 


Vous devez, lorsque vous effectuez ce genre de calculs, garder en tête que :




  • Toute transformation sur un objet s’effectué par rapport au repère de cet objet.


  • La multiplication de matrices, n’est pas commutative.

 Revenons justement sur ces deux notions.



 

Repère de l’objet 


 Lorsqu’on effectue une transformation sur un objet, cette transformation se fait par rapport au point d’origine dans le repère de l’objet. Le repère de l’objet est le repère par rapport auquel vous vous référez lorsque vous placez les vertices du modèle que vous voulez afficher. 


Prenons par exemple les huits points de ce cube :


vertices[0].Position = new Vector3(-10f, -10f, -10f);


vertices[1].Position = new Vector3(-10f, -10f, 10f);


vertices[2].Position = new Vector3(10f, -10f, 10f);


vertices[3].Position = new Vector3(10f, -10f, -10f);


vertices[4].Position = new Vector3(10f, 10f, 10f);


vertices[5].Position = new Vector3(10f, 10f, -10f);


vertices[6].Position = new Vector3(-10f, 10f, -10f);


vertices[7].Position = new Vector3(-10f, 10f, 10f);


Ici le cube fait 20 unités de coté (de -10 à +10).  Le point d’origine (0, 0, 0) du repère qui lui est associé se trouve pile au centre du cube. Si nous effectuons une rotation du cube, celle-ci s’effectuera donc par rapport à cette origine qui, étant au centre du cube, donnera l’impression que le cube tourne sur lui-même. L’image suivant illustre cela :


Le cube tourne par rapport au centre du repère associé qui se trouve au centre du cube


 (nous effectuons ici une rotation sur X et Y).


 Maintenant regardons un cube créé avec les 8 points suivants : 

vertices[0].Position = new Vector3(0f, 0f, 0f);vertices[1].Position = new Vector3(0f, 0f, 20f);vertices[2].Position = new Vector3(20f, 0f, 20f);vertices[3].Position = new Vector3(20f, 0f, 0f);vertices[4].Position = new Vector3(20f, 20f, 20f);vertices[5].Position = new Vector3(20f, 20f, 0f);vertices[6].Position = new Vector3(0f, 20f, 0f);//vertices[7].Position = new Vector3(0f, 20f, 20f);

Là encore, chaque coté du cube possède une taille de 20 unités. Mais le centre du repère (0 ,0 ,0) correspond au premier point. La rotation que nous avons effectué précédemment associée à ce cube se fera par rapport à ce point. Le cube ne tournera donc plus sur lui-même mais par rapport à un de ses sommets. L’image suivant illustre celà :


La rotation est excentrée parcequ'effectuée par rapport au centre du repère associée qui n'est pas au centre du cube


 Ici, c’est le point de couleur jaune qui est au centre du repère.


Nous venons de faire un grand pas pour bien comprendre comment maitriser les matrices et les transformations associées.


 


Ordre des transformations / Communativité


Au tout début de cet article, nous avons vu l’instruction suivante : 


  matriceTransformation = matriceRotation * matriceTranslation


Sachant que la classe Matrix nous offre un ensemble de méthodes statiques clé en main pour créer ce type de matrices, en langage C# nous aurions quelque chose du genre 


matriceTransformation = Matrix.CreateRotationY(MathHelper.PIOver2)* Matrix.CreateTranslation(20, 0, 0)


Pour créer une rotation de 90° suivie d’une translation. Le verbe au participe passé “suivie’ est important dans cette phrase. Je n’ai pas utilisé le complément “et”. En effet il y’a une notion d’ordre dans le multiplication des matrices ; C’est au niveau de ce genre de traitement qu’il nous faut faire attention. La multiplication de matrices n’est en effet pas commutative ; l’instruction précécent ne donne pas le même résultat que :


matriceTransformation = matriceTranslation *matriceRotation


On peut se référer au premier point de cet article où les transformations s’effectuent par rapport au modèle de l’objet. Prenons l’affichage suivant :


Notre cube au départ


Nous disposons ici d’un cube. Si nous effectuons une rotation sur l’axe Z, l’arrete située sur l’axe X se trouvera sur l’axe Y. Le pavé aura tourné autour de son origine. Si maintenant nous effectuons une translation sur l’axe X, le pavé tourné va être déplacé et nous obtiendrons comme résultat pour : 


  matriceTransformation = matriceRotation * matriceTranslation


 Le rendu :



Revenons maitenant au premier affichage et essayons l’instruction :


matriceTransformation = matriceTranslation *matriceRotation


La transformation commence d’abord ici par une translation d’une distance que nous appelerons D. Nous obtenons donc le même résultat que l’image précédente mais avec un pavé non tourné. Une rotation est ensuite effectée. C’est là que la différence se fait pleinement sentir : la rotation se situe non pas par rapport au pavé mais par rapport au centre du repère de l’objet qui se situe à une distance D de l’objet. La rotation va donc faire tourner l’objet autour d’un cercle imaginaire possèdant un rayon d’une taille équivalente à D. Le résultat donne donc :



Cet exemple illustre parfaitement la non commutativité de la multiplication de matrices. Même si cet exemple est simple, il faut toujour garder à l’esprit cette loi fondamentale, surtout lorsqu’on travaille sur un très grand nombre de matrices qui interagissent entre elles.


 


Exemple de classe “Transform-Safe”


Pour terminer cet article, voici une classe dont hérite la plupart des objets métier affiché à l’écran dans les applications 3D que je réalise. Elle donne à ses objets enfants un ensemble de propriétés et de méthodes simples qui permettent d’interragir avec l’objet de manière transparente et intuitive pour modifier l’objet avant son rendu.

/// <summary>/// <para>Defines a transformable object.</para>/// </summary>public class TransformBase{     #region Private members     [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]    private Matrix _translation;    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]    private Matrix _rotation;    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]    private Matrix _scale;    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]    float _rotationX, _rotationY, _rotationZ;     #endregion      #region Properties     /// <summary>    /// <para>Gets the associated transformation matrix.</para>    /// </summary>    public Matrix Transform    {        get        {            return this._scale * this._rotation * this._translation;        }    }      /// <summary>    /// <para>Gets or sets the current object’s position.</para>    /// </summary>    public Vector3 Position    {        get        {            return new Vector3(this._translation.M41, this._translation.M42, this._translation.M43);        }        set        {            this._translation.M41 = value.X;            this._translation.M42 = value.Y;            this._translation.M43 = value.Z;        }    }     /// <summary>    /// <para>Gets or sets the position on the X axis.</para>    /// </summary>    public float X    {        get        {            return this._translation.M41;        }        set        {            this._translation.M41 = value;        }    }     /// <summary>    /// <para>Gets or sets the position on the Y axis.</para>    /// </summary>    public float Y    {        get        {            return this._translation.M42;        }        set        {            this._translation.M42 = value;        }    }     /// <summary>    /// <para>Gets or sets the position on the Z axis.</para>    /// </summary>    public float Z    {        get        {            return this._translation.M43;        }        set        {            this._translation.M43 = value;        }    }     /// <summary>    /// <para>Gets or sets the current rotation in radian on X axis.</para>    /// </summary>    public float XRotation    {        get        {            return this._rotationX;        }        set        {            this.Rotate(value, _rotationY, _rotationZ);        }    }     /// <summary>    /// <para>Gets or sets the current rotation in radian on Y axis.</para>    /// </summary>    public float YRotation    {        get        {            return this._rotationY;        }        set        {            this.Rotate(_rotationX, value, _rotationZ);        }    }     /// <summary>    /// <para>Gets or sets the current rotation in radian on Z axis.</para>    /// </summary>    public float ZRotation    {        get        {            return this._rotationZ;        }        set        {            this.Rotate(_rotationX, _rotationY, value);        }    }     /// <summary>    /// <para>Gets or sets the current scale on X axis.</para>    /// </summary>    public float XScale    {        get        {            return this._scale.M11;        }        set        {            this._scale.M11 = value;        }    }     /// <summary>    /// <para>Gets or sets the current scale on X axis.</para>    /// </summary>    public float YScale    {        get        {            return this._scale.M22;        }        set        {            this._scale.M22 = value;        }    }     /// <summary>    /// <para>Gets or sets the current scale on X axis.</para>    /// </summary>    public float ZScale    {        get        {            return this._scale.M33;        }        set        {            this._scale.M33 = value;        }    }     #endregion      #region Constructor     /// <summary>    /// <para>Default constructor.</para>    /// </summary>    public TransformBase()    {        Reset();    }     #endregion      #region Public methods     /// <summary>    /// <para>Reset the matrices to default position.</para>    /// </summary>    public void Reset()    {        this._translation = Matrix.Identity;        this._rotation = Matrix.Identity;        this._scale = Matrix.Identity;        this._rotationX = _rotationY = _rotationZ = 0.0f;    }     #endregion      #region Transform methods     #region Translate     /// <summary>    /// <para>Move current object to the specified location.</para>    /// </summary>    /// <param name=”x”>Position X-axis value.</param>    /// <param name=”y”>Position Y-axis value.</param>    /// <param name=”z”>Position Z-axis value.</param>    public void MoveTo(float x, float y, float z)    {        this._translation.M41 = x;        this._translation.M42 = y;        this._translation.M43 = z;    }     /// <summary>    /// <para>Move current object to the specified location.</para>    /// </summary>    /// <param name=”translation”>Position’s vector.</param>    public void MoveTo(Vector3 translation)    {        this.MoveTo(translation.X, translation.Y, translation.Z);    }     /// <summary>    /// <para>Translate current object from the current position with the specified vector.</para>    /// </summary>    /// <param name=”x”>Vector X-axis value.</param>    /// <param name=”y”>Vector Y-axis value.</param>    /// <param name=”z”>Vector Z-axis value.</param>    public void Translate(float x, float y, float z)    {        _translation.M41 += x;        _translation.M42 += y;        _translation.M43 += z;    }     /// <summary>    /// <para>Translate current object from the current position with the specified vector.</para>    /// </summary>    /// <param name=”translation”>Translation vector</param>    public void Translate(Vector3 translation)    {        this.Translate(translation.X, translation.Y, translation.Z);    }     #endregion      #region Rotate     /// <summary>    /// <para>Rotate current object on the X,Y,Z axis.</para>    /// </summary>    /// <param name=”x”>X axis rotation in radians</param>    /// <param name=”y”>Y axis rotation in radians</param>    /// <param name=”z”>Z axis rotation in radians</param>    public void Rotate(float x, float y, float z)    {        this._rotationX = x;        this._rotationY = y;        this._rotationZ = z;        this._rotation = Matrix.CreateRotationX(x)*Matrix.CreateRotationY(y)*Matrix.CreateRotationZ(z);    }     /// <summary>    /// <para>Make a rotation from the current rotation.</para>    /// </summary>    /// <param name=”x”>X axis rotation in radians</param>    /// <param name=”y”>Y axis rotation in radians</param>    /// <param name=”z”>Z axis rotation in radians</param>    public void Turn(float x, float y, float z)    {        this._rotationX += x;        this._rotationY += y;        this._rotationZ += z;        this.Rotate(this._rotationX, this._rotationY, this._rotationZ);    }      #endregion      #region Scale     /// <summary>Resize the current object.</summary>    /// <param name=”x”>Scale factor for X values.</param>    /// <param name=”y”>Scale factor for Y values</param>    /// <param name=”z”>Scale factor for Z values</param>    public void Resize(float x, float y, float z)    {        this._scale.M11 = x;        this._scale.M22 = y;        this._scale.M33 = z;    }     /// <summary>Scale current object from current size.</summary>    /// <param name=”x”>X</param>    /// <param name=”y”>Y</param>    /// <param name=”z”>Z</param>    public void Scale(float x, float y, float z)    {        this._scale.M11 += x;        this._scale.M22 += y;        this._scale.M33 += z;    }     #endregion     #endregion }

 


Vous remarquerez ici des instructions du type :

            this._scale.M11 = x;            this._scale.M22 = y;            this._scale.M33 = z; 

Les propriété comme M11 ou M23 correspondent à des valeurs situées dans la matrices. Nous reviendrons dans un prochain article sur leur raison d’être.


 


Conclusion 


Cet article est incontournable si vous ne maitrisez pas encore la notion de transformation. Ayez toujours en tête les principes élémentaires qu’il a énuméré afin de vous éviter de nombreuses heures de recherches après des invraissemblances graphiques, qui sont, au final, relativement simples à éviter.


[Soon]


Valentin Billotte


[Help] 


Retourner au sommaire des cours 

XNA Tutorial 7 : Cas concret, l’affichage d’une ville et du système solaire

Retourner au sommaire des cours  


Nous avons passé, avec le dernier tutorial, une première étape dans le développement de jeux. Ce tutorial marque la fin de cette étape et, en conclusion, va nous apprendre à utiliser nos connaissances acquises pour créer une ville (rudimentaire, il faut rester humble), et pour créer une simulation de notre bon vieux système solaire. Ces deux projets ne sont pas seulement ludiques, il vont nous être très utiles. La ville tout d’abord va nous permettre de bien comprendre comment s’orienter dans l’espace et de mesurer l’utilité d’un bon système objet. La simulation quand à elle, poussera nos connaissances et notre maitrise des Matrices dans leur derniers retranchements. Ces deux projets seront aussi la base de nombreux projets qui vont suivre pour expliciter les notions que nous apprendrons aux fils des tutoriaux qui vont suivre.


L’objet de toutes les convoitises


La ville que nous allons créer sera simple. Chaque paté de maison sera en fait constitué d’un seul batiment dont la hauteur variera de manière aléatoire. Nous utiliserons pour la créer trois types de cubes : un cube pour créer le sol, un cube par paté pour créer les trottoires, un cube pour créer chaque gratte ciel. Evidemment nous n’allons pas comme dans le précédent tutoriel, nous amuser à instancier un vertex buffer et un index buffer pour chaque cube créé à la main. Nous allons plutot utiliser une classe “Cube” qui va faire tout le travail à notre place en nous offrant deux méthodes : Load et Render. Nous reprendrons le code du tutorial précédent en le factorisant pour permettre un développement plus modularisé.


La nouvelle classe Cube se présente ainsi :

/// <summary>/// <para>3D representation of a cube.</para>/// </summary>public class Cube : IDisposable{     #region Private members     private GraphicsDevice _device;    private VertexBuffer _vertexBuffer = null;    private IndexBuffer _indexBuffer = null;    private float _width;    private float _height;    private float _depth;    private Color _color = Color.TransparentWhite;    private float _x, _y, _z;    private float _rotationX, _rotationY, _rotationZ;    private static short[] indices = new short[36]{0,1,2,                                          0,2,3,                                          3,2,4,                                          3,4,5,                                          5,4,7,                                          5,7,6,                                          6,7,1,                                          6,1,0,                                          6,0,3,                                          6,3,5,                                          1,7,4,                                          1,4,2};    private Matrix _transformationMatrix;    private Matrix _translationMatrix;    private Matrix _scaleMatrix;    private Matrix _rotationMatrix;     #endregion      #region Properties      /// <summary>    /// <para>Gets or sets the Cube’s color.</para>    /// </summary>    public Color Color    {        get        {            return this._color;        }        set        {            this._color = value;        }    }     /// <summary>    /// <para>Gets or sets the Cube’s current transformation.</para>    /// </summary>    public Matrix Transformation    {        get        {            return this._transformationMatrix;        }        set        {            this._transformationMatrix = value;        }    }     /// <summary>    /// <para>Gets the cube’s Width.</para>    /// </summary>    public float Width    {        get        {            return this._width;        }    }     /// <summary>    /// <para>Gets the cube’s height.</para>    /// </summary>    public float Height    {        get        {            return this._height;        }    }     /// <summary>    /// <para>Gets the cube’s depth.</para>    /// </summary>    public float Depth    {        get        {            return this._depth;        }    }     /// <summary>    /// <para>Gets the cube’s position on x-axis.</para>    /// </summary>    public float X    {        get        {            return this._x;        }    }     /// <summary>    /// <para>Gets the cube’s position on y-axis.</para>    /// </summary>    public float Y    {        get        {            return this._y;        }    }     /// <summary>    /// <para>Gets the cube’s position on z-axis.</para>    /// </summary>    public float Z    {        get        {            return this._z;        }    }     #endregion      #region Constructors     /// <summary>    /// <para>Instanciate a new Cube objet.</para>    /// </summary>    public Cube()    {        this._transformationMatrix = Matrix.Identity;        this._vertexBuffer = null;         this._width = 3.0f;        this._height = 3.0f;        this._depth = 3.0f;         this._x = 50.0f;        this._y = 10.0f;        this._z = 10.0f;         this._rotationMatrix = Matrix.Identity;        this._translationMatrix = Matrix.Identity;        this._scaleMatrix = Matrix.CreateScale(1f, 1f, 1f);    }     public void Dispose()    {        if (this._vertexBuffer != null)            this._vertexBuffer.Dispose();         if (this._indexBuffer != null)            this._indexBuffer.Dispose();         this._indexBuffer = null;        this._vertexBuffer = null;    }      #endregion      #region Initialization     public void Load(GraphicsDevice device)    {        this._device = device;        this.InitializeIndices();        this.InitializeVertices();    }     private void InitializeIndices()    {        this._indexBuffer = new IndexBuffer(            this._device,            typeof(short),            36,            ResourceUsage.WriteOnly,            ResourceManagementMode.Automatic);         this._indexBuffer.SetData(Cube.indices);    }     private void InitializeVertices()    {        VertexPositionColor[] vertices = new VertexPositionColor[8];         vertices[0].Position = new Vector3(0f, 0f, 0f);        vertices[0].Color = this.Color;        vertices[1].Position = new Vector3(0f, 0f, 1f);        vertices[1].Color = this.Color;        vertices[2].Position = new Vector3(1f, 0f, 1f);        vertices[2].Color = this.Color;        vertices[3].Position = new Vector3(1f, 0f, 0f);        vertices[3].Color = this.Color;        vertices[4].Position = new Vector3(1f, 1f, 1f);        vertices[4].Color = this.Color;        vertices[5].Position = new Vector3(1f, 1f, 0f);        vertices[5].Color = this.Color;        vertices[6].Position = new Vector3(0f, 1f, 0f);//        vertices[6].Color = this.Color;        vertices[7].Position = new Vector3(0f, 1f, 1f);        vertices[7].Color = this.Color;         this._vertexBuffer = new VertexBuffer(        this._device,        typeof(VertexPositionColor),        8,        ResourceUsage.WriteOnly,        ResourceManagementMode.Automatic);         this._vertexBuffer.SetData(vertices);    }      #endregion      #region Rendering     /// <summary>    /// <para>Render the cube on the device.</para>    /// </summary>    public void Render()    {        this._device.Vertices[0].SetSource(this._vertexBuffer, 0, VertexPositionColor.SizeInBytes);        this._device.Indices = this._indexBuffer;        this._device.VertexDeclaration = new VertexDeclaration(this._device, VertexPositionColor.VertexElements);        this._device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 8, 0, 12);     }       #endregion      #region Public methods     /// <summary>    /// Sets a size for the cube.    /// </summary>    public void SetRotation(float rotationX, float rotationY, float rotationZ)    {        this._rotationX = rotationX;        this._rotationY = rotationY;        this._rotationZ = rotationZ;         this._rotationMatrix = Matrix.CreateRotationX(rotationX) * Matrix.CreateRotationY(rotationY) * Matrix.CreateRotationZ(rotationZ);        this.UpdateTransformation();    }     /// <summary>    /// Sets a size for the cube.    /// </summary>    public void SetSize(Vector3 size)    {        this._width = size.X;        this._height = size.Z;        this._depth = size.Y;         this._scaleMatrix = Matrix.CreateScale(size.X, size.Y, size.Z);        this.UpdateTransformation();    }     /// <summary>    /// Sets a position for the cube.    /// </summary>    public void SetPosition(Vector3 location)    {        this._x = location.X;        this._y = location.Y;        this._z = location.Z;         this._translationMatrix = Matrix.CreateTranslation(location.X, location.Y, location.Z);        this.UpdateTransformation();    }     private void UpdateTransformation()    {        this._transformationMatrix = this._scaleMatrix * this._rotationMatrix * this._translationMatrix;    }     #endregion }
 

rien de bien compliqué ; il s’agit d’une simple factorisation du code du tutoriel 6. J’ai pris le code de la classe Game1 d’alors pour le copier coller à l’intérieur de cette classe. Si vous regardez le code de la nouvelle classe Game1 vous  verrez une nette amélioration de la lisibilité du code. Notons toutefois que nous utilisons ici une couleur qui peut être spécifiée lors de la création du cube en lieu et place des huit couleurs que nous utilisions jusqu’ici pour chaque sommet du cube.

Le constructeur de Game1 contient désormais une nouvelle instruction :


this._cube = new Cube();

La méthode Initialize ne fait plus appel à this.InitializeIndices();this.InitializeVertices();

mais à


this.InitializeCubes();


qui correspond à la méthode

private void InitializeCubes()
{
    this._cube.Load(this.graphics.GraphicsDevice);

}


La méthode d’affichage dessine le cube en une instruction :


this._cube.Render();


Enfin, les actions utilisateur sur le clavier modifient la taille et la position du code par l’intermédiaire d’appels à des méthodes explicites :

this._cube.SetSize(size);

this._cube.SetPosition(position);


La transformation étant obtenue par l’intermédiaire de la propriété Transformation. Celle-ci se calcule automatiquement à partir des méthodes SetRotation, SetSize et SetPosition.

//modulolong iTime = (long)(gameTime.TotalGameTime.TotalMilliseconds % 2000f);//passage en radianfloat fAngle = iTime * (2.0f * MathHelper.Pi) / 2000.0f; this._cube.SetSize(size);this._cube.SetPosition(position);             //la transformatio en elle mêmeMatrix world = Matrix.CreateRotationY(fAngle)    * Matrix.CreateRotationX(fAngle)    * this._cube.Transformation; 

effect.Parameters[“xWorld”].SetValue(world);



A l’exécution vous obtenez pourtant une application dont le rendu est complètement différent de notre précédent tutoriel :



En fait on peut avoir l’impression au premier abord d’avoir regressé. Au contraire nous avons fait un grand pas en avant ! Notre cube est plus petit tout simplement parcequ’il possède désormais une taille unitaire (1 pour chacun de ses arrètes contre 2 précédemment), afin de facilement spécifier des tailles précises. Il possède une couleur pâle. Ceci est du au fait que nous affectons à chaque vertex la couleur spécifiée par l’utilisateur avant l’appel à la méthode Load du cube. Reportez vous au code de la méthode InitializeVertices pour mieux comprendre :

private void InitializeVertices(){    VertexPositionColor[] vertices = new VertexPositionColor[8];     vertices[0].Position = new Vector3(0f, 0f, 0f);    vertices[0].Color = this.Color;    vertices[1].Position = new Vector3(0f, 0f, 1f);    vertices[1].Color = this.Color;    vertices[2].Position = new Vector3(1f, 0f, 1f);    vertices[2].Color = this.Color;    vertices[3].Position = new Vector3(1f, 0f, 0f);    vertices[3].Color = this.Color;    vertices[4].Position = new Vector3(1f, 1f, 1f);    vertices[4].Color = this.Color;    vertices[5].Position = new Vector3(1f, 1f, 0f);    vertices[5].Color = this.Color;    vertices[6].Position = new Vector3(0f, 1f, 0f);//    vertices[6].Color = this.Color;    vertices[7].Position = new Vector3(0f, 1f, 1f);    vertices[7].Color = this.Color;     this._vertexBuffer = new VertexBuffer(    this._device,    typeof(VertexPositionColor),    8,    ResourceUsage.WriteOnly,    ResourceManagementMode.Automatic);     this._vertexBuffer.SetData(vertices);

}



Enfin pour terminer, le cube ne semble pas tourner sur lui-même mais par rapport à un point correspondant au vertice 0. Là encore tout est normal, le vertice 0 se trouve à l’origine dans le repère 3D à l’intérieur duquel nous créer notre cube (0f, 0f, 0f). C’est par rapport à l’origine d’un objet 3D que se calculent toutes les transformations qui lui sont appliquées. Notre cube se voit rotaté sur lui-même, cette rotation s’effectuera donc par rapport à ce vertice.


Pour terminer  ce point il est plus que necessaire de lire l’article qui se trouve ici, pour bien comprendre l’importance de l’ordre de la multiplication des matrices de transformation (rotation, redimentionnement, translation) pour modifier l’affichage d’un objet à l’écran afin de vous éviter toute surprise et bug de rendu incompréhensible.


La théorie étant acquise, un exercice peaufinera notre pratique : Maintenant que nous avons une classe clé en main pour créer un cube et l’afficher simplement, essayez d’en afficher plusieurs à l’écran.


 


telecharger Vous pouvez télécharger le sample, les deux moteurs et les exercices ici.   


 


New York !


Si vous avez reussi l’exercice précédent, notre premier monde de jeu sera une formalité pour vous. La ville que nous allons créer ici sera plus que rudimentaire. Le sol sera un cube avec une hauteur de 1, les trottoires des pavés applatis avec une hauteur de 5, enfin les batiments seront eux aussi des cubes dont la hauteur sera variable. A ce stade de nos connaissances nous ne pouvons pas ajouter de lumières d’ambiance, pas de texture aux immeubles et pas d’optimisation d’affichage. C’est pourtant un avantage certain puisque dans les articles qui suivront nous améliorerons le code dans ce sens et pourrons mesurer facilement l’importances des notions que nous allons acquerir. Cette ville nous offre aussi la possibilité de bien comprendre comment placer ses objets à l’écran.


Les seules modifications que nous apporterons au programme que nous venons de faire vont porter sur la classe Game1. Nous venons d’énumérer trois types de cube. Commencez donc à déclarer ces cubes au tout début de cette classe :

private Cube _ground;
private Cube[] _trottoires;
private Cube[] _batiments;


 Ajoutez de même un ensemble de constantes qui nous éviterons de remplir notre code de valeur numériques incompréhensibles :

private static int NumberOfBatimentsOnASide = 5;
private static int BatimentSize = 50;
private static int TrottoireSize = 70;
private static int TrottoireHeight = 5;
private static int RoadWidth = 40;
private static int BatimentMinimalHeight = 50;

private static int BatimentMaximalHeight = 450;



Celles ci sont assez explicites pour ne pas avoir à être présentées. Le constructeur instanciera tous les cubes.

_ground = new Cube();
_trottoires = new Cube[NumberOfBatimentsOnASide * NumberOfBatimentsOnASide];
_batiments = new Cube[NumberOfBatimentsOnASide * NumberOfBatimentsOnASide];


Et la méthode InitializeCubes les chargera en mémoire



 

private void InitializeCubes(){    TerrainSize = (RoadWidth + TrottoireSize) * NumberOfBatimentsOnASide + RoadWidth;    Random random = new Random();     //sol    this._ground.Color = Color.Gray;    this._ground.Load(this.graphics.GraphicsDevice);    this._ground.SetSize(new Vector3(        TerrainSize,        TerrainSize,        1));    this._ground.SetPosition(new Vector3(-TerrainSize / 2, -TerrainSize / 2, 0));     //trottoires    for (int i = 0; i < NumberOfBatimentsOnASide; i++)    {        for (int j = 0; j < NumberOfBatimentsOnASide; j++)        {            this._trottoires[i * NumberOfBatimentsOnASide + j] = new Cube();             this._trottoires[i * NumberOfBatimentsOnASide + j].Color = Color.LightGray;            this._trottoires[i * NumberOfBatimentsOnASide + j].Load(this.graphics.GraphicsDevice);            this._trottoires[i * NumberOfBatimentsOnASide + j].SetSize(new Vector3(TrottoireSize, TrottoireSize, TrottoireHeight));             _trottoires[i * NumberOfBatimentsOnASide + j].SetPosition(new Vector3(RoadWidth + j * (TrottoireSize + RoadWidth) – TerrainSize/2 , RoadWidth+(TrottoireSize-BatimentSize)/2 + BatimentSize + i * (TrottoireSize + RoadWidth) – TerrainSize/2 , 0));        }    }     //grattes ciels     for (int i = 0; i < NumberOfBatimentsOnASide; i++)    {        for (int j = 0; j < NumberOfBatimentsOnASide; j++)        {            this._batiments[i * NumberOfBatimentsOnASide + j] = new Cube();             byte grayColorComposante = (byte)random.Next(0, 256);            this._batiments[i * NumberOfBatimentsOnASide + j].Color = new Color(grayColorComposante, grayColorComposante, grayColorComposante);            this._batiments[i * NumberOfBatimentsOnASide + j].Load(this.graphics.GraphicsDevice);            int size = random.Next(BatimentMinimalHeight, BatimentMaximalHeight);            this._batiments[i * NumberOfBatimentsOnASide + j].SetSize(new Vector3(BatimentSize, BatimentSize, size));            this._batiments[i * NumberOfBatimentsOnASide + j].SetPosition(new Vector3(BatimentSize + j * (BatimentSize + RoadWidth + (TrottoireSize – BatimentSize)) – TerrainSize / 2, RoadWidth + (TrottoireSize – BatimentSize) + BatimentSize + i * (TrottoireSize + RoadWidth) – TerrainSize / 2, 0));        }    }}   
Pour chaque cube nous :


  • Spécifions une couleur


  • Chargeons les données 3D en mémoire


  • Spécifions une taille


  • Specifions une position.

La taille et la position sont calculées en fonction des constantes déclarées au début de la classe. Ne reste plus que l’affichage dans la méthode Draw :

for (int i = 0; i < NumberOfBatimentsOnASide; i++){    for (int j = 0; j < NumberOfBatimentsOnASide; j++)    {         effect.Parameters[2].SetValue(this._trottoires[i * NumberOfBatimentsOnASide + j].Transformation);         foreach (EffectPass pass in effect.CurrentTechnique.Passes)        {            pass.Begin();            _trottoires[i * NumberOfBatimentsOnASide + j].Render();            pass.End();        }    }

}



Notons ici que nous spécifions la transformation world avant chaque affichage de cube.

       effect.Parameters[2].SetValue(this._batiments[i * NumberOfBatimentsOnASide + j].Transformation);


La valeur 2 correspond à l’index “xWorld” que nous utilisions précédemment.

La méthode Update met à jour la matrice View pour faire tourner la camera autour de la ville.

    // TODO: Add your update logic here

    int iTime = Environment.TickCount % 10000;    //passage en radian    float fAngle = iTime * (2.0f * (float)Math.PI) / 10000.0f;     // Mettre en place notre matrice de vue.     Matrix view = Matrix.CreateLookAt(new Vector3(500.0f * (float)Math.Cos(fAngle), 500.0f * (float)Math.Sin(fAngle), 510f + 500.0f * (float)Math.Sin(fAngle)), new Vector3(0.0f, -20.0f, 0f), new Vector3(0.0f, 0.0f, 1.0f));     this.effect.Parameters[0].SetValue(view);//0 equivalent de “xView”

Au final à l’affichage nous obtenons :



Comme exercice pour terminer sur ce premier monde de jeu, essayez de modifier la taille des trottoires, l’espacement entre les batiments et, plus difficile deplacer la caméra à l’aide du clavier.


telecharger Vous pouvez télécharger le sample, les deux moteurs et les exercices ici.   


Système solaire 


 Passons maintenant au second moteur de jeu : le système solaire. Nous allons aller ici beaucoup plus loin dans l’utilisation des matrices et d’un modèle objet. Notre but est de créer une vue de l’espace où apparaitra Mercure, Venus, La Terre, Mars et Jupiter. Nous ajouterons de même la Lune. Plusieurs points critiques seront abordés : le développement d’une classe Sphere, sur le modèle de la classe Cube pour afficher … une sphère. Un système d’héritage dans le mesure ou nous allons implémenter des dérivés de cette classe sphere et enfin des calculs Matriciels très poussés.


 A ce propos -NDA : je me repete mais c’est important- il est plus que necessaire de lire l’article qui se trouve ici, pour bien comprendre l’importance de l’ordre de la multiplication des matrices de transformations  (rotation, redimentionnement, translation) pour modifier l’affichage d’un objet à l’écran afin de vous éviter toute surprise et bug de rendu incompréhensible.


 La classe Sphere


La classe sphere est relativement similaire à la classe Cube. Le développeur doit toutefois spécifier deux informations à la création d’une sphere : le nombre d’anneaux (résolution verticale), et le nombre de segments (résolution horizontale). Ces deux données permettent de faire varier la qualité et la douceur des courbes du modèle 3D. Inutile d’afficher ici le code de la classe : une lecture suffit. Les points compliqués se situent dans les deux méthodes de création du VertexBuffer et de l’IndexBuffer. Nous utilisons là un code à base de formules trigonométrique pour générer notre spère. Ce que fait réellement le code là, n’est pas très important…


Maintenant nous sommes pret à créer un modèle objet. Nous suivrons le plan suivant :



Une classe Planet sera créée et héritera de Sphere. Elle implémentera en spécifique 4 Matrices :




  • Une matrice pour la distance entre la planete courante en tant que sattelite et la planete principale (terre<->soleil)


  • Une matrice pour la rotation de la planète sur elle même.


  • Une matrice pour la révolution de la planète courant en tant que sattelite autour de la planète principale.


  • Une matrice résultat qui contiendra le résultat de toutes ces transformation et qui sera appliquée à la matrice World.

 Elle implémentera 2 constantes sur lesquels se baseront les calculs pour les mouvement de chaque planète. Ces constantes correspondrons au temps de rotation de la terre (jour) et au temps de révolution de la terre (année). Elle implémentera une propertie Transformation qui sera une surcharge obscurante de la propriété du même nom héritée. Cette propriété renverra le contenu de la matrice résultat des transformations de la planète courante. Enfin cette classe oblige toutes les classes filles à implémenter une méthode Update. Cette méthode contiendra le code qui mettra à jour cette matrice résultat.


La classe Planet se présente ainsi :

public abstract class Planete : Sphere{    internal Matrix planetMatrix, moveMatrix, rotationMatrix, revolutionMatrix;      public const float DayTime = 10;    public const float YearTime = DayTime * 365.0f;     public new Matrix Transformation    {        get        {            return planetMatrix;        }    }     public Planete()        : base(30, 30)    {     }     public abstract void Update(GameTime gameTime);} 

Septs planètes hériteront de Planete : Sun, Mercure, Venus, Earth, Moon, Mars, Jupiter. Nous étudierons deux d’entre elles : Earth et Mercure. Earth se présente ainsi :

public class Earth : Planete{     public Earth()    {         this.Color = Microsoft.Xna.Framework.Graphics.Color.Blue;    }     public override void Update(GameTime gameTime)    {        rotationMatrix = Matrix.CreateRotationY(Environment.TickCount / DayTime);//une rotation par jour        revolutionMatrix = Matrix.CreateRotationY(Environment.TickCount / YearTime);//365 rotation par an        moveMatrix = Matrix.CreateTranslation(20.0f, 0.0f, 0.0f);//58 milllions de km de distance du soleil        Matrix pivotMatrix = Matrix.CreateRotationZ(23.44f * (float)Math.PI / 180);//obliquité de l’axe de rotation         planetMatrix = Matrix.CreateScale(1f, 1f, 1f) * rotationMatrix * pivotMatrix;//création d’une seule matrice pour tous ces calculs        planetMatrix *= moveMatrix;        planetMatrix *= revolutionMatrix;    }

}


La classe Earth offre une spécificité par rapport à la classe Planete, en donnant une couleur bleue au modèle 3D (dans le constructeur) et en affectant une valeur à chacune de nos matrices (dans la méthode Update).


rotationMatrix = Matrix.CreateRotationY(Environment.TickCount / DayTime);


revolutionMatrix = Matrix.CreateRotationY(Environment.TickCount / YearTime);


moveMatrix = Matrix.CreateTranslation(20.0f, 0.0f, 0.0f);


Matrix pivotMatrix = Matrix.CreateRotationZ(23.44f * (float)Math.PI / 180);


planetMatrix = Matrix.CreateScale(1f, 1f, 1f) * rotationMatrix * pivotMatrix;

planetMatrix *= moveMatrix;planetMatrix *= revolutionMatrix;


La première matrice correspond à la rotation de la terre autour d’elle-même. Nous divisons le temps passé par la constant correspondant à un jour terrestre. La seconde matrice correspond au temps de révolution de la terre autour du soleil. Même calcul mais par rapport à une année terrestre. La troisième matrice spécifie la distance terre / soleil. La matrice suivant spécifie l’obiquité de la terre (la terre est légerement penchée de 23° sur l’axe des pôles). Nous créons ensuite une matrice pour dimensionner la terre (la terre est la planète sur laquelle nous basons nos calculs, donc ici aucun redimentionnement). Les trois dernières instructions multiplient ces matrices entre elles pour affecter la matrice résultat. Si vous avez lu l’article sur l’importance de l’ordre des multiplications de matrices vous devez porter une attention particulière à ces opérations. Après l’instruction :


planetMatrix = Matrix.CreateScale(1f, 1f, 1f) * rotationMatrix * pivotMatrix;



La matrice résultat permet de faire tourner la terre, en lui donnant une obiquité et une taille. La terre ne tourne pas autour du soleil et se trouve au centre de l’univers (haaa Aristote…). Après

planetMatrix *= moveMatrix;

La terre se trouve à une distance de 20 du soleil, tourne sur elle même et possède une taille et une obiquité. Mais elle ne tourne pas autour du soleil (nous nous rapprochons de Galilé toutefois). C’est ce que nous obtenons avec

planetMatrix *= revolutionMatrix;


Nous affectons donc ici une rotation autour d’un cercle imaginaire de 20 de rayon (multiplication précédente d’une matrice de translation de 20 par une matrice de rotation).


Regardons maintenant la classe Mercure.

public class Mercure : Planete{     public Mercure()    {         this.Color = Microsoft.Xna.Framework.Graphics.Color.LightGray;    }      public override void Update(GameTime gameTime)    {        rotationMatrix = Matrix.CreateRotationY(Environment.TickCount / DayTime * 58.6f);//une rotation par jour        revolutionMatrix = Matrix.CreateRotationY(Environment.TickCount / YearTime * 1.6f);//365 rotation par an        Matrix moveMatrix = Matrix.CreateTranslation(7.0f, 0.0f, 0.0f);//58 milllions de km de distance du soleil        Matrix pivotMatrix = Matrix.CreateRotationZ(0.0f * (float)Math.PI / 180);//obliquité de l’axe de rotation        planetMatrix = Matrix.CreateScale(0.4f, 0.4f, 0.4f) * rotationMatrix * pivotMatrix;//création d’une seule matrice pour tous ces calculs        planetMatrix *= moveMatrix;        planetMatrix *= revolutionMatrix;    }

}


Mercure possèdera une couleur gris clair. Nous savons qu’elle tourne atour de son axe en 58,6 jours. Qu’elle effectue 1,6 révolution pendant un an. Nous la plaçons a une distance de 7 du soleil. Elle n’a aucun obiquité. Elle fait 0,4 fois la taille de la terre. Le contenu de la méthode Update coule de source si la méthode Update de Earth a été comprise. Jettez encore un oeil au contenu de la classe Moon. Sa méthode Update suit le même principe que Earth et Mercure et ajoute deux calculs en plus :


 

            planetMatrix *= this._earth.moveMatrix;            planetMatrix *= this._earth.revolutionMatrix;


A ce stade la lune ne se trouve pas autour de la terre mais autour du soleil bien que les calculs précédents soient bons. Nous devons faire une translation correspondant à la distance terre / soleil. C’est ce que fait la première instruction. De même, tout en tournant autour de la terre, la lune tourne forcement autour du soleil puisque la terre le fait. C’est la raison de la seconde instruction affichée ici.


Etudions maintenant le contenu de la classe Game1. Nous commonçons déjà par déclarer les 7 planètes que nous manipulons ici.

private Sun _sun;
private Earth _earth;
private Mercure _mercure;
private Mars _mars;
private Venus _venus;
private Jupiter _jupiter;

private Moon _moon;


Après création, elles sont ajoutées dans une liste générique afin de simplifier le code par la suite.

_sun = new Sun();_earth = new Earth();_mercure = new Mercure();_mars = new Mars();_venus = new Venus();_jupiter = new Jupiter();_moon = new Moon(_earth);_planetes.AddRange(new Planete[] { _sun, _earth, _mercure, _mars, _venus, _jupiter, _moon }); Vient ensuite les étapes traditionnelles de chargement à l’aide de la méthode Load et d’affichage avec Render. Notons que la méthode Update boucle sur toutes les planètes et appelle leur méthode Update pour permettre de mettre à jour leur mouvement.

 A l’exécution vous obtenez :



 Pas très beau, mais très instructif. Là encore, comme pour le moteur de ville, nous l’améliorerons au fil des tutoriaux qui vont suivre. Nous l’utiliserons dans notre apprentissage de la lumière et des effets spéciaux. Voici au final ce que donne notre moteur spacial :


 


 Notre moteur spacial est fini ! Comme exercice, je ne saurais trop vous conseiller d’ajouter Jupiter, et un satellite de Jupiter. De même pourquoi ne pas exporter notre variable effet, au niveau de la classe Sphere ?


 


 telecharger Vous pouvez télécharger le sample, les deux moteurs et les exercices ici.   


Conclusion


La première étape de notre apprentissage est désormais terminée. Si cette étape portait sur les bases du développement 3D, l’étape qui vient portera sur le réalisme. Si vous jetez un oeil aux deux images précédentes (moteur spacial) vous comprendrez que ce mot prend une signification importante en 3D. Ce que nous avons vu jusqu’ici, porte sur des notions qui sont à la base de n’importe quel développement d’application 3D. Bien les comprendre, bien les maitriser, c’est assurer une base stable, saine et reutilisable pour ses futures productions. Nous aborderons maintenant le texturing, les Lumières, les effets de brouillard, le texte en 3D, les particules, les Meshs …


 


telecharger Vous pouvez télécharger le sample, les deux moteurs et les exercices ici.   


 


[Soon]


Valentin


[Help]

Retourner au sommaire des cours