Astuces DotNet (Sébastien Courtois)

07/10/2010

[XNA 4] Tutoriel 3D 3 : Affichage de textures sur un objet 3D

Filed under: .NET, 3D, C# 4, Débutant, Windows Phone, XBOX 360, XNA — Étiquettes : , , , , , , , — sebastiencourtois @ 14:24

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.

xna_tuto3d3_3 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 :

xna_tuto3d3_4

  • 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.

cube_dice

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 :

xna_tuto3d3_5

  • 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 :

xna_tuto3d3_6a

xna_tuto3d3_6b

Texture “3D”

Modèle 3d + Texture 3d

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.

Source Code de ce tutoriel

12 commentaires »

  1. salut a toi ! tes tuto sont vraiment super. c’est du très bon travail.
    J’aimerais te demander une petite chose : je souhaiterais utiliser le dé que je viens de créer dans une classe a part, comment dois-je procéder ?

    Commentaire par geoffrey berton — 11/04/2011 @ 17:43

    • Une solution serait de créer une méthode Initialize/Update/Draw dans ta classe Dé et de les appeler depuis ton programme principale (a la limite tu peux toujours hériter de DrawableGameComponent mais il parait que les performances sont assez mauvaises). Apres cela dépend de l’utilisation que tu compte faire de ta classe et de l architecture de ton programme.

      Commentaire par sebastiencourtois — 11/04/2011 @ 17:54

  2. Salut! j’ai un petit probleme si tu pouvais m’aider =). « this.TexDice = Content.Load(« cube_dice »); », visual me met qu’il y a un probleme dans cette ligne, qu’il ne peut pas convertir TexDice de Texture2D en Texture2D[]. Que dois-je faire?

    Commentaire par Vince — 09/11/2011 @ 20:06

    • Normalement TexDice doit etre en Texture2D et non pas en Texture2D[]. « cube_dice » est une seule texture meme si elle contient les 6 faces.

      Commentaire par sebastiencourtois — 09/11/2011 @ 20:10

  3. Ca j’avais compris mais visual lui ne comprend pas =/

    Commentaire par Vince — 09/11/2011 @ 21:06

    • C’est bon en faite j’ai trouver mon erreur j’avais oublier de diviser indices.Length par 3

      Commentaire par Vince — 09/11/2011 @ 21:13

  4. Salut ! Merci déjà de faire partager tes connaissances!
    J’ai un problème dans ton tuto … Là où il faut juste poser une texture sur une face avec une image (tulipes, manchots… etc)
    Sur cette ligne: « this.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, this.verticesTex, 0, this.verticesTex.Length, this.indices, i * 6, 2); »
    Il génère la solution, il se lance et après j’ai un « L’exception NullReferenceException n’a pas été gérée »
    Merci d’avance

    Commentaire par MARTINET — 16/12/2011 @ 11:23

    • Salut,

      Je viens de restester la solution. Elle fonctionne sans probleme chez moi pour une texture par face.
      Si tu telecharge le code source en fin d’article, tu aura un cube avec une seule texture. Pour passer correctement en une texture par face, il faut faire deux choses.
      1) Ligne 43/44 : Commenter this.CreateIndexedCube3DTextured(); et decommenter this.CreateIndexedCubeTextured();
      2) Ligne 340 / 362 : Decommenter les lignes 342 a 351 (Zone « Cube Texture ») et Commenter les lignes 354 a 361 (Zone « Cube De »)

      Si tu as toujours le probleme, verifie si les variables « vertices », « verticesTex » ou « indices » sont pas null. Si c’est le cas, verifie l’endroit ou les variables sont initialises.

      Ces tutoriaux commencent a dater et je m’apercois qu’il y a des « erreurs ». Par exemple, plutot que d’utiliser le this.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, this.verticesTex, 0, this.verticesTex.Length, this.indices, i * 6, 2), tu peux utiliser une methode plus rapide : this.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, this.verticesTex.Length, i * 6, 2);

      Commentaire par sebastiencourtois — 16/12/2011 @ 18:13

      • Ouais merci! J’avais effectivement relus mon programme et donc réussi à le faire marcher. J’ai pas pu effacer mon message mais en tout cas merci d’avoir répondu.
        Sinon j’aimerai vraiment continuer à apprendre le jeu en 3D, sur une autre page tu dis que XNA commence à se faire dépasser par silverlight.. Donc si tu pouvais m’orienter vers des docs de jeux 3D et sur ce qui se fait de mieux je suis preneur, et tes conseils me seront des plus utiles😉

        Commentaire par MARTINET — 22/12/2011 @ 22:29

      • Ce que je dis et que je pense est que XNA et Silverlight sont des technologies mortes a moyen terme. Silverlight sera remplace au profit de HTML5 et XNA va disparaitre petit a petit pour devenir peut etre quelque chose d autres pour la sortie de la prochaine XBOX.

        De toute facon, le jeu 3d passe par le C++ et le DirectX ou OpenGL (et derive mobile).XNA est un bon marche pied car il cache beaucoup de chose et permet d’apprendre en douceur.

        Comme dit dansd mes differents commentaires, j’ai arrete le XNA pour me concentrer sur le DirectX. Je suis en train de travailler sur un moteur de jeu DirectX 11 / PhysX ecrit en C# .NET. Le projet est sur codeplex meme s’il est pas encore public (Nom : Orion Game Engine). Niveau article, je fais une pause actuellement pour bien apprehender tout ca et je reviendrais avec des articles sur l’architecture de moteur de jeu, la 3d en DirectX … et surement un jeu 3d.

        Si tu veux continuer sur XNA, je te conseille le site : http://xna-uk.net/blogs/

        Commentaire par sebastiencourtois — 22/12/2011 @ 22:38

  5. bonjour,
    est-ce que c’est possible d’utiliser ces création 3d faite avec blender sous xna ?
    parce que je souhaite faire un jeux de voiture et je me vois mal donner toute les coordonnée ( voiture, circuit, ciel, decord …… )

    Commentaire par mokmap — 18/12/2011 @ 12:56

    • Tout a fait. Il suffit de les exporter dans un format gerer en natif par XNA (le .FBX de Autodesk par exemple). Pour cela, tu fais File>Exporter>Autodesk FBX dans Blender.
      Pour importer dans XNA, tu l’ajoute dans la library Content de ta solution XNA.
      Si jamais tu veux utiliser un autre format, tu dois ajouter des library a XNA ou creer ta propre library (ce qui peut etre un peu complexe).

      Commentaire par sebastiencourtois — 18/12/2011 @ 19:57


RSS feed for comments on this post. TrackBack URI

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

Propulsé par WordPress.com.

%d blogueurs aiment cette page :