< 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é.
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é.
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
; //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 :

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" :
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.
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 :
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
; //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.
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
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
.Position = new Vector3(0f, 1f, 0f);// vertices
.Color = this.Color; vertices
.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
.Position = new Vector3(0f, 1f, 0f);// vertices
.Color = this.Color; vertices
.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
.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
.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

(je reviendrais dans une annexe sur la manière de faire des mouvements amples et fluides pour ses objets dans une annexe prochainement).
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 vu de loin est belle ...
... 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 2x2 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