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++){    treesIdea.Effect.View = viewMatrix;    treesIdea.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;         &nb