Astuces DotNet (Sébastien Courtois)

16/05/2011

[XNA/DirectX/OpenGL] Les bases de la programmation 3D

Filed under: 3D, Débutant, DirectX, Intermediaire, Jeux vidéos, OpenGL, Windows Phone, XBOX 360, XNA — Étiquettes : , , , , , , , , — sebastiencourtois @ 09:00

Je n’ai pas été très bavard ces derniers temps (mon dernier post remonte à 5 mois… Confus). Une des mes excuses est mon départ de France pour travailler pour une société américaine à San Francisco. Je suis bien installé maintenant et je vais me remettre à bloguer un peu plus Sourire.

Cela fait quelques temps que je fais des posts sur la 3D mais je n’arrivais jamais à expliquer les bases de la programmation 3D de façon simple et claire. Au travers de mes recherches dans ce domaine, je suis tombé sur le site de Bobby Anguelov. Plutôt spécialisé en Intelligence Artificielle, il a écrit des cours très agréables à lire sur DirectX 10 en C++. Il a aussi été enseignant à l’université de Pretoria (Afrique du Sud). C’est un de ses cours que j’ai décidé de traduire et de publier ici.

Pourquoi ? :

  • Le cours est claire, simple et bien illustré. Pourquoi refaire quelque chose qui a déjà été bien fait par quelqu’un d’autre ? Sourire
  • Le cours est vraiment grand public/débutant. Il suffit d’aimer la 3D et de connaitre les bases de la programmation pour pouvoir lire ce cours.
  • Le cours est plutôt générique et les informations contenues dans ces documents peuvent aussi s’appliquer à toutes les technologies 3D (DirectX/OpenGL/XNA) ==> Il n’y a aucun code source dans ce cours.

Note : La traduction a été complexe car de nombreux termes sont les termes utilisés dans les blogs/forums … Par conséquent, j’ai essayé un maximum de conserver ces termes tout en fournissant les traductions françaises.

Le cours se découpe en 6 chapitres :

  1. Introduction et historique de la 3D (30 Slides)
  2. Mathématiques appliquées à la 3D (37 Slides)
  3. Le pipeline graphique (27 Slides)
  4. L’étape applicative du pipeline graphique (23 Slides)
  5. L’étape géométrique du pipeline graphique (24 Slides)
  6. L’étape rastérisation du pipeline graphique (84 Slides)

Le cours complet en francais est disponible en format zip.

Si vous préférez la version anglaise, vous pourrez trouver les cours à cette adresse : http://takinginitiative.net/computer-graphics-course-slides/

NOTE : Si vous comptez utiliser ces documents pour les publier ou les enseigner, merci de nous prévenir. Nous ne voulons pas de droit d’auteurs sur ces documents Sourire mais nous souhaitons juste savoir où sont utilisés ces informations (par fierté Sourire) et être cité (lien vers nos blogs …). Si vous souhaitez la version Powerpoint, contactez nous (commentaire sur ce blog ou celui de Bobby).

  • La suite … ?

J’ai pas mal d’idées de posts pour la suite. Les statistiques de ce blog montrent que XNA 4 est très populaire en ce moment et j’envisage deux tutoriaux sur le HLSL en XNA 4. Je vais continuer les posts sur DirectX. Toutefois, je vais me focaliser sur DirectX 11. Enfin, j’ai récupérer le SDK de PhysX 3 (sorite ce mois ci) et je vais faire des cours d’introduction à cette API physique très populaire dans les jeux vidéos. Je vais aussi voir pour des tutoriaux sur le Cry Engine 3 quand le SDK sera publique (Août aux dernières nouvelles).

En ce qui concerne XNA sur Silverlight 5…. Je ferais des tutoriaux lors de la sortie finale car l’API actuelle n’est pas encore complète (fin d’année je pense).

N’hésitez pas à mettre des commentaires sur ce cours et si vous avez des idées de tutoriaux/sujets que vous voudriez voir approfondis.

Publicités

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

06/10/2010

[XNA 4] Tutoriel 3D 2 : Cube / VertexBuffer / IndexBuffer

Après avoir joué avec un triangle lors du tutoriel précédent, nous allons maintenant voir comment faire un cube coloré. Nous allons aussi étudier la gestion des vertices au sein de la carte graphique et comment optimiser l’affichage et le rendu 3d.

