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

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

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