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

Publicités

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

05/10/2010

[XNA 4] Tutoriel 3D 1 : Hello World 3D

Filed under: .NET, 3D, C# 4, Débutant, Intermediaire, XBOX 360, XNA — Étiquettes : , , , , , , , , — sebastiencourtois @ 16:31

Après une série de tutoriels sur la programmation 2D en XNA, nous allons passer à une partie plus intéressante et surtout plus complexe … la 3D. Qui n’a pas rêvé de recréer soi-même un Crysis ou encore un Gran Tourismo.

new-crysis-dx10-screenshot-20061110001326035 gran_turismo_5
Copie d’écran de Crysis Copie d’écran de Gran Tourismo 5

Il faut bien se rendre compte que ces jeux, magnifique graphiquement, ont été réalisés par des équipes composés de dizaines de programmeurs, artistes 2D/3D  qui ont travaillé pendant des mois pour arriver à un tel réalisme. Même si être ambitieux/optimiste  aide dans ce métier, il faut se rendre à l’évidence que vous ne pourrez pas seul et avant de nombreuses années arriver à un tel résultat.

Toutefois, si cela peut vous consoler, nous allons utiliser des outils/concepts proches de ce que les développeurs de ces jeux utilisent et nous seront confrontés (à notre niveau) aux mêmes types de problèmes inhérent à la création de jeu vidéo.

  • Un peu de vocabulaire/concept

Le monde de la 3d est un monde qui parait assez obscur et mathématiques avec son propre vocabulaire et ses propres concepts. Toutefois, ceux-ci peuvent être rattachés à des éléments du quotidien (on vit dans un univers en 3d, non :)).

Un Vertex (pluriel : vertices) : Un vertex est un point dans un univers 3d. C’est l’élément de base dans la construction de formes 3D primitives (lignes, triangle). Il est généralement décrit par sa position dans l’univers 3d ainsi que sa couleur, normals et d’autres informations nécessaires au rendu 3d que nous verrons plus tard.

Un ligne : Primitive 3D composé de deux vertices

Un triangle : Primitive 3d composé de 3 vertices. C’est la primitive la plus évolué qu’une carte graphique peut dessiner. Les modèles 3d que vous pouvez voir dans les jeux vidéo ne sont généralement qu’une accumulation de triangles mis cote à cote.

eames_wireframe Remarque : Les cartes graphiques peuvent aussi dessiner des primitives à 4 vertices mais cette fonctionnalité n’est pas disponible dans XNA 4.

Graphics Device : Ce terme représente l’accès à la carte graphique. Toute les opérations graphiques passeront par des appels au graphics Device (notamment la partie de rendu 3d).

  • Premier projet 3D en XNA

Dans ce tutoriel, nous allons créer un triangle.

Pour commencer, il faut créer un nouveau projet XNA (Windows Game). XNA nous crée déjà le graphics Device nécessaire à la communication avec la carte graphique ainsi que les outils pour la gestion du contenu (ContentManager).

Pour une scène 3d, il est nécessaire d’avoir au minimum 2 choses :

    • Un modèle 3D

Dans notre exemple, notre modèle 3D va être relativement simple car il s’agira d’un triangle. Pour cela, il nous suffit de créer un tableau de 3 vertices. Chaque vertex aura une position et une couleur. Dans notre classe Game, nous allons créer un tableau pour contenir les vertices ainsi qu’une méthode créant les données du triangle.

VertexPositionColor[] vertices;
//Création du triangle
private void CreateTriangle()
{
   this.vertices = new VertexPositionColor[]
   {
       new VertexPositionColor( new Vector3(-1,-1,0), Color.Red),
       new VertexPositionColor( new Vector3(0,1,0), Color.Green),
       new VertexPositionColor( new Vector3(1,-1,0), Color.Blue),
   };
}

Pour contenir les données d’un vertex, XNA fournit une série de classe de base permettant de stocker correctement ces données. Dans notre cas, nous souhaitons définir seulement la position et la couleur des points. Nous utiliserons donc la structure VertexPositionColor prenant en paramètre une position dans un univers 3D (sous la forme d’une structure Vector3) et une couleur. Nous stockons le tout dans un tableau défini au préalable dans notre classe Game.