Nous reprendrons le résultat du dernier tutoriel pour commencer celui-ci.

  • Au commencement était le cube…

Nous allons commencer par un petit rappel sur la définition mathématique d’un cube. Un cube est une forme géométrique à 3 dimensions comprenant 6 faces, 12 arêtes et 8 sommets.

180px-Hexahedron

Comme vous pouvez le voir sur l’image ci-dessus, les faces mathématiques sont des carrés. Or XNA ne permet de définir que des triangles. Il faudra compter donc 2 triangles par face pour notre cube. Cela donnera quelque chose comme cela.

xna_tuto3d2_1

Comme indiqué lors de l’introduction de cet article, nous souhaitons réaliser un cube coloré. Chaque face ayant une couleur uni et différente des autres faces or, en XNA, chaque sommet a une position et une couleur unique. Un sommet d’un cube est commun à trois faces. Mais on ne peut pas, avec un seul sommet, assigné trois couleurs différentes. il sera donc nécessaire de créer, pour un même sommet mathématique, 3 vertices (un pour chaque face auquel il est relié). On passe donc de 8 sommets mathématiques à 36 vertices.

Nous allons créer une nouvelle méthode CreateCubeColored afin de créer l’ensemble des vertices.

private void CreateCubeColored()
{
    this.vertices = new VertexPositionColor[]
    {
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Red), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Red), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Red), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Red), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Red), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Red), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Green), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Green), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Green), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Green), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Green), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Green), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Purple), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Yellow), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Yellow), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Yellow), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Yellow), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Yellow), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Yellow), 
    };
}

Remarque : l’ordre des vertices est important car dépendant du backface culling expliqué dans le tutoriel précédent. L’écriture de ces définitions de vertices peut être fastidieux à la longue. Il est à noter que cela est rarement utilisé. On préfère utiliser des modeleurs 3D (type Blender, Maya ou 3DSMAX) pour réaliser des modèles fournissant directement ces données.

On modifie maintenant la méthode Initialize pour appeler la méthode CreateCubeColored.

protected override void Initialize()
{
    //this.CreateTriangle();
    this.CreateCubeColored();
    this.CreateCamera();

    base.Initialize();
}

Puis on réalise l’affichage du cube de la même façon que pour le triangle (sans changer une ligne de code).

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateZ);
    effect.VertexColorEnabled = true;

    foreach(EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
    }
    base.Draw(gameTime);
}

Vous obtenez un Cube coloré qui tourne sur lui-même.

xna_tuto3d2_2

  • Gestion de la mémoire : VertexBuffer

Lorsque nous créons un tableau de vertices, celui-ci est stocké au niveau de la RAM du microprocesseur (CPU). Lors de l’appel à la méthode DrawUserPrimitives, nous fournissons le tableau de vertices. La méthode va chercher en RAM les données pour les copier sur la mémoire de la carte graphique puis faire le rendu adéquat. Or, vu que la méthode DrawUserPrimitives est appelée 60 fois par secondes, cette copie à lieu un grand nombre de fois inutilement car les données n’ont pas changé.

Pour pallier ce problème, il existe une classe VertexBuffer chargé de copier une fois pour toute les données des vertices sur la mémoire de la carte graphique et de les réutiliser directement.

Pour réaliser créer le VertexBuffer, il suffit de modifier CreateCubeColored.

private VertexBuffer vb;

private void CreateCubeColored()
{
    this.vertices = new VertexPositionColor[] { /* Données du cube */ };
    this.vb = new VertexBuffer(this.GraphicsDevice, typeof(VertexPositionColor), this.vertices.Length, BufferUsage.WriteOnly);
    vb.SetData(this.vertices);
    this.GraphicsDevice.SetVertexBuffer(this.vb);
}

Le constructeur du VertexBuffer prend en paramètre le lien vers la carte graphique, le type de vertex utilisé, le nombre de vertices à stocker ainsi que l’utilisation que l’on compte faire du VertexBuffer. Ce dernier paramètre peut avoir deux valeurs : None (par défaut) ou WriteOnly (optimisé pour l’écriture et la modification des données). Une fois créé, on remplit le VertexBuffer avec le tableau vertices puis on fournit le VertexBuffer à la carte graphique grâce à la méthode SetVertexBuffer en vue de la copie des données.

