Annexe : Billboard en Xna

Retourner au sommaire des cours  


Le billboard est un élément essentiel pour décharger le GPU de l’affichages de formes complexes. 


Un billboard (en français “panneau”) est un plan simulant un objet 3D. Le principe des billboards est de toujours faire face à la caméra : ainsi quelque soit l’endroit d’où on les regarde, ils donneront toujours  l’illusion que l’image qui les texture est une forme 3D.


L’avantage est de réduire énormément la complexité de la scène, puisque l’on va pouvoir remplacer des objets potentiellement complexes par deux simples triangles texturés formant le carré (ou plan). La texture bien entendu doit être de qualité et si possible faire partie d’une animation.


Les billboards sont utilisés pour la végétation, les explosions, les effets météorologique (nuages, …), ou encore des objets très lointains pour lesquels on ne pourra que très difficilement déceler le trucage.


Jusqu’à présent avec DirectX pour créer un billboard on devait travailler sur la matrice de vue ou créer de toute pièce une matrice de transformation à partir de la position de la caméra. En Xna tout est plus simple, il nous suffit d’appeller une méthode statique de la classe Matrix nommée CreateBillboard.


Nous verrons trois samples pour mettre en évidence l’utilité de cette technique.Un sample de présentation qui va montrer de manière explicite le billboard en action, un sample qui mettra en évidence l’effet réaliste que produit le billboarding (nous repredrons un sample du SDK Direct) et enfin un sample identique au précédent mais avec des animations.

 

Vous devez avoir lu les tutoriaux Xna jusqu’au chapitre 8 pour comprendre ce cours.

Les billboards utilisés ici exploitent la méthode CreateBillboard de la classe Matrix et n’utilisent en rien les fichiers effets.


Premier sample


Dans ce premier sample nous allons afficher deux objets : un cube tout d’abord dont la taille sera augmentée de telle sorte que la camera se trouvera à l’intérieur. Ses parois serviront alors de référence lorsque nous déplacerons la caméra à l’aide de la souris. Ensuite un objet billboard. Il s’agira tout simplement d’une face carrée composée de deux triangles. En “marche” normale, ce billboard sera desactivé et tournera de manière solidaire avec le cube lorsque la caméra sera déplacée. Mais lorsqu’on appuyera sur la touche “Espace”, le billboard présentera toujours sa face texturée à la caméra, et ceci, quelque soit la position de cette dernière.


La classe Billboard

Sur le plan technique 3D notre classe sera relativement simple. Elle n’aura pour tâche que d’afficher un simple plan 3D composé de deux triangles isocèles rectangles. Le plan sera texturé et de couleur paramétrable. La classe disposera d’une méthode Update permettant au billboard de se repositionner par rapport à la position de la caméra, par rapport au point vers lequel la caméra regarde et enfin par rapport à la normale de la caméra. Enfin une propriété sera ajoutée -pour les besoins de l’exemple- permettant d’activer ou de desactiver le billboard. Terminons en ajoutant qu’elle hérite de la classe mère TransformBase qui a été présentée ici.

Analysons son code :