Remarque : D’autres types de structures de stockage de vertices existent dans XNA (VertexPositionColor / VertexPositionColorTexture / VertexPositionNormalTexture / VertexPositionTexture). Vous pouvez créer votre propre classe en implémentant l’interface IVertexType. Nous verrons dans un  prochain tutoriel la création de notre propre structure de stockage.

    • Une visualisation (ou caméra)

Une fois le modèle 3D défini, il est nécessaire d’indiquer comment on souhaite le voir. Pour cela, il est nécessaire de fournir la position de l’observateur, l’endroit qu’il regarde et d’autres informations comme l’angle de vision. Ces informations seront ensuite stockées sous forme de matrices mathématiques qui serviront à la carte graphique pour le rendu 3D.

Matrix View;
Matrix Projection;
Matrix World;

Vector3 CameraPosition = new Vector3(0.0f, 0.0f, 5.0f);
Vector3 CameraTarget = Vector3.Zero;
Vector3 CameraUp = Vector3.Up;

//Création de la caméra
private void CreateCamera()
{
    View = Matrix.CreateLookAt(CameraPosition, CameraTarget, CameraUp);
    Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, this.GraphicsDevice.Viewport.Width / this.GraphicsDevice.Viewport.Height, 0.01f, 1000.0f);
    World = Matrix.Identity;
}

La première matrice est la matrice View (dite de “Visualisation”). Elle sert à positionner la caméra et la façon dont celle-ci regarde la scène. On utilise une méthode d’aide fourni par XNA (CreateLookAt) qui prend en premier paramètre la position de l’utilisateur dans l’univers 3D (sous la forme d’un Vector3). Le paramètre suivant est l’endroit que regarde l’utilisateur. Le troisième paramètre indique “l’orientation” de la caméra. Dans notre exemple, nous souhaitons que le dessus de la camera soit orienté vers le haut (coordonnées {0,1,0} ce qui équivaut au Vector3.Up).

La deuxième matrice est la matrice de projection. Celle-ci indique les caractéristique de la caméra (lentille, distance focale). Une méthode d’aide XNA permet de créer cette matrice facilement (CreatePerspectiveFieldOfView). Le premier paramètre est l’angle d’ouverture (ou angle de vision) de la caméra. Par exemple, un humain a un angle de vision allant de 1° (angle d’attention) jusqu’à 180 ° (angle de perception).Cela veut dire qu’il peut, sans bouger la tête voir tout ce qui l’entoure à 180 °. Arbitrairement, nous allons décider d’utiliser un angle de vision de 45° ou PI/4 (car les angles sont décrits en radians en XNA). Le paramètre suivant est le ratio d’aspect. Cela indique le format de la caméra (16/9 comme au cinéma, 4/3). Cela correspond tout simplement à la largeur de la zone visible divisé par la hauteur. Les deux derniers paramètres correspondent à la distance minimale et maximale d’affichage. Ainsi tous les objets trop proche ou trop loin de la caméra ne seront pas affichés.

La troisième matrice nous permettra de placer les objets dans l’univers 3d. Pour l’instant, nous lui assignons la valeur de base (matrice identité).

    • Méthode d’initialisation

Nous avons maintenant tous les éléments nécessaire pour réaliser un scène 3d. Nous allons maintenant appelé les deux méthodes décrites plus haut dans la méthode Initialize().

protected override void Initialize()
{
   this.CreateTriangle();
   this.CreateCamera();
   base.Initialize();
}
  • Affichage d’un triangle

Afin de réaliser l’affichage, nous allons utiliser un shader. Un shader est un programme créé spécifiquement pour être exécuté sur la carte graphique. En effet, la carte graphique permet la réalisation de calcul mathématique très rapidement. La création de shader sera l’objet de plusieurs tutoriels mais, dans ce premier article sur la 3d, nous allons utilisé celui fournit par XNA : BasicEffect.

BasicEffect effect;

protected override void LoadContent()
{
  spriteBatch = new SpriteBatch(GraphicsDevice);
  this.effect = new BasicEffect(this.GraphicsDevice);
}

BasicEffect se crée en fournissant uniquement le graphicsDevice et il sera utilisé lors de la partie affichage (méthode Draw).

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

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World;
    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);
}