Au niveau de l’affichage, on va remplacer la méthode DrawUserPrimitives par DrawPrimitives.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateZ);
    effect.VertexColorEnabled = true;

    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        //this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
        this.GraphicsDevice.DrawPrimitives      (PrimitiveType.TriangleList,           0, this.vertices.Length / 3);
    }
    base.Draw(gameTime);
}

Les deux méthodes ont une seule différence dans leur paramètre. Le deuxième paramètre de DrawUserPrimitives n’existe plus dans DrawPrimitives. Ce paramètre servait à fournir le tableau de vertices. Cela n’est plus nécessaire car DrawPrimitives va chercher les données sur les vertices dans le VertexBuffer défini précédemment.

On obtient le même résultat que précédemment si on exécute l’application.

  • IndexBuffer : Optimisation mémoire des vertices

Si vous regarder la liste des vertices décrivant le cube, vous remarquerez que certains sont redondant (le [0] et le [3], le [2] et le [4] …). XNA fournit un moyen de réduire ces redondances en ne fournissant qu’un seul vertex qui sera réutilisé par plusieurs faces. Pour cela, on a besoin de deux tableaux : un tableau de vertices “uniques” et un tableau d’index qui va permettre de lié les vertices et les faces.

int[] indices;

private void CreateIndexedCubeColored()
{
    this.vertices = new VertexPositionColor[]
    {
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Red),
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Red),
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Red),
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Red),
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Green),
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Green),
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Green),
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Green),
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Orange),
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Orange),
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Orange),
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Orange),
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Purple),
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Purple),
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Purple),
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Purple),
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Yellow),
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Yellow),
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Yellow),
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Yellow)
    };

    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            
    };

}

La nouvelle méthode CreateIndexedCubeColored créé un tableau de 24 vertices uniques et un tableau d’indices liant les triangles aux vertices. Ainsi le premier triangle sera les trois premières valeurs du tableau d’indices (0,3,2). Cela permet de réduire les redondances et d’alléger la mémoire.

Méthode sans IndexBuffer : 36 vertices de 16 octets => 576 octets

Méthode avec IndexBuffer : 24 vertices de 16 octets + 144 octets d’indices => 528 octets (ou 24*16+72 => 456 octets si on utilise des short à la place de int pour les indexs).

Même si la différence ne parait pas énorme sur un cube, il faut imaginer qu’elle est très grande quand les vertices/modèles deviennent très complexe.

Pour l’affichage de données indexés, il est nécessaire comme pour les vertex d’utiliser un VertexBuffer et un IndexBuffer.

VertexBuffer vb;
IndexBuffer ib;
int[] indices;

private void CreateIndexedCubeColored()
{
    this.vertices = new VertexPositionColor[]  {  /*Vertices*/ };

    this.indices = new int[]  { /*Indices*/ };

    this.vb = new VertexBuffer(this.GraphicsDevice, typeof(VertexPositionColor), this.vertices.Length, BufferUsage.WriteOnly);
    vb.SetData(this.vertices);
    this.GraphicsDevice.SetVertexBuffer(this.vb);

    this.ib = new IndexBuffer(this.GraphicsDevice, IndexElementSize.ThirtyTwoBits, this.indices.Length, BufferUsage.WriteOnly);
    this.ib.SetData(this.indices);
    this.GraphicsDevice.Indices = this.ib;

}

L’indexBuffer se crée de la même façon que le VertexBuffer. La seule différence se situe sur le deuxième paramètre où il est nécessaire d’indiquer le type de données utilisée pour les index. Deux possibilités : 32 bits (int) ou 16 bits (short). Le choix est ouvert et dépend du nombre de vertices utilisés (short est compris entre –32768 à +32768.) et de la technologie sous-jacente. Le Windows Phone, par exemple, ne gère que des indexs 16 bits.

Une fois les VertexBuffer et IndexBuffer configurés et liés à la carte graphique, il ne reste plus qu’à afficher le nouveau cube.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateZ);
    effect.VertexColorEnabled = true;

    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        //this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
        //this.GraphicsDevice.DrawPrimitives      (PrimitiveType.TriangleList,           0, this.vertices.Length / 3);
        this.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, this.vertices.Length, 0, this.indices.Length / 3);
    }
    base.Draw(gameTime);
}