/// <summary>
/// <para>Defines a billboard object.</para>
/// </summary>
/// <remarks>A billboard object always present its face in front of the camera.</remarks>
public class Billboard : TransformBase
{
    private GraphicsDevice _device;
    private VertexBuffer _vertexBuffer = null;
    private Color _color = Color.TransparentWhite;
    private Texture2D _texture;
    private BasicEffect _effect;
    private bool _activated;    private Matrix _billboardMatrix = Matrix.Identity;      /// <summary>    /// <para>Gets or sets a value indicating if the biilboard is activated.</para>    /// </summary>    public bool Activated    {        get        {            return this._activated;        }        set        {            this._activated = value;        }    }     /// <summary>    /// <para>Gets or sets the cube’s texture.</para>    /// </summary>    public Texture2D Texture    {        get        {            return this._texture;        }        set        {            this._texture = value;            this._effect.Texture = value;        }    }      /// <summary>    /// <para>Gets the cube’s effect.</para>    /// </summary>    public BasicEffect Effect    {        get        {            return this._effect;        }    }     /// <summary>    /// <para>Gets or sets the Cube’s color.</para>    /// </summary>    public Color Color    {        get        {            return this._color;        }        set        {            this._color = value;        }    }     public void Load(GraphicsDevice device)    {        this._device = device;        this.InitializeVertices();        this.InitializeEffect();    }     private void InitializeEffect()    {        this._effect = new BasicEffect(this._device, null);        this._effect.VertexColorEnabled = true;        this._effect.TextureEnabled = true;    }     private void InitializeVertices()    {        VertexPositionColorTexture[] vertices = new VertexPositionColorTexture[4];         vertices[0].Position = new Vector3(-100.5f, 100.5f, 0);        vertices[0].Color = this.Color;        vertices[0].TextureCoordinate = new Vector2(0, 0);         vertices[1].Position = new Vector3(100.5f, 100.5f, 0);        vertices[1].Color = this.Color;        vertices[1].TextureCoordinate = new Vector2(1, 0);         vertices[2].Position = new Vector3(100.5f, -100.5f, 0);        vertices[2].Color = this.Color;        vertices[2].TextureCoordinate = new Vector2(1, 1);         vertices[3].Position = new Vector3(-100.5f, -100.5f, 0);        vertices[3].Color = this.Color;        vertices[3].TextureCoordinate = new Vector2(0, 1);         this._vertexBuffer = new VertexBuffer(        this._device,        typeof(VertexPositionColorTexture),        4,        ResourceUsage.WriteOnly,        ResourceManagementMode.Automatic);         this._vertexBuffer.SetData(vertices);    }     /// <summary>    /// <para>Render the cube on the device.</para>    /// </summary>    public void Render()    {        this._device.RenderState.CullMode = CullMode.None;        this._effect.Begin();        this._effect.World = (this._activated ? this._billboardMatrix : Matrix.Identity)*this.Transform;          foreach (EffectPass pass in this._effect.CurrentTechnique.Passes)        {            pass.Begin();             this._device.Textures[0] = this.Texture;            this._device.Vertices[0].SetSource(this._vertexBuffer, 0, VertexPositionColorTexture.SizeInBytes);            this._device.VertexDeclaration = new VertexDeclaration(this._device, VertexPositionColorTexture.VertexElements);            this._device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);              pass.End();        }         this._effect.End();        this._device.RenderState.CullMode = CullMode.CullClockwiseFace;    }      public void Update(GameTime gameTime, Vector3 cameraPosition, Vector3 cameraLookAt, Vector3 cameraUpVector)            _billboardMatrix = Matrix.CreateBillboard(Vector3.Zero, this.Position – cameraPosition, cameraUpVector, cameraLookAt);
      }
}

Aucune réelle difficulté pour comprendre ce code. Seules deux instructions seront portées à notre attention. La première se trouve dans la méthode Update :

 _billboardMatrix = Matrix.CreateBillboard(Vector3.Zero, this.Position-cameraPosition, cameraUpVector, cameraLookAt);

 Elle charge dans la variable _billboardMatrix  une matrice de billboard renvoyée par la méthode Matrix.CreateBillboard. Comment savoir vers où diriger la face du billboard ? En y reflechissant bien, nous n’avons besoin de connaître que quatre propriétés :


  1. La position de l’objet.
  2. La position de la caméra.
  3. Le point vers lequel regarde la caméra
  4. La normale de la caméra. 

C’est justement ce que demande cette méthode. Pour l’heure (10/03/07) elle semble être bugguée. Normalement on lui passe la position de l’objet courant en premier paramètre, la position de la caméra en second paramètre, la normale de la caméra en avant dernier paramètre et enfin le point vers lequel la caméra regarde. Faire cela ne fonctionne que si votre objet se trouve en (0, 0, 0). Pas terrible… Si l’objet se trouve ailleur vous vous retrouvez avec un décallage équivalent à deux fois la distance de l’objet à l’origine. L’astuce est de donner la valeur Vector3.Zero en première paramètre et la soustraction de la poistion de l’objet par la position de la caméra (this.Position-cameraPosition) en second.


