Suite aux tutoriels précédents, nous avons appris à créer un cube coloré en 3D. La plupart des jeux utilisent des textures pour rendre plus réalistes leurs formes. Une texture n’est rien d’autre qu’un image que l’on plaque sur des faces 3d.
- Mapping de texture sur une face
Prenons l’exemple d’une face carré créée par deux triangles. Nous avons une texture que l’on souhaite afficher sur cette face. Pour cela, il va falloir relier chaque vertex avec un point de la texture.
Image représentant les coordonnées d’une texture sur deux triangles (tiré du tutoriel XNA de Riemiers)
Ainsi le vertex en haut à gauche est lié au coin haut gauche de la texture. La coordonnées de la texture (aussi appelé UV Mapping) correspondant à ce coin est (0,0). Les coordonnées limites de chaque textures est 1. Ainsi si l’on définit les vertex pour un face, on obtient le code suivant :
new VertexPositionTexture( new Vector3(1 , 1 , -1) ,new Vector2(1,0)), new VertexPositionTexture( new Vector3(1 , -1 , -1) ,new Vector2(1,1)), new VertexPositionTexture( new Vector3(-1 , -1 , -1) ,new Vector2(0,1)), new VertexPositionTexture( new Vector3(-1 , 1 , -1) ,new Vector2(0,0)),
On utilise maintenant une structure VertexPositionTexture pour définir nos vertex contenant la position 3d et la coordonnées de textures. Il suffira de fournir la texture avant l’appel du Draw pour que la carte graphique affiche la texture.
- Texturer un cube avec 6 textures
Nous allons reprendre l’exemple du tutoriel précédent et nous allons appliquer une texture différente pour chaque face. Pour cela, il est nécessaire de rajouter 6 images dans le projet de contenu (pour l’exemple, nous avons pris des images dans le dossier Sample de Windows 7).
Texture2D[] faces; protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); this.effect = new BasicEffect(this.GraphicsDevice); this.faces = new Texture2D[6]; this.faces[0] = Content.Load<Texture2D>("Chrysanthemum"); this.faces[1] = Content.Load<Texture2D>("Desert"); this.faces[2] = Content.Load<Texture2D>("Hydrangeas"); this.faces[3] = Content.Load<Texture2D>("Lighthouse"); this.faces[4] = Content.Load<Texture2D>("Penguins"); this.faces[5] = Content.Load<Texture2D>("Tulips"); }
Nous chargeons chacune des textures dans un tableau. Nous créons ensuite une méthode pour créer l’ensemble des données du cube.
VertexPositionTexture[] verticesTex; private void CreateIndexedCubeTextured() { this.verticesTex = new VertexPositionTexture[] { new VertexPositionTexture( new Vector3(1 , 1 , -1) ,new Vector2(1,0)), new VertexPositionTexture( new Vector3(1 , -1 , -1) ,new Vector2(1,1)), new VertexPositionTexture( new Vector3(-1 , -1 , -1) ,new Vector2(0,1)), new VertexPositionTexture( new Vector3(-1 , 1 , -1) ,new Vector2(0,0)), new VertexPositionTexture( new Vector3(1 , 1 , 1) ,new Vector2(1,0)), new VertexPositionTexture( new Vector3(-1 , 1 , 1) ,new Vector2(1,1)), new VertexPositionTexture( new Vector3(-1 , -1 , 1) ,new Vector2(0,1)), new VertexPositionTexture( new Vector3(1 , -1 , 1) ,new Vector2(0,0)), new VertexPositionTexture( new Vector3(1 , 1 , -1) ,new Vector2(1,0)), new VertexPositionTexture( new Vector3(1 , 1 , 1) ,new Vector2(1,1)), new VertexPositionTexture( new Vector3(1 , -1 , 1) ,new Vector2(0,1)), new VertexPositionTexture( new Vector3(1 , -1 , -1) ,new Vector2(0,0)), new VertexPositionTexture( new Vector3(1 , -1 , -1) ,new Vector2(1,0)), new VertexPositionTexture( new Vector3(1 , -1 , 1) ,new Vector2(1,1)), new VertexPositionTexture( new Vector3(-1 , -1 , 1) ,new Vector2(0,1)), new VertexPositionTexture( new Vector3(-1 , -1 , -1) ,new Vector2(0,0)), new VertexPositionTexture( new Vector3(-1 , -1 , -1) ,new Vector2(1,0)), new VertexPositionTexture( new Vector3(-1 , -1 , 1) ,new Vector2(1,1)), new VertexPositionTexture( new Vector3(-1 , 1 , 1) ,new Vector2(0,1)), new VertexPositionTexture( new Vector3(-1 , 1 , -1) ,new Vector2(0,0)), new VertexPositionTexture( new Vector3(1 , 1 , 1) ,new Vector2(1,0)), new VertexPositionTexture( new Vector3(1 , 1 , -1) ,new Vector2(1,1)), new VertexPositionTexture( new Vector3(-1 , 1 , -1) ,new Vector2(0,1)), new VertexPositionTexture( new Vector3(-1 , 1 , 1) ,new Vector2(0,0)) }; this.indices = new int[] { 0, 3, 2, 0, 2, 1, 4, 7, 6, 4,6, 5, 8, 11, 10,8,10, 9, 12, 15, 14, 12, 14, 13, 16, 19, 18,16,18, 17, 20, 23, 22,20,22, 21 }; this.vb = new VertexBuffer(this.GraphicsDevice, typeof(VertexPositionTexture), this.verticesTex.Length, BufferUsage.WriteOnly); vb.SetData(this.verticesTex); this.ib = new IndexBuffer(this.GraphicsDevice, IndexElementSize.ThirtyTwoBits, this.indices.Length, BufferUsage.WriteOnly); this.ib.SetData(this.indices); this.GraphicsDevice.SetVertexBuffer(this.vb); }
Remarque : Afin de simplifier, j’ai choisi de créer une méthode pour chaque type de façon de créer des cubes. De plus j’ai aussi choisi de créer une propriété verticesTex pour les données sur les vertices texturées.
La seule différence avec le tutoriel précédent est le remplacement des couleurs par les coordonnées de textures. Les différences principales se situent au niveau de l’affichage.
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); effect.View = View; effect.Projection = Projection; effect.World = World * Matrix.CreateRotationY(RotateY) * Matrix.CreateRotationZ(RotateZ); effect.VertexColorEnabled = false; effect.TextureEnabled = true; for (int i = 0; i < 6; i++) { effect.Texture = this.faces[i]; foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); this.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, this.verticesTex, 0, this.verticesTex.Length, this.indices, i * 6, 2); } }
base.Draw(gameTime);
}
Premier changement, la désactivation des couleurs des vertex et l’activation des textures sur le BasicEffect. Cela indique qu’il faudra texturer les triangles que nous allons dessiner. Afin de pouvoir d’appliquer une texture par faces, il va être nécessaire de dessiner chacune des face séparément. Chacune des itérations de la boucle sera donc une face du cube comprenant 2 triangles. Pour chacune de ces faces, on définit la texture que l’on souhaite appliquer en remplissant la propriété Texture de Effect.
Remarque : J’utilise la méthode DrawUserIndexPrimitives ici sans raison particulière. La méthode DrawIndexPrimitives permet aussi l’affichage des textures :). Concernant l’avant dernier paramètre, le i * 6 correspond à la position de départ dans la table des index. I représente le numéro de la face et 6 représente le nombre d’indices par face (6 car 2 triangles à 3 indices). Le dernier paramètre indique que l’on souhaite afficher deux triangles. Pour plus d’informations sur les différents paramètres des méthodes Draw*(), voir le tutoriel précédent.
Remarque 2 : J’ai rajouté une deuxième variable de rotation afin de pouvoir visualiser plus de faces. Le code modifié est le suivant
float RotateZ = 0.0f; float RotateY = 0.0f; protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) this.Exit(); RotateZ += MathHelper.ToRadians(1.0f); RotateY += MathHelper.ToRadians(3.0f); base.Update(gameTime); }
Lorsque vous exécutez le projet vous devriez trouver le résultat suivant :
- Texturer un cube avec 1 texture
Une autre méthode pour texturer un objet 3D est d’utiliser une seule texture pour l’ensemble de l’objet. Cette texture fera l’office de patron comme cela se fait en couture ou en dessin. Pour notre exemple, nous allons créer un dé. Pour cela on utilise une texture patron comme celle-ci.
Comme vous pouvez le remarquer, les 6 faces du dé sont représentées sur cette texture. C’est grâce aux coordonnées de textures de nos vertex que nous allons appliquer des morceaux de textures aux faces. Ainsi, si l’on prend la face contenant cinq rond, ses coordonnées de textures vont de (0.666;0.333) à (1.000,0.666). Connaissant cela, nous allons recréer une nouvelle fois nos vertices pour cette texture particulière.
private void CreateIndexedCube3DTextured() { this.verticesTex = new VertexPositionTexture[] { new VertexPositionTexture( new Vector3(1 , 1 , -1) , new Vector2(0.666f,0.333f)), new VertexPositionTexture( new Vector3(1 , -1 , -1) , new Vector2(0.666f,0.666f)), new VertexPositionTexture( new Vector3(-1 , -1 , -1), new Vector2(0.333f,0.666f)), new VertexPositionTexture( new Vector3(-1 , 1 , -1) , new Vector2(0.333f,0.333f)), new VertexPositionTexture( new Vector3(1 , 1 , 1) , new Vector2(0.333f,0.333f)), new VertexPositionTexture( new Vector3(-1 , 1 , 1) , new Vector2(0.333f,0.666f)), new VertexPositionTexture( new Vector3(-1 , -1 , 1) , new Vector2(0,0.666f)), new VertexPositionTexture( new Vector3(1 , -1 , 1) , new Vector2(0,0.333f)), new VertexPositionTexture( new Vector3(1 , 1 , -1) , new Vector2(0.666f,0.666f)), new VertexPositionTexture( new Vector3(1 , 1 , 1) , new Vector2(0.666f,1)), new VertexPositionTexture( new Vector3(1 , -1 , 1) , new Vector2(0.333f,1)), new VertexPositionTexture( new Vector3(1 , -1 , -1) , new Vector2(0.333f,0.666f)), new VertexPositionTexture( new Vector3(1 , -1 , -1) , new Vector2(0.666f,0)), new VertexPositionTexture( new Vector3(1 , -1 , 1) , new Vector2(0.666f,0.333f)), new VertexPositionTexture( new Vector3(-1 , -1 , 1) , new Vector2(0.333f,0.333f)), new VertexPositionTexture( new Vector3(-1 , -1 , -1), new Vector2(0.333f,0)), new VertexPositionTexture( new Vector3(-1 , -1 , -1), new Vector2(1,0.333f)), new VertexPositionTexture( new Vector3(-1 , -1 , 1) , new Vector2(1,0.666f)), new VertexPositionTexture( new Vector3(-1 , 1 , 1) , new Vector2(0.666f,0.666f)), new VertexPositionTexture( new Vector3(-1 , 1 , -1) , new Vector2(0.666f,0.333f)), new VertexPositionTexture( new Vector3(1 , 1 , 1) , new Vector2(1,0.666f)), new VertexPositionTexture( new Vector3(1 , 1 , -1) , new Vector2(1,1)), new VertexPositionTexture( new Vector3(-1 , 1 , -1) , new Vector2(0.666f,1)), new VertexPositionTexture( new Vector3(-1 , 1 , 1) , new Vector2(0.666f,0.666f)) }; this.indices = new int[] { 0, 3, 2, 0, 2, 1, //1 4, 7, 6, 4,6, 5, //2 8, 11, 10,8,10, 9, //3 12, 15, 14, 12, 14, 13, //4 16, 19, 18,16,18, 17, //5 20, 23, 22,20,22, 21 //6 }; this.vb = new VertexBuffer(this.GraphicsDevice, typeof(VertexPositionTexture), this.verticesTex.Length, BufferUsage.WriteOnly); vb.SetData(this.verticesTex); this.ib = new IndexBuffer(this.GraphicsDevice, IndexElementSize.ThirtyTwoBits, this.indices.Length, BufferUsage.WriteOnly); this.ib.SetData(this.indices); this.GraphicsDevice.SetVertexBuffer(this.vb); }
Nous allons aussi charger la texture avec le code suivant (cube_dice étant le nom de la texture ajouté dans le dossier contenu) :
Texture2D[] faces; Texture2D TexDice; protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); this.effect = new BasicEffect(this.GraphicsDevice); this.faces = new Texture2D[6]; this.faces[0] = Content.Load<Texture2D>("Chrysanthemum"); this.faces[1] = Content.Load<Texture2D>("Desert"); this.faces[2] = Content.Load<Texture2D>("Hydrangeas"); this.faces[3] = Content.Load<Texture2D>("Lighthouse"); this.faces[4] = Content.Load<Texture2D>("Penguins"); this.faces[5] = Content.Load<Texture2D>("Tulips"); this.TexDice = Content.Load<Texture2D>("cube_dice"); }
C’est encore au niveau de la fonction d’affichage que les choses vont changer :
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); effect.View = View; effect.Projection = Projection; effect.World = World * Matrix.CreateRotationZ(RotateZ) * Matrix.CreateRotationY(RotateY); effect.VertexColorEnabled = false; effect.TextureEnabled = true; effect.Texture = this.TexDice; foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); this.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, this.verticesTex, 0, this.verticesTex.Length, this.indices, 0, this.indices.Length / 3); } base.Draw(gameTime); }
Nous n’avons plus besoin de séparer chacun des faces car nous n’allons utiliser qu’une seule texture et que ce sont les coordonnées de textures des vertices qui vont sélectionner les morceaux de textures pour chaque face. Il suffit d’affecter la texture au BasicEffect et appelé la méthode Draw comme d’habitude. Le résultat est le suivant :
- Pourquoi utiliser une texture “3D” plutôt que 6 textures ?
La première raison peut être la taille. En effet, 6 textures sont souvent plus grosses qu’une seule. De plus la création de 6 texture2D va faire augmenter la mémoire en RAM. L’exemple du patron du dé n’est pas un bon exemple car il y a de nombreux blancs dans ce patron ce qui peut la rendre plus grosse que 6 textures. Mais dans les jeux un peu plus poussés les textures ressemblent parfois à ça :
Remarque : ces images sont tirées du blog : http://alexismigdalski.wordpress.com/category/dernieres-publications/3d/. Il est à noter que ce type de texture peut être générer par un modeleur 3D après que vous ayez dessiné vous-même sur votre modèle 3D.
L’autre avantage de la texture “3d” est qu’il n’y a qu’un seul appel à la méthode Draw. Cela évite donc les dialogues entre GPU/CPU. Cela permet souvent de gagner en performances.
- Conclusion
Vous savez maintenant utiliser des textures pour vos modèles 3D. Nous verrons dans le prochain tutoriel comment charger un modèle 3D provenant d’un modeleur 3d et comment utiliser les lumières fournis par BasicEffect.