Première étape dans la méthode Draw. Nettoyer l’écran avec une couleur de fond de la même façon que pour la 2D. On fournit ensuite les trois matrices de projections au shader BasicEffect. On active aussi le booléen VertexColorEnabled pour indiquer que l’on souhaite que les couleurs définis sur les vertices soient prises en compte. On utilise ensuite la propriété CurrentTechnique pour utiliser les différentes passes du shaders (le fonctionnement exacte des shaders sera expliqué dans un autre tutoriel et sa compréhension n’est pas indispensable pour celui-ci).

La méthode la plus importante de ce morceau de code est le DrawUserPrimitivers qui va prendre en paramètre le type de primitive. Il s’agit de la façon dont nous allons associer les vertices.

Initialising_Article_triangles

Dans notre exemple, nous utiliserons TriangleList. Cette primitive prend 3 vertex pour en faire un triangle puis les 3 vertex suivants pour un autre triangle et ainsi de suite.

Le deuxième paramètre de DrawUserPrimitives est le tableau de vertices proprement dit. Le paramètre suivant est l’index de départ du tableau de vertices. Dans notre cas, on souhaite utiliser tous les vertices donc on indique que le premier vertex est en position 0. Le dernier paramètre indiquent le nombre de primitives (ici triangle) que l’on souhaite dessiner. On prend le nombre total de vertices de notre tableau (3) et on divise par le nombre de vertex nécessaire pour faire un triangle (3). On indique que l’on souhaite dessiner un seul triangle.

Si vous lancer le projet, vous devriez obtenir l’écran suivant :

xna_tuto3d1_1

On remarque que le fond est bleu ce qui correspond à la couleur d’effacement au début de notre méthode Draw (Clear(Color.CornFlowerBlue)). Le triangle est visible mais on remarque que les couleurs, correctes aux sommets, fusionnent entre elles en s’écartant des sommets. Il s’agit du comportement de base de DirectX. Si l’on souhaite faire un triangle uni, il faut que les sommets aient la même couleur.

  • Un peu de mouvement

Nous allons introduire une rotation sur ce triangle afin de prouver que nous sommes bien dans un univers 3D. Pour cela, on va rajouter une variable de rotation que nous allons incrémenter dans la méthode Update.

float RotateZ = 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(2.0f);

    base.Update(gameTime);
}

Nous incrémentons donc la variable de 2° par appel à la méthode Update (60 x par secondes par défaut sur XNA). Il est à noter que la valeur est toujours à stocker en Radians. MathHelper fournit des méthodes d’aides pour traduire les angles de degré en radians et inversement.

Une fois la variable modifié, il suffit de modifier la matrice World qui est chargé de placer le triangle dans l’espace.

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

Il suffit de multiplier la matrice World avec une matrice de rotation créé à partir de l’angle RotateZ pour réaliser cette effet. Si vous relancer l’application le triangle tourne…

  • Mon triangle disparait puis réapparait sans arrêt.

Lorsque vous lancez l’application le triangle commence à tourner mais il disparait un moment au bout d’un demi-tour puis réapparait. Cela est dû à une optimisation de la carte graphique nommé Backface Culling. En effet, les cartes graphiques n’affiche pas les faces (triangles) ne leur faisant pas face. Vous trouverez une explication plus complète sur le fonctionnement  ici. Il est possible de désactiver cette optimisation en modifiant la propriété RasterisationState du graphicsDevice. Cela se réalise généralement dans la méthode Initialize().

this.GraphicsDevice.RasterizerState = new RasterizerState()
{
   CullMode = CullMode.None,
   FillMode = FillMode.Solid
};

Remarque : Vous pouvez aussi régler par la propriété FillMode si vous souhaitez une vision des objets “solides” ou en fil de fer.

Vous pouvez remarquer que CullMode à 3 valeurs possibles : CullClockwiseFace ,CullCounterClockwiseFace, None. Par défaut, le CullMode est à CullClockwise ce qui veut dire que les vertex doivent être définis dans l’ordre des aiguilles d’une montre pour que la face soit visible.