Si ce bug a été corrigé à l’heure où vous lisez ces lignes, ou bien si c’est moi qui suis bugué (pas trop le temps de vérifier en ce moment :) ) merci de me l’indiquer.


La seconde instruction se trouve dans la méthode Render :

this._effect.World = (this._activated ? this._billboardMatrix : Matrix.Identity)*this.Transform;

Ici, si le billboarding est activé nous affectons à la matrice World le résultat de la multplication de la matrice de _billboardMatrix par la matrice de transformation (position de l’objet, taille, rotation). Sinon World prend pour valeur le contenu de Transform.


Le reste du code est assez simple pour être assimilé et compris sans être présenté ici. Si vous exécutez l’application vous verrez apparaitre les parois de notre cube et le billboard au centre de celui-ci. En déplaçant la souris sans appuyer sur Enter un affichage similaire à l’animation suivante se produit :


Pas de billboard activé, le billboard tourne de manière solidaire avec le cube.


On remarque que le billboard au centre du cube tourne de manière solidaire avec le cube et se présente donc sous une infinité d’angles à la caméra. Si vous appuyez sur Espace, l’affichage change comme ceci :


Le billboard activé reste toujours face à la caméra


Cette fois, le billboard reste face à la caméra quelque soit la position de cette dernière. 


Si cet exemple illustre parfaitement le principe des billboards, il ne met pas en evidence de manière flagrante leur avantage. Ce sera l’objet de notre second Sample.


 


Second sample


Un exemple bien plus ludique nous attend. Une foret va être affichée sur un territoire valonné. Chaque arbre sera en fait un billboard et donnera l’illusion d’être un modèle 3D complexe. Aucune action utilisateur ici, le développeur admirera juste le rendu (ce qui est déjà très bien …).


Le code


Il faut, pour bien comprendre les effets de transparences utilisés ici, avoir lu le chapitre 8 jusqu’au point portant sur les effets spéciaux et blending (inclu).  


 Ici, seules deux classes ont été modifiées en profondeur, la classe Game1 et la classe Region. La première va avoir pour tâche d’afficher X billboards texturés avec une image d’arbre. Elle leur donnera une position, une taille et une couleur différente. La classe Region va simplement afficher un relief en utilisant des fonctions trigonométriques pour valonner le paysage.


Le relief


Le relief visible dans l’image ci-dessous se réalise par l’intermédiaire d’une méthode retournant une altitude en fonction d’une abscisse X et d’une ordonnée Y fournies. Le calcul se base sur les fonctions trigonométriques Cosinus et Sinus :

/// <summary>/// Simple function to define “hilliness” for terrain/// </summary>public static float HeightField(float x, float y){    return 30 * ((float)Math.Cos(x / 40 + 0.2f) * (float)Math.Cos(y / 35 – 0.2f) + 1.0f);

}



Désormais, au lieu de passer la valeur 0 en profondeur Z pour chaque vertex nous donnons le résultat de cette méthode.


Les arbres


Pour les arbres/billboards, tout se passe dans la classe Game1. Une liste générique est déclarée avec une constante indiquant le nombre d’arbres affichés.

private const int numberOfTrees = 400;
private List<Billboard> trees;

Vient ensuite l’initialisation de chaque arbre dans la liste. 