On utilise cette fois la méthode DrawIndexedPrimitives. Le premier paramètre reste le type de primitives à dessiner. Le dernier paramètre correspond toujours au nombre de primitives (triangles) à dessiner.

Les 4 paramètres intermédiaires se nomment baseVertex,minVertexIndex, numVertices et startIndex et leur fonctionnement est un peu plus complexe.

Le paramètre baseVertex permet de décaler la valeur des indices d’une constante. Ainsi si on a un tableau d’indices {0,2,3} et que l’on met baseVertex à 30; durant le temps de l’appel DrawIndexPrimitives, le tableau d’indices va devenir {30,32,33}. Dans notre cube, on laisse cette valeur à 0 car nos indices sont déjà aux bonnes valeurs.

fig4

Exemple de l’affichage de deux triangles à partir d’un index Buffer (tiré  du blog de John Steed).

Prenons l’exemple ci-dessus d’un index buffer contenant deux triangles. Si l’on souhaite n’afficher que le deuxième, on mettra la valeur de startIndex à 3 (startIndex représentant la position de début du triangle dans l’indexbuffer). MinIndex représente la valeur d’index minimale utilisé (ici les valeurs d’index pour le triangle sont 3,2,0 => Minimun : 0). Enfin la plage d’index pour ce triangle est 0=>3. Par conséquent le nombre de vertices qui seront potentiellement utilisable est de 4. En effet, on prend aussi les vertices contenues dans la plage même s’ils ne sont pas utile (ici, le vertex 1 n’est pas utile pour le dessin du triangle 2).

Pour une explication peut être plus claire et en anglais sur ces paramètres, je vous conseille ce post.

  • Et les performances dans tout ça…

J’ai rajouté deux compteurs de FPS afin d’analyser les différences de performances entre les différentes méthodes. Les résultats pour le cube ont été sensiblement équivalents pour l’ensemble des méthodes. Toutefois, il ne faut pas en conclure que les VertexBuffer/IndexBuffer sont inutiles. En effet, dans le cas de modèles importants, le temps et la mémoire gagnés par ces mécanismes permet une amélioration sensible des performances.

Pour savoir la méthode qui convient le mieux à votre projet, vous n’avez pas d’autres moyens que de tester et mesurer les performances pour chacune des méthodes.

  • Conclusion

Après le triangle, nous avons vu comment créer un cube en optimisant les données fournies à la carte graphique. Dans le prochain tutoriel, nous allons créer un dé en texturant notre cube.

Code Source de ce tutoriel

16/09/2010

[XNA 4] Tutoriel 2 : Gestion des images (2D)

Filed under: .NET, Débutant, Windows Phone, XBOX 360, XNA — Étiquettes : , , , , , , , , — sebastiencourtois @ 16:34

Remarque : Ce tutorial fait suite au Tutorial 1 et reprend le projet utilisé dans ce dernier.

Nous allons voir dans ce tutorial, comment charger, afficher et déplacer des images (aussi appelé sprites). Vous trouverez ici les images utilisées pour ce tutorial.

  • Ajout des images au projet

Lorsque vous souhaitez utiliser une image dans un projet XNA (que ce soit en 2D ou en 3D), il est nécessaire de l’ajouter au projet Content associé au projet XNA (Tutorial2DContent dans notre exemple).

xna_tuto2_1

Clic droit sur le projet Tutorial2DContent > Ajouter > Elément existant.

xna_tuto2_2

On sélectionne les fichiers que l’on souhaite ajouter et on clique sur le bouton “Ajouter”. Une fois que les fichiers sont ajoutés, on peut voir leurs propriétés.

xna_tuto2_3

La propriété Asset Name est le nom de l’image que nous utiliserons au sein de l’application. Vous pouvez noter qu’il n’y a pas d’extension. Vous pouvez la modifier si vous le souhaitez.

  • Chargement des images

Une fois que nous avons ajouté les images au projet, nous allons les charger au démarrage de l’application. Cela se fait généralement dans la méthode LoadContent() de la classe Game.

protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    //Chargement des deux textures
    this.tank = Content.Load<Texture2D>("tank_body");
    this.canon = Content.Load<Texture2D>("tank_canon");
}

Pour charger des images, il suffit d’appeler la méthode Load de la propriété Game.Content. Il est nécessaire de lui donner le type de données que l’on souhaite charger. Dans notre cas, on souhaite charger une image qui est représenté, dans XNA, par la classe Texture2D. On passe en paramètre  l’Asset Name de l’image à charger (et non pas le nom du fichier).

Le résultat du chargement est stocké dans une propriété de type Texture2D.

//Stockage de la texture du corps du tank
Texture2D tank;
//Stockage de la texture du canon
Texture2D canon;

  • Libération de la mémoire prise par les images (Optionnel)

Bien que .NET soit pourvu d’un Garbage collector nettoyant régulièrement les zones mémoires non utilisés, il est possible de faire le ménage soit même. Ainsi, si l’on souhaite libérer la mémoire prise par ces deux textures, il suffit d’aller dans la méthode UnloadContent de Game et d’appeler les Dispose() sur chacune des textures.

protected override void UnloadContent()
{
   this.tank.Dispose();
   this.canon.Dispose();
}

Rappel : La méthode UnloadContent() est appelée lorsque l’on sort de la boucle de jeu courante.

  • Affichage des images

Maintenant que nous avons chargé nos images, nous allons les afficher. Cela se passe dans la méthode Draw() de la classe Game.

//Position du tank
 Vector2 TankPosition = new Vector2(30, 40);

 protected override void Draw(GameTime gameTime)
 {
     GraphicsDevice.Clear(Color.CornflowerBlue);
     spriteBatch.Begin();
     spriteBatch.Draw(this.tank, TankPosition, Color.White);
     spriteBatch.End();
     base.Draw(gameTime);
 }

On utilise spriteBatch pour dessiner les textures à l’écran. On doit “préparer” spriteBatch avant son utilisation en appelant la méthode Begin() (nous parlerons de ses paramètres optionnels plus loin). On utilise ensuite la méthode Draw() afin d’afficher l’image. Dans sa version la plus simple, Draw comprend 3 paramètres :

  1. L’image à afficher
  2. La position de l’image par rapport au coin haut gauche de l’écran. La position est définie dans une structure Vector2. Afin de simplifier le code, on a créé une variable TankPosition pour stocker et manipuler la position de l’image.
  3. La teinte de l’image. Si l’on souhaite avoir l’image originale sans modification, on met la couleur blanche.

Une fois que l’on a dessiné l’ensemble des images souhaités, on appelle la méthode End() du spriteBatch afin que celui-ci envoie l’ensemble des informations à la carte graphique (ce comportement dépend du mode de spriteBatch choisi… voir plus loin dans cet article …).

  • Déplacement des images

Si l’on souhaite déplacer l’image, il suffit de modifier les données de la variable TankPosition entre chaque appel de la méthode Draw(). Si vous vous rappelez de la structure d’une boucle de jeu XNA, une méthode est appelé avant chaque Draw() ==> Update(). Cette méthode est destinée à faire toutes les tâches non graphiques (qui n’interagisse pas directement avec la carte graphique). Ainsi si l’on souhaite déplacer notre tank d’un pixel vers la droite et d’un pixel vers le bas, on utilisera le code suivant :

protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
      this.Exit();

   TankPosition.X += 1;
   TankPosition.Y += 1;
   base.Update(gameTime);
}
  • Système de coordonnées & Origines

Par défaut, l’ensemble des entités 2D de XNA (fenêtres, sprites …) sont définies dans un système de coordonnées cartésienne orthonormé dont l’unité est le pixel.

xna_tuto2_4

Le coin haut gauche de l’entité est l’origine (0,0) et les x sont positif lorsque l’on va vers la droite. Les y sont positif quand on va vers le bas.

Il est important de noter que le point d’origine est le même que ce soit pour la position de l’entité comme pour d’autres transformations comme la rotation. Nous allons voir, dans le paragraphe suivant, qu’il est possible de modifier ce point d’origine.

  • Rotation des images

Nous allons maintenant faire tourner le tank sur lui-même. Pour cela nous allons rajouter une propriété permettant de conserver la rotation

//Rotation en degré du tank
float TankRotation = 30.0f;

Attention : La rotation 0 est l’image tel qu’importée dans XNA. De plus la rotation se fait selon l’axe du point d’origine. Si l’on souhaite faire tourner une image sur elle-même, il est nécessaire de déplacer le point d’origine au centre de l’image (Width/2, Height/2).

Pour réaliser la rotation,nous utilisons une version plus avancée de la méthode Draw du spritebatch :

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    spriteBatch.Begin();
    spriteBatch.Draw(this.tank,                                  // Texture (Image)
                     TankPosition,                               // Position de l'image
                     null,                                       // Zone de l'image à afficher
                     Color.White,                                // Teinte
                     MathHelper.ToRadians(TankRotation),         // Rotation (en rad)
                     new Vector2(tank.Width/2,  tank.Height/2),  // Origine
                     1.0f,                                       // Echelle
                     SpriteEffects.None,                         // Effet
                     0);                                         // Profondeur
    spriteBatch.End();
    base.Draw(gameTime);
}

Les deux premiers et le quatrième paramètres ont déjà été décrit précédemment. Intéressons-nous aux autres paramètres :

    • Zone de l’image : Il est possible de ne copier qu’une partie de la texture à l’écran. Dans ce cas, on indique un rectangle indiquant la partie de la texture à afficher. Si l’on souhaite afficher la texture en entier, on passe la valeur null.
    • Rotation : Dans ce paramètre, on indique la rotation que l’on souhaite appliquer à la texture. Attention, l’angle de rotation doit être donné en RADIANS. Pour vous aider,  XNA fournir une classe MathHelper avec une méthode statique convertissant les degrés en radians.
    • Origine : Ce paramètre permet d’indiquer le point d’origine de la texture pour l’ensemble des transformations (déplacement, rotation…). Par défaut, il est situé en haut à gauche mais il est possible de le modifier comme indiqué dans notre exemple (le point d’origine de notre exemple se trouve au centre de la texture afin que celle-ci tourne sur elle-même.).
    • Echelle : Il est possible d’agrandir et de réduire la taille de votre texture. La valeur 1 permet de conserver la taille originale.
    • Effet : Ce paramètre permet l’application d’un effet de renversement (renversement vertical ou horizontal).Si vous ne souhaitez pas utiliser ces effets, mettre à SpriteEffects.None.
    • Profondeur : Le dernier paramètre permet d’indiquer la profondeur à laquelle vous souhaitez mettre votre texture. Cela permet de gérer les cas de chevauchement de texture (voir paragraphe suivant).

L’ensemble des méthodes Draw() de spriteBatch sont décrites en détail dans la MSDN.

  • Superposition et transparence

Si nous faisons la même chose avec le canon du tank, on obtient le code suivant :

//Position du tank
Vector2 TankPosition = new Vector2(30, 40);
//Rotation en degré du canon
float CanonRotation = 40.0f;

protected override void Draw(GameTime gameTime)
{
   GraphicsDevice.Clear(Color.CornflowerBlue);
   spriteBatch.Begin();
   spriteBatch.Draw(this.canon, TankPosition, null, Color.White, MathHelper.ToRadians(CanonRotation), new Vector2(canon.Width / 2 - 4.0f, canon.Height / 2), 1.0f, SpriteEffects.None, 0);
   spriteBatch.Draw(this.tank, TankPosition, null, Color.White, MathHelper.ToRadians(TankRotation), new Vector2(tank.Width / 2, tank.Height / 2), 1.0f, SpriteEffects.None, 0);
   spriteBatch.End();
   base.Draw(gameTime);
}

Remarque : Le point d’origine pour le canon contient un –4.0f pour X. Cela provient du décalage entre le centre de la texture du tank et celle du canon. Afin d’avoir un affichage un peu plus réaliste, j’ai décalé légèrement le point d’origine du canon.

Cela semble correct mais lorsque l’on affiche le résultat :

xna_tuto2_5a

xna_tuto2_5b

Canon et Tank correctement placé

Lors de la rotation du canon, une partie de celui ci disparait sous le tank lui même.

Ce problème est du à la façon dont spriteBatch affiche les sprites. Cela peut être défini dans la méthode Begin() grâce au premier paramètre qui est l’énumération SpriteSortMode

Valeur

Description

Deferred

Valeur par défaut et la plus utilisée. Envoi les opérations à la carte graphique lors de l’appel de End(). Cela permet d’utiliser plusieurs spritesBatchs sans risquer d’avoir de conflit au niveau de la carte graphique. Dépend du paramètre profondeur de la méthode Draw.Le premier plan étant 1 et le fond 0.

BackToFront

Affichage des sprites selon le paramètre de profondeur passé par  la méthode Draw. Le premier plan étant 1 et le fond 0.

FrontToBack

Affichage des sprites selon le paramètre de profondeur passé par  la méthode Draw. Le premier plan étant 0 et le fond 1.

Immediate

Affiche les images en écrasant les pixels déjà présent. Ce mode est le plus rapide mais celui qui gère le moins bien les transparences et supperposition ainsi que l’utilisation de multiples spritesBatch.

Texture

Affiche les images dans l’ordre des textures. Ainsi, si plusieurs appels à Draw se font avec la même texture, l’ensemble des appels sera fait en même temps pour gagner du temps en transfert de données vers la carte graphiques.

Pour la documentation originale sur SpriteSortMode : La page MSDN.

Begin peut prendre aussi un autre paramètre pour indiquer comment il doit gérer les cas d’écrasement de pixel (vouloir dessiner un pixel qui a déjà été dessiné par une autre texture). Ce paramètre est de type BlendState et comprend 4 valeurs possibles.

Valeur

Description

AlphaBlend

Valeur par défaut. Fusion du pixel source/destination en tenant compte du canal alpha (transparence).

Additive

Ajoute les canaux RGBA entre les deux pixels

NonPremultiplied

Ajoute les canaux RGB entre les deux pixels sans tenir compte de la transparence

Opaque

La couleur du nouveau pixel écrase celle du tampon.

Pour la documentation originale sur BlendState: La page MSDN.

Pour résoudre le problème que nous avons rencontré, il y a donc deux solutions :

    • Intervertir les deux méthodes Draw()
protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    spriteBatch.Begin();
    spriteBatch.Draw(this.tank, TankPosition, null, Color.White, MathHelper.ToRadians(TankRotation), new Vector2(tank.Width / 2, tank.Height / 2), 3.0f, SpriteEffects.None, 0);            
    spriteBatch.Draw(this.canon, TankPosition, null, Color.White, MathHelper.ToRadians(CanonRotation), new Vector2(canon.Width / 2 - 4.0f, canon.Height / 2), 3.0f, SpriteEffects.None, 0);
    spriteBatch.End();
    base.Draw(gameTime);
}

    • Changer la méthode Begin
protected override void Draw(GameTime gameTime)
{
     GraphicsDevice.Clear(Color.CornflowerBlue);
     spriteBatch.Begin(SpriteSortMode.FrontToBack,null);
     spriteBatch.Draw(this.canon, TankPosition, null, Color.White, MathHelper.ToRadians(CanonRotation), new Vector2(canon.Width / 2 - 4.0f, canon.Height / 2), 3.0f, SpriteEffects.None, 0);
     spriteBatch.Draw(this.tank, TankPosition, null, Color.White, MathHelper.ToRadians(TankRotation), new Vector2(tank.Width / 2, tank.Height / 2), 3.0f, SpriteEffects.None, 0);
     spriteBatch.End();
     base.Draw(gameTime);
}

On obtient un affichage “correct” :

xna_tuto2_6

Remarque : Les petites zones bleues proviennent de mon placement tank/canon qui n’est pas parfait sur pour ces images.

  • Conclusion

Vous savez maintenant charger ,afficher et déplacement un image dans un univers 2D XNA. Dans un prochain post, nous verrons comment déplacer ces images en fonction des entrées de l’utilisateurs (souris,clavier,Manette XBOX 360, Touch Windows Phone). Le prochain post est consacré à l’affichage de texte dans la fenêtre.

Téléchargement du résultat final

  • Exercices pour tester ses compétences
  1. Faire se déplacer le char dans l’écran en le faisant rebondir sur les coins de l’écran.
    • Indice 1 : Attention au point d’origine
    • Indice 2 : Les dimensions de l’écran sont disponibles au travers de graphics.GraphicsDevice.Viewport
    • Indice 3 : Attention au pixel transparent autour du tank
  2. Faire le même exercices en gérant le couple char+canon.

Solution possible

Propulsé par WordPress.com.

%d blogueurs aiment cette page :