TriangleAll
Représentation d’un triangle dont les vertices sont dans le sens des aiguilles d’une montre (tiré d’un tutoriel anglais XNA : http://www.toymaker.info/Games/XNA/html/xna_simple_triangle.html).

Lorsque le triangle a fait son premier demi-tour, les vertex passent dans le sens inverses. N’étant plus dans le sens des aiguilles d’une montre, le triangle n’est plus affiché.

Une autre solution, qui permet de conserver le Backface Culling, serait de créer un deuxième triangle avec les vertices dans l’ordre inverse. Ainsi, si on modifie le tableau de vertices comme suit  :

//Création du triangle
private void CreateTriangle()
{
    this.vertices = new VertexPositionColor[]
    {
        //Triangle 1
        new VertexPositionColor( new Vector3(-1,-1,0), Color.Red),
        new VertexPositionColor( new Vector3(0,1,0), Color.Green),
        new VertexPositionColor( new Vector3(1,-1,0), Color.Blue),
        //Triangle 2
        new VertexPositionColor( new Vector3(1,-1,0), Color.Blue),
        new VertexPositionColor( new Vector3(0,1,0), Color.Green),
        new VertexPositionColor( new Vector3(-1,-1,0), Color.Red),

    };
}

On obtient un triangle (en fait deux triangles qui s’interchange) qui tourne correctement.

  • Conclusion

Après ce tutoriel un peu long et complexe, je pense que vous aurez compris que la route est longue avant de pouvoir faire une jeu aussi techniquement abouti que les jeux présentés plus haut. Vous savez maintenant faire un triangle ce qui est la base de toutes les applications 3D. Dans les prochains tutoriels, nous verrons comment afficher un cube, le texturer puis se déplacer dans un univers 3d. Nous verrons aussi quelques astuces pour optimiser l’affichage.

N’hésitez pas à donner vos commentaires et poser vos questions sur ce tutoriel.

Lien vers la solution

18/09/2010

[XNA 4] Tutoriel 4 : Gestion des entrées : Clavier, Souris, Manette XBOX 360, Windows Phone Touch

Filed under: .NET, C# 4, Débutant, Jeux vidéos, XBOX 360, XNA — Étiquettes : , , , , , , , , , , , — sebastiencourtois @ 13:50

Jusqu’a présent, nous avons fait des applications démos tournant automatiquement. Même si certaines démos commerciales ou films d’animations peuvent être agréables à regarder, un jeu vidéo est intéressant grâce à l’interaction joueur/univers du jeu.

Code Source pour commencer ce tutoriel

Au travers de ce tutoriel, nous allons voir quatre outils d’interactions avec le joueur pour déplacer le tank du tutoriel précédent :

    • Clavier (PC & XBOX 360)
    • Souris (PC uniquement)
    • Touch (Windows Phone uniquement)
    • Manette XBOX 360 (PC / XBOX 360)

Remarques : Il est possible de brancher un clavier USB à une XBOX 360 et récupérer les entrées du clavier comme s’il s’agissait d’un pc. Pour le touch, XNA ne prend pas en charge que le toucher des écrans multitouch PC vendus dans le commerce.

Comme tout les codes de mise à jour du jeu non graphique, la gestion des interactions avec l’utilisateur se réalise dans la méthode Update() de la boucle de jeu.

  • Gestion du clavier

La première chose à faire est de récupérer l’état du clavier à un instant T. Cela se réalise par la méthode statique GetState() de la classe Keyboard

KeyboardState kbState = Keyboard.GetState();

Remarque : GetState() peut avoir un paramètre PlayerIndex (allant de Player.One à Player.Four). Cela est utilisé pour identifier la manette de l’utilisateur utilisant un Chatpad (clavier se branchant directement sur la manette). Pour le PC, on utilisera la version sans paramètre.

On récupère un structure KeyboardState contenant les informations sur les touches pressées ou non. KeyboardState contient 3 méthodes :

    • GetPressedKeys() : Méthode renvoyant un tableau des touches enfoncées (enum Keys)
    • IsKeyUp(Keys key) : Indique si la touche du clavier passée en paramètre est relaché (true si relâchée)
    • IsKeyDown(Keys key) : Indique si la touche du clavier passée en paramètre est enfoncée (true si enfoncée)

L’énumération Keys est une énumération de l’ensemble des touches du clavier (Keys.Up correspond à la flèche du haut, Keys.B à la touche B…). Il est possible d’utiliser les valeurs numériques correspondant à ces alias.

Partant des principes ci-dessus, on souhaite déplacer le tank en utilisant les touches du clavier et doubler la vitesse si, en plus on appuie sur la touche majuscule. On divisera par deux la vitesse si on appuie sur la touche control pendant le déplacement.

//Déplacement remise à 0 du déplacement
TankVelocity = Vector2.Zero;
//Vitesse maximun du tank
int TankMaximunSpeed = 2;

//Gestion du clavier
KeyboardState kbState = Keyboard.GetState();

if (kbState.IsKeyDown(Keys.Up))
    TankVelocity.Y -= TankMaximunSpeed;
if (kbState.IsKeyDown(Keys.Down))
    TankVelocity.Y += TankMaximunSpeed;
if (kbState.IsKeyDown(Keys.Left))
    TankVelocity.X -= TankMaximunSpeed;
if (kbState.IsKeyDown(Keys.Right))
    TankVelocity.X += TankMaximunSpeed;
if (kbState.IsKeyDown(Keys.LeftShift) || kbState.IsKeyDown(Keys.RightShift))
    TankVelocity *= 2;
if (kbState.IsKeyDown(Keys.LeftControl) || kbState.IsKeyDown(Keys.RightControl))
    TankVelocity /= 2;

La variable TankMaximunSpeed est une constante définie comme la vitesse maximum du tank dans une direction donnée. TankVelocity décrit le vecteur de déplacement du tank pour l’itération courante.

  • Gestion de la souris

Comme nous l’avons fait pour le clavier, la première tâche pour récupérer l’état de la souris.

//Gestion de la souris
MouseState mouseState = Mouse.GetState();

La structure MouseState contient trois propriétés importantes :

    • Propriétés *Button : Permet de choisir un des boutons de la souris et de savoir s’il est pressé ou non (Enumération ButtonState).
    • Propriétés X, Y : Indique la position (X,Y) du curseur sur l’écran de l’ordinateur (la position en dehors de la fenêtre de jeu est transmise).
    • Propriété ScrollWheelValue : Indique le nombre d’utilisation du scroll depuis le début du jeu.(positif si scroll vers l’avant).

Si l’on souhaite déplacer le tank vers le clic de l’utilisateur, on utilisera le code suivant (le bouton gauche de la souris doit rester enfoncé pour faire avancer le tank).

if (mouseState.LeftButton == ButtonState.Pressed)
{
    Vector2 PositionMouse = new Vector2(mouseState.X, mouseState.Y);
    TankVelocity = PositionMouse - TankPosition;
    TankVelocity.Normalize();
    TankVelocity *= TankMaximunSpeed;
}

Par défaut, XNA ne gère pas les doubles clics ou les clics longs. C’est au développeur de gérer ces cas. Nous verrons, dans un prochain post, comment réaliser ces opérations particulières.

  • Gestion du Touch Windows Phone

Pour utiliser le Touch, il faut tout d’abord vérifier si le matériel peut gérer cette capacité.

//Gestion du touch
TouchPanelCapabilities touchCap = TouchPanel.GetCapabilities();
if (touchCap.IsConnected)
{
   [...]
}

Un fois la compatibilité matérielle vérifié, on récupère l’état de l’interface tactile.

TouchCollection touches = TouchPanel.GetState();

La struncture TouchCollection est un tableau de TouchLocation contenant chacune un identifiant, une position et un état TouchLocationState parmi les 4  états suivants :

    • Pressed : Un nouveau point de pression est disponible
    • Released : Un point pression existant a disparu (l’utilisateur a retiré son doigt de l’écran
    • Moved : Un point de pression existant lors de l’itération précédente est toujours là et la position a été mise à jour
    • Invalid : Problème de reconnaissance des points de pressions (souvent un nouveau point de pression qui est pris pour un point existant).

Si l’on souhaite réaliser la même chose que l’exemple précédent avec la souris, on utilisera le code suivant : (on ne prend que le premier contact tactile pour simuler un curseur de souris).

//Gestion du touch
TouchPanelCapabilities touchCap = TouchPanel.GetCapabilities();
if (touchCap.IsConnected)
{
   TouchCollection touches = TouchPanel.GetState();
   if (touches.Count >= 1)
   {
      Vector2 PositionTouch = touches[0].Position;
      TankVelocity = PositionTouch - TankPosition;
      TankVelocity.Normalize();
      TankVelocity *= TankMaximunSpeed;
   }
}
XNA_WindowsPhoneInput 

L’utilisation de l’interface Touch du Windows Phone sera décrit plus en détail dans un prochain post (notamment l’utilisation de gestures et du multitouch).

  • Gestion de la manette XBOX 360

La manette XBOX 360 est accessible au travers de l’API XNA. La  première étape consiste à voir si une ou plusieurs manettes sont connectées puis de récupérer son état. Dans notre exemple, nous ferons cela juste pour le premier joueur.

GamePadState gamepadState = GamePad.GetState(PlayerIndex.One);
if (gamepadState.IsConnected)
{
    [...]
}

La manette XBOX 360 n’est pas la seule utilisable avec XNA. Un certain nombre d’autres manettes peuvent être vu par XNA (sur PC uniquement). Afin de vérifier les éléments disponibles sur ces manettes, la classe GamepadCapabilities permet d’indiquer la présence ou non des joysticks/boutons.

GamePadCapabilities gamepadCaps = GamePad.GetCapabilities(PlayerIndex.One);

if (gamepadCaps.HasLeftXThumbStick && gamepadCaps.HasLeftYThumbStick)
    TankVelocity = gamepadState.ThumbSticks.Left * TankMaximunSpeed;

else if (gamepadCaps.HasLeftXThumbStick && gamepadCaps.HasLeftYThumbStick)
    TankVelocity = gamepadState.ThumbSticks.Right * TankMaximunSpeed;

else if (gamepadCaps.HasDPadUpButton && gamepadCaps.HasDPadLeftButton && gamepadCaps.HasDPadRightButton && gamepadCaps.HasDPadDownButton)
{
    if (gamepadState.IsButtonDown(Buttons.DPadUp))
       TankVelocity.Y -= TankMaximunSpeed;
    if (gamepadState.IsButtonDown(Buttons.DPadDown))
       TankVelocity.Y += TankMaximunSpeed;
    if (gamepadState.IsButtonDown(Buttons.DPadLeft))
       TankVelocity.X -= TankMaximunSpeed;
    if (gamepadState.IsButtonDown(Buttons.DPadRight))
       TankVelocity.X += TankMaximunSpeed;
}

Pour notre exemple, nous utilisons le joystick gauche s’il existe sinon nous utilisons le joystick droit s’il existe sinon nous utilisons la croix directionnelle (si elle existe). Vous pouvez remarquer que le GamepadState a deux méthodes IsButtonDown/IsButtonUp qui fonctionne de la même façon que le clavier.

Les joysticks (Thumbstick) ont un fonctionnement légèrement différent.La propriété ThumbSticks regroupe l’ensemble des joysticks (2 joysticks au maximum : Left et Right). Chaque joystick est un Vector2 indiquant le décalage  (X,Y) par rapport au point central. Le point central est (0,0) et les “extrémités” étant –1 et 1. On obtient donc une valeur comprise entre –1 et 1 pour chacun des axes.

Certaines manettes (la manette 360 notamment) sont équipées de moteurs pour faire vibrer celle-ci lors de certains évènements. Pour cela, on utilisera la méthode statique SetVibration de la classe GamePad.

if (gamepadCaps.HasLeftVibrationMotor && gamepadCaps.HasRightVibrationMotor)
    GamePad.SetVibration(PlayerIndex.One, 0.7, 0.25);

Les valeurs possibles pour chacun des moteurs vont de 0 (aucune vibration) à 1 (vibration maximum).

Un utilitaire disponible sur le site XNA montre plus clairement la gestion de la manette.

  • Conclusion

Vous pouvez maintenant récupérer les informations provenant de l’utilisateur et modifier l’environnement du jeu en conséquence. Cela conclut la première partie des tutoriels 2D de base sur XNA. Les prochains tutoriels auront pour thème les animations, le son, l’IA, les effets 2D et la gestion de contenu avancé (Content Pipeline) pour arriver sur la 3D.

Code Source de ce tutoriel

  • Exercices pour aller plus loin
  1. Changer le système de déplacement clavier/manette afin que les touches gauche/droite fasse tourner le tank et que les touches haut/bas fasse avancer/reculer le tank en fonction de son orientation.
  2. Faire tourner le tank dans la direction du clic de souris (ou du touch) pendant le déplacement (l’avant du tank doit toujours être “face” à sa destination).

La solution sera fournis prochainement.

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

Créez un site Web ou un blog gratuitement sur WordPress.com.

%d blogueurs aiment cette page :