Random rand = new Random();for (int i = 0; i < numberOfTrees; i++){    Billboard tree = new Billboard();    float size = 4 + 8 * (float)rand.NextDouble();    tree.Resize(size, size, size);    do    {        int x = rand.Next(0, 512);        int y = rand.Next(0, 512);        tree.Position = new Vector3(x, y, Region.HeightField(x, y) + size);    }    while (!IsTreePositionValid(tree.Position));      tree.Activated = true;     int r = (255 – 190) + (int)(190 * (float)(rand.NextDouble()));    int g = (255 – 190) + (int)(190 * (float)(rand.NextDouble()));    int b = 255;    tree.Color = new Color((byte)r, (byte)g, (byte)b, 255);     tree.Load(this.graphics.GraphicsDevice);     trees.Add(tree);


Ici une taille aléatoire, une position aléatoire et une couleur aléatoire sont données à chaque arbre. La méthode IsTreePositionValid vérifie simplement que les arbres sont assez espacés. A chaque mise à jour, la métrice view est rafraichie et la méthode Update de chaque arbre est appelée avec la position de la caméra, le nouveau point vers lequel elle regarde et sa normale :

for (int i = 0; i < numberOfTrees; i++){    trees[i].Effect.View = viewMatrix;    trees[i].Update(gameTime, vEyePt, vLookatPt, new Vector3(0, 0, 1f));

}



Notons enfin que la liste d’arbres est re-ordonnée à chaque Update afin d’afficher les arbres dans l’ordre de leur apparition.


Le rendu final nous offre un monde possédant des arbres et de la végétation qui semble être en 3D :


 




Dernier Sample 


 Les billboards ont aussi une autre utilisation très utile : les effets graphiques et les animations. Nous allons ici modifier notre classe billboard de façon à permettre l’affichage d’un plan animé comme celui-ci:


 


 Le code


La classe Billboard se présente maintenant ainsi :

public class Billboard : TransformBase{     #region Private members     private GraphicsDevice _device;    private Color _color = Color.TransparentWhite;    private Texture2D _texture;    private BasicEffect _effect;    private bool _activated;    private Matrix _billboardMatrix = Matrix.Identity;    private int _animationRows;    private int _animationColumns;    private long _animationFrequency;    private AnimateType _animateType;    private List<VertexBuffer> _animations;    private VertexBuffer _currentAnimation;    private int _animationIndex;    private Blend _sourceBlend;    private Blend _destinationBlend;    private double _lastUpdate;     #endregion      #region Properties     /// <summary>    /// <para>Occurs when the animation is over.</para>    /// </summary>    public event EventHandler AnimationEnded;     /// <summary>    /// <para>Gets or sets a value indicating if the biilboard is activated.</para>    /// </summary>    public bool Activated    {        get        {            return this._activated;        }        set        {             this._activated = value;        }    }     /// <summary>    /// <para>Gets or sets the number of animations on a colum for the associated texture.</para>    /// </summary>    public int AnimationColumns    {        get        {            return this._animationColumns;        }        set        {            if ((value < 0) || (value > 16))            {                throw new ArgumentOutOfRangeException(“AnimationColumns”, “AnimationColumns must be filled with a value between 1 and 16″);            }            this._animationColumns = value;        }    }     /// <summary>    /// <para>Gets or sets the animation frequency/para>    /// </summary>    public long AnimationFrequency    {        get        {            return this._animationFrequency;        }        set        {            this._animationFrequency = value;        }    }     /// <summary>    /// <para>Gets or sets the number of animations on a row for the associated texture.</para>    /// </summary>    public int AnimationRows    {        get        {            return this._animationRows;        }        set        {            if ((value < 0) || (value > 16))            {                throw new ArgumentOutOfRangeException(“AnimationRows”, “AnimationRows must be filled with a value between 1 and 16″);            }            this._animationRows = value;        }    }     /// <summary>    /// <para>Gets or sets a value indicating the type of the animation.</para>    /// </summary>    public AnimateType AnimateType    {        get        {            return this._animateType;        }        set        {            this._animateType = value;        }    }     /// <summary>    /// <para>Gets or sets the Cube’s color.</para>    /// </summary>    public Color Color    {        get        {            return this._color;        }        set        {            this._color = value;        }    }     /// <summary>    /// <para>Gets or sets the destination blend.</para>    /// </summary>    public Blend DestinationBlend    {        get        {            return this._destinationBlend;        }        set        {            this._destinationBlend = value;        }    }     /// <summary>    /// <para>Gets the cube’s effect.</para>    /// </summary>    public BasicEffect Effect    {        get        {            return this._effect;        }    }     /// <summary>    /// <para>Gets or sets the source blend.</para>    /// </summary>    public Blend SourceBlend    {        get        {            return this._sourceBlend;        }        set        {            this._sourceBlend = value;        }    }     /// <summary>    /// <para>Gets or sets the cube’s texture.</para>    /// </summary>    public Texture2D Texture    {        get        {            return this._texture;        }        set        {            this._texture = value;            this._effect.Texture = value;        }    }      #endregion      #region Constructors     /// <summary>    /// <para>Empty constructors (no animations).</para>    /// </summary>    public Billboard()    {        this._animationIndex = 0;        this._animationRows = 1;        this._animationColumns = 1;        this._animateType = AnimateType.None;        this._animations = new List<VertexBuffer>();    }     /// <summary>    /// <para>Instanciate a new Billboard with an animation.</para>    /// </summary>    /// <param name=”animationRows”></param>    /// <param name=”animationColumns”></param>    /// <param name=”animationFrequency”></param>    public Billboard(int animationRows, int animationColumns, long animationFrequency) : this()    {        this._animateType = AnimateType.Loop;        this._animationColumns = animationColumns;        this._animationFrequency = animationFrequency;        this._animationRows = animationRows;    }      #endregion      #region Initialization     public void Load(GraphicsDevice device)    {        this._device = device;        this.InitializeVertices();        this.InitializeEffect();    }     private void InitializeEffect()    {        this._effect = new BasicEffect(this._device, null);        this._effect.VertexColorEnabled = true;        this._effect.TextureEnabled = true;    }     private void InitializeVertices()    {        this._animations.Clear();         float animationSizeU = (1f / this.AnimationColumns);        float animationSizeV = (1f / this.AnimationRows);         for (int column = 0; column < this.AnimationColumns; column++)        {            for (int row = 0; row < this.AnimationRows; row++)            {                VertexBuffer vertexBuffer = null;                VertexPositionColorTexture[] vertices = new VertexPositionColorTexture[4];                float tu = 0 + animationSizeU * column;                float tv = 0 + animationSizeV * row;                 vertices[0].Position = new Vector3(-1f, 1f, 0);                vertices[0].Color = this.Color;                vertices[0].TextureCoordinate = new Vector2(tu, tv);                 vertices[1].Position = new Vector3(1f, 1f, 0);                vertices[1].Color = this.Color;                vertices[1].TextureCoordinate = new Vector2(tu + animationSizeU, tv);                 vertices[2].Position = new Vector3(1f, -1f, 0);                vertices[2].Color = this.Color;                vertices[2].TextureCoordinate = new Vector2(tu + animationSizeU, tv + animationSizeV);                 vertices[3].Position = new Vector3(-1f, -1f, 0);                vertices[3].Color = this.Color;                vertices[3].TextureCoordinate = new Vector2(tu, tv + animationSizeV);                 vertexBuffer = new VertexBuffer(                this._device,                typeof(VertexPositionColorTexture),                4,                ResourceUsage.WriteOnly,                ResourceManagementMode.Automatic);                 vertexBuffer.SetData(vertices);                 this._animations.Add(vertexBuffer);            }        }         this._currentAnimation = this._animations[0];    }     #endregion      #region Render     /// <summary>    /// <para>Render the cube on the device.</para>    /// </summary>    public void Render()    {        this._effect.Begin();        this._effect.World = (this._activated ? this._billboardMatrix : Matrix.Identity) * this.Transform;         foreach (EffectPass pass in this._effect.CurrentTechnique.Passes)        {            pass.Begin();            this._device.RenderState.AlphaBlendEnable = true;            this._device.RenderState.SourceBlend = this.SourceBlend;            this._device.RenderState.DestinationBlend = this.DestinationBlend;             if (this._device.GraphicsDeviceCapabilities.AlphaCompareCapabilities.SupportsGreaterEqual)            {                this._device.RenderState.AlphaTestEnable = true;                this._device.RenderState.ReferenceAlpha = 0x08;                this._device.RenderState.AlphaFunction = CompareFunction.Greater;            }             this._device.Textures[0] = this.Texture;            this._device.Vertices[0].SetSource(this._currentAnimation, 0, VertexPositionColorTexture.SizeInBytes);            this._device.VertexDeclaration = new VertexDeclaration(this._device, VertexPositionColorTexture.VertexElements);            this._device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);             this._device.RenderState.AlphaBlendEnable = false;            this._device.RenderState.AlphaTestEnable = false;             pass.End();        }         this._effect.End();    }       #endregion      #region Public methods      public void Update(GameTime gameTime, Vector3 cameraPosition, Vector3 cameraLookAt, Vector3 cameraUpVector)    {        this._billboardMatrix = Matrix.CreateBillboard(Vector3.Zero, this.Position – cameraPosition, cameraUpVector, cameraLookAt);         if (this.AnimateType != AnimateType.None)        {            this.Animate(gameTime);        }    }     private void Animate(GameTime gameTime)    {        _lastUpdate += gameTime.ElapsedGameTime.TotalMilliseconds;        if (_lastUpdate > this.AnimationFrequency)        {            _lastUpdate = 0;            this.IncrementAnimation();            this._currentAnimation = this._animations[this._animationIndex++];        }    }     private void IncrementAnimation()    {        this._animationIndex++;         if (this._animationIndex > (this._animationColumns * this._animationRows – 1))        {            if (this.AnimationEnded != null)                this.AnimationEnded(this, EventArgs.Empty);            this._animationIndex = 0;        }    }     #endregion 

}



Etudions là point par point. Le premier ajout concerne l’arrivée de nouveaux membres permettant d’exploiter l’animation :

    private int _animationRows;    private int _animationColumns;    private long _animationFrequency;    private AnimateType _animateType;    private List<VertexBuffer> _animations;    private VertexBuffer _currentAnimation;    private int _animationIndex;    private Blend _sourceBlend;    private Blend _destinationBlend;    private double _lastUpdate;

On trouve ici dans l’ordre : le nombre d’animation en haut et largeur, la vitesse de l’animation (en millisecondes), le type d’animation (non utilisé ici), la liste de vertexbuffer (nous reviendrons sur ce membre plus loin), le vertexbuffer correspondant à l’animation courante, l’index de l’animation dans la liste de vertexbuffer, le blend source et de destination (afin de pouvoir spécifier à la création du billboard de type de blend que l’ont désire) et enfin le temps écoulé depuis le dernier update du billboard.


Deux constructeurs font leur apparition :

    /// <summary>    /// <para>Empty constructors (no animations).</para>    /// </summary>    public Billboard()    {        this._animationIndex = 0;        this._animationRows = 1;        this._animationColumns = 1;        this._animateType = AnimateType.None;        this._animations = new List<VertexBuffer>();    }     /// <summary>    /// <para>Instanciate a new Billboard with an animation.</para>    /// </summary>    /// <param name=”animationRows”></param>    /// <param name=”animationColumns”></param>    /// <param name=”animationFrequency”></param>    public Billboard(int animationRows, int animationColumns, long animationFrequency) : this()    {        this._animateType = AnimateType.Loop;        this._animationColumns = animationColumns;        this._animationFrequency = animationFrequency;        this._animationRows = animationRows;    }

Le premier existait déjà mais initialise les membres de classe en prennant en compte qu’il n’y a aucun animation à jouer (c’est le cas pour les billboard affichant les arbres et l’herbe). Le second prend en paramètre les animations en hauteur et largeur ainsique la fréquence. Il initialize les membres pour l’animation spécifiée.


La plus grosse modification vient de la méthode d’initialisation des vertices :

    private void InitializeVertices()    {        this._animations.Clear();         float animationSizeU = (1f / this.AnimationColumns);        float animationSizeV = (1f / this.AnimationRows);         for (int column = 0; column < this.AnimationColumns; column++)        {            for (int row = 0; row < this.AnimationRows; row++)            {                VertexBuffer vertexBuffer = null;                VertexPositionColorTexture[] vertices = new VertexPositionColorTexture[4];                float tu = 0 + animationSizeU * column;                float tv = 0 + animationSizeV * row;                 vertices[0].Position = new Vector3(-1f, 1f, 0);                vertices[0].Color = this.Color;                vertices[0].TextureCoordinate = new Vector2(tu, tv);                 vertices[1].Position = new Vector3(1f, 1f, 0);                vertices[1].Color = this.Color;                vertices[1].TextureCoordinate = new Vector2(tu + animationSizeU, tv);                 vertices[2].Position = new Vector3(1f, -1f, 0);                vertices[2].Color = this.Color;                vertices[2].TextureCoordinate = new Vector2(tu + animationSizeU, tv + animationSizeV);                 vertices[3].Position = new Vector3(-1f, -1f, 0);                vertices[3].Color = this.Color;                vertices[3].TextureCoordinate = new Vector2(tu, tv + animationSizeV);                 vertexBuffer = new VertexBuffer(                this._device,                typeof(VertexPositionColorTexture),                4,                ResourceUsage.WriteOnly,                ResourceManagementMode.Automatic);                 vertexBuffer.SetData(vertices);                 this._animations.Add(vertexBuffer);            }        }         this._currentAnimation = this._animations[0];    }

Cette méthode a un but simple, connaissant le nombre d’animation sur la texture à afficher, elle va créer autant de vertexbuffer que nécessaire : tous auront le même position et la même couleur, mais il auront une coordonnée de texture lié à l’emplacement de la frame sur l’image. La première instruction vide la liste de vertexbuffer. Les deux suivantes calculent le % en largeur et hauteur d’une frame sur la texture :

        float animationSizeU = (1f / this.AnimationColumns);
        float animationSizeV = (1f / this.AnimationRows);

(ou 1 == 100%). A partir de là on parcours chaque colonne et chaque ligne pour créer un vertex buffer adapté à la frame voulue. On ajoute le dis vertexbuffer à la liste. La dernière instruction fait pointer l’animation courante sur la première animation. C’est celle-ci (qui est en fait un vertexbuffer) qui sera utilisé pour l’affichage (méthode Draw)


Viennent enfin les méthodes de mise à jour du billboard :

    public void Update(GameTime gameTime, Vector3 cameraPosition, Vector3 cameraLookAt, Vector3 cameraUpVector)    {        this._billboardMatrix = Matrix.CreateBillboard(Vector3.Zero, this.Position – cameraPosition, cameraUpVector, cameraLookAt);         if (this.AnimateType != AnimateType.None)        {            this.Animate(gameTime);        }    }     private void Animate(GameTime gameTime)    {        _lastUpdate += gameTime.ElapsedGameTime.TotalMilliseconds;        if (_lastUpdate > this.AnimationFrequency)        {            _lastUpdate = 0;            this.IncrementAnimation();            this._currentAnimation = this._animations[this._animationIndex++];        }    }     private void IncrementAnimation()    {        this._animationIndex++;         if (this._animationIndex > (this._animationColumns * this._animationRows – 1))        {            if (this.AnimationEnded != null)                this.AnimationEnded(this, EventArgs.Empty);            this._animationIndex = 0;        }    }

 Ces méthodes se contentent juste de pointer sur l’animation suivante si le temps écouté depuis le dernier update est suffisant.


 L’utilisation d’un billboard animé reste similaire à ce que nous avons fait précédemment (classe Game1). Elle passe par 4 étapes :


La création et l’initialisation :

Billboard smoke = new Billboard(8,8,50);float size = 5;smoke.Resize(size, size, size);do{    int x = rand.Next(0, 512);    int y = rand.Next(0, 512);    smoke.Position = new Vector3(x, y, Region.HeightField(x, y) + size);}while (!IsTreePositionValid(smoke.Position)); smoke.SourceBlend = Blend.SourceColor;smoke.DestinationBlend = Blend.One; 

smoke.Activated = true;

smoke.Color = new Color(255, 255, 255, 255);

smoke.Load(this.graphics.GraphicsDevice);


L’affection d’une texture contenant un ensemble de frame :

            for (int i = 0; i < numberOfSmokes; i++)
                smokes[i].Texture = textures[5];


La mise à jour de la matrice de vue et l’appel à la méthode Update :

            for (int i = 0; i < numberOfSmokes; i++)
            {
                smokes[i].Effect.View = viewMatrix;
                smokes[i].Update(gameTime, vEyePt, vLookatPt, new Vector3(0, 0, 1f));
            }


et enfin, bien entendu, l’affichage :

                smokes[i].Render();

 Au final à l’affichage, on voit apparaitre des halo de fumées dans le décors :



 


 Bonus stage 


 Je me suis amusé à créer un sample dérivé du dernier pour afficher un effet de météorite s’écrasant sur terre. Admirez ce qu’il est possible de faire en une petite heure à peine avec des billboards :


 Alerte ! Une météorite fonce droit sur la terre !


Trop tard : Boum :)


 Le code de la classe météore qui s’occupe de gérer un météore (en fait un ensemble de billboard) n’est pas terrible et optimisé mais je n’avais qu’une heure  pour le faire, il  donne néanmoins une idée de ce qu’on peut faire avec du temps, et de belles images…


Le code se trouve avec les samples de ce cours.


Conclusion


Tous les jeux font appels aux bilboards. Et pour cause : ils permettent un affichage réaliste, 3D à moindre cout pour les effets spéciaux. Des sociétés comme Blizzard en sont friands (World Of Warcraft en regorge à chaque écran). Sachant que leur utilisation dans un programme est maintenant simple grâce à notre classe clé en main, pourquoi s’en priver ?


[soon]


Valentin Billotte


[Help] 


telecharger Vous pouvez télécharger les trois samples ici.   


Retourner au sommaire des cours 

21 thoughts on “Annexe : Billboard en Xna”

  1. Le second exemple avec les arbres est particulièrement impressionnant a regarder sur l’écran. Cela saccade un peu, rien d’étonnant sur un PC de 5 ans d’âge..

  2. j’ai pas trop optimisé non plus pour laisser le code lisible…
    L’exemple suivant avec des animations de fumées va être pire…

  3. Je n’ai pas trouvé de fumée dans les Samples, ni de météore. En fait, il n’y a que les deux premiers exemples dans le fichier en téléchargement.

  4. Les billboards me font penser aux monstres de DOOM 1, tous le temps “orientés” vers le joueur. Drôle d’expérience de voir un cadavre tourner sur le sol pour être toujours dans le sens de la caméra.. C’est parce que les monstres étaient dessinés sous la forme de sprites 2D plaqués sur le décor, comme les arbres de tes exemples. Il n’y avait pas de 3D dans DOOM 1, juste une fausse 3D géniale réalisée en 2D. Enfin c’était il y a 15 ans déja..

  5. euh.. je ne vois pas le rapport entre DOOM premier du nom, ancétre de tous les FPS, et un lien sur une image d’un RTS sortant fin Mars.

  6. Je pensais que les tirs étaient des effets de particles. Mais en y réfléchissant on peut trés bien dessiner les particules en 2D sur un Billboard.

  7. I have bought a new computer. All of my existing ITune songs/library etc are on my partners computer at a different location. When I plpug my Iphone into my computer to trabsfer all the data it wont allow me and I am worried that I will delete all existing songs on my IPhone. Is there an easy way to do this?

    ________________
    unlock iphone 3g

  8. Bonjour,
    Excellent tout ça mais je n’arrive pas à convertir le projet en XNA 3.1
    Une nouvelle version est-elle prévue ?
    Merci pour votre aide

  9. Encore moi … J’ai converti BilBoardTree a la mano. c’est ok. Par contre je ne suis pas certain de vraiment pouvoir utiliser cette méthode : j’essaye d’implémenter des billboard sur des terrains sphériques. Cela complique la tache car evidemment, les BB ne sont pas tous dirigés vers le “haut” ! et la vue type simulateur de vol fait apparaitre les BB en rotation sur eux mêmes, ce qui est normal quand je plonge vers le sol !!! J’ai bien peur de devoir utiliser des mesh. Une idée peut être ? merci.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>