Sample sur le SpriteBatch

Publié le par Jérémy JANISZEWSKI

INTRODUCTION

Hier nous avons vu un cours sur le SpriteBatch, aujourd'hui, nous allons créer un programme nous permettant de voir les différences sur le BlendState du SpriteBatch. En effet, lors d'un rendu graphique, le SpriteSortMode ne permet pas de "voir" les différences lors du rendu, cela tient au fait que les sprites sont triés avant le rendu à l'écran. Donc à part mettre un chronomètre, afficher 1000 sprites, à part un temps de rendu un peu plus long, il sera exactement le même.

RECUPERATION DU PROJET

Récupérez le squelette du projet ici : link

ECRITURE DU PROGRAMME

C
omme nous l'avons vu précédemment, il existe 4 mode de Blending : Additive, AlphaBlend, NonPremultiplied et Opaque.


Nota : D'une certaine manière, AlphaBlend et NonPremultiplied s'exécute de la même façon. La différence entre les 2 tient au fait qu'AlphaBlend prémultiplie les canaux RVB avec le canal alpha (c'est ça, on "injecte" la transparence dans les 3 canaux, ce qui est en accord avec la façon dont un écran [LCD, plasma, cathodique] rend les couleurs); alors que NonPremultiplied considère que le canal alpha est indépendant des 3 autres couleurs (c'est ça, comme si l'écran en plus de posséder les canaux RGB possédait un canal alpha).
Lorsque nous utilisons la technique AlphaBlend, pour rendre transparent ou opaque une image, on utilise cette formule :
color = color * alpha
Alors qu'avec la technique NonPremultiplied, pour rendre transparent ou opaque une image, on utilise cette formule :
color = White
color.A = 0 // transparent
Je conseillerais de toujours utiliser AlphaBlend, lorsque les images proviennent du répertoire Content, mais d'utiliser NonPremultiplied si les images sont chargées en mémoire avec la méthode FromStream de la classe Texture2D. (on verra un exemple de cela, lorsque nous développerons un (mini) JukeBox).

Pour cela, nous avons définir les états du blending. Ajoutez à l'interieur de la classe Game1, cette énumération :

 enum State { Additive, AlphaBlend, NonPremultiplied, Opaque } 

Dans ce projet, pour bien voir les différences de chaque Blend, nous allons laisser le choix à l'utilisateur de pouvoir manipuler l'image affichée à sa guise. Pour cela, nous aurons besoin de connaître l'état du clavier. (Un cours sur les périphériques arrivera bientôt, donc pour l'instant pas de préoccupations par rapport au clavier). Nous aurons donc besoin de 2 variables; une qui contiendra l'état du clavier durant l'image actuellement affichée et une qui contiendra l'état du clavier durant l'avant dernière image affichée.

 // Gestion du clavier. KeyboardState lastKeyboardState; KeyboardState currentKeyboardState; 

Ensuite, il faut définir ce que l'utilisateur pourra modifier :
- Le type de rendu (Additive, AlphaBlend, NonPremultiplied, Opaque).
- L'angle de rotation de l'objet.
- L'origine de rotation de l'objet.
- L'homothétie de l'objet.
- La couleur de l'objet.
- Le degré de transparence de l'objet.
- L'effet à appliquer à l'objet (retournement vertical, horizontal, mixe des deux, aucun).

Cela nous donne ces variables :

 // Mode de mélange pour le sprite batch. State state; // Angle de rotation du sprite. float angle; // Origin of rotation Vector2 origin; // Scale of the sprite. Vector2 scale; // Color of the sprite. Color tint; // Transparency of the object. float alpha; // Sprite effects to apply on the object. SpriteEffects se; 

Il nous faudra aussi connaître la texture à rendre, la police de caractères à utiliser et le texte à rendre. Les variables sont donc les suivantes :

 // Police de caractères pour rendre du texte. SpriteFont spriteFont; // Texte à rendre. StringBuilder text; // Texture à utiliser. Texture2D texture; 

Et enfin, il nous faudra une variable pour pouvoir générer aléatoirement une couleur et une variable pour savoir si oui ou non, notre texte doit être modifiée. (Cette variable permet d'améliorer les performances en partant du principe que le texte qui n'est pas modifié n'a pas lieu d'être recréé à chaque image) 

 // Generateur de nombres aléatoires Random rnd; // Booléen pour savoir si le texte doit être mis à jour. bool buildText; 

Maintenant il convient d'initialiser ces variables, et l'endroit le plus intéressant est la méthode Initialize. (On peut aussi le faire dans le constructeur).

 /// <summary> /// Permet au jeu de s’initialiser avant le démarrage. /// Emplacement pour la demande de services nécessaires et le chargement de contenu /// non graphique. Calling base.Initialize passe en revue les composants /// et les initialise. /// </summary> protected override void Initialize() { // Etat du sprite batch mis à Additive par défaut. state = State.Additive; // Pas de rotation de l'objet. angle = 0f; // Origin de rotation : Haut-gauche de la texture. origin = Vector2.Zero; // Homothétie de 1. scale = Vector2.One; // Filtre blanc. tint = Color.White; // Object opaque. alpha = 1f; // Création du générateur de nombre aléatoires. rnd = new Random(); // Le texte doit être mis à jour. buildText = true; // Contiendra le texte à afficher. text = new StringBuilder();  base.Initialize(); } 

Ensuite, il nous faut charger notre texture et notre police de caractères. Tout chargement d'objets doit intervenir dans la méthode LoadContent. Donc dans la méthode LoadContent, ajoutons ceci :

 protected override void LoadContent() {  // Créer un SpriteBatch, qui peut être utilisé pour dessiner des textures. spriteBatch = new SpriteBatch(GraphicsDevice); spriteFont = Content.Load<SpriteFont>("Fonts/14_regular"); texture = Content.Load<Texture2D>("Textures/01"); } 

Nota : La méthode Load de Content est une méthode générique, c'est à dire que le paramètre qui se situe entre < > peut prendre n'importe quelle valeur à la condition que cette valeur  puisse être comprise par Content. Dis comme ça, ce n'est pas très compréhensible mais il faut juste comprendre que si vous créez une classe X, si vous voulez que cette classe puisse être chargé par Content, il faut que celle-ci soit Contentable. Un cours sur la façon de rendre sa classe Contentable verra le jour très bientôt sur ce blog.

Pour l'heure, voici certaines des classes qui peuvent être utilisées dans la méthode Load de Content :
- SpriteFont : charge une police d'écriture.
- Texture2D : charge une image.
- Texture3D : charge une image volumétrique.
- TextureCube : charge un cube avec une image appliquée dessus (skybox)
- Model : charge un model 3D.
- Effect : charge un effet spécial.
- Song : charge une musique.
- SoundEffect : charge un effet sonore

Et avec le cours sur le ContentPipeline, vos propres classes pourront être chargées via la méthode Load de Content, ce qui offre un nombre de possibilités quasi-infinis.

Tout objet chargé avec la méthode LoadContent doit être déchargé dans la méthode UnloadContent. Certaines classes disposent de la méthode Dispose, mais pas toutes, il faut donc faire attention ici, à ne pas se tromper.

 protected override void UnloadContent() { texture.Dispose(); texture = null; spriteFont = null; } 

Comme nous pouvons le voir, il y'a une méthode Dispose pour la texture mais pas pour la police de caractères.

Maintenant, nous allons gérer le clavier. Pour cela, il convient de définir quelles touches nous allons utiliser et à quoi elles vont servir.
- La touche ESPACE : va permettre de changer le mode de mélange.
- La touche + : augmente l'homothétie de l'objet.
- La touche - : diminue l'homothétie de l'objet.
- La touche R : tourne l'objet.
- La touche O : change l'origine de rotation de l'objet.
- La touche C : change la couleur de l'objet.
- La touche B : change l'effet à appliquer à l'objet (retournement horizontal, vertical etc...)

C'est dans la méthode Update que tout cela va être mis en place. Tout d'abord, il faut récupérer l'état du clavier durant la dernière image et l'état du clavier durant l'image actuelle.

 lastKeyboardState = currentKeyboardState; currentKeyboardState = Keyboard.GetState(); 

Pour permettre de voir le changement de mode de mélange, il sera utile de ne détecter qu'un seul appuie de touche sur ESPACE. Cela se fait en détectant si ESPACE n'est pas appuyé durant la dernière image et si ESPACE est appuyé durant l'image actuellement affichée.
Si c'est le cas, on change l'état du mélange. La formule est du type :
etat = (etat + 1) modulo (nombre(etats))
Cela signifie que l'on ajoute 1 à la valeur de l'état et que l'on divise ce résultat par le nombre d'états disponibles et que l'on prend le reste de la division entière.
Par exemple : resultat = 15 modulo 2.
On effectue tout d'abord la division : 15 / 2 = 7.5
On prend le résultat entier : 7
Puis on calcul le reste de la division : 15 - (7 * 2) = 1
Donc le résultat de 15 modulo 2 est 1.

 if (lastKeyboardState[Keys.Space] == KeyState.Up && currentKeyboardState[Keys.Space] == KeyState.Down) { buildText = true; state = (State)(((int)state + 1) % 4); } 

Ne pas oublier qu'à chaque changement de demander la reconstruction du texte. Ensuite si nous appuyons continuellement sur + ou -, nous agrandissons ou rétrecissons le sprite par une valeur de 0.1.

 if (lastKeyboardState[Keys.Add] == KeyState.Down && currentKeyboardState[Keys.Add] == KeyState.Down) { buildText = true; scale.X += 0.1f; scale.Y += 0.1f; } if (lastKeyboardState[Keys.Subtract] == KeyState.Down && currentKeyboardState[Keys.Subtract] == KeyState.Down) { buildText = true; scale.X -= 0.1f; scale.Y -= 0.1f; } 

Nous allons ensuite considérer que tant que la touche R est appuyé, le sprite tourne (et le texte est reconstruit).

 if (lastKeyboardState[Keys.R] == KeyState.Down && currentKeyboardState[Keys.R] == KeyState.Down) { buildText = true; angle = (angle + 1) % 360; } 

Là encore, nous utilisons un modulo pour garder la valeur de l'angle en 0° et 359° (360° = 0°).
Ensuite si la touche O n'est pas appuyée durant la dernière image et que O est appuyée durant l'image actuelle, alors on change l'origine de rotation (soit (0,0) qui est le bord haut-gauche du sprite, soit le milieu du sprite)

 if (lastKeyboardState[Keys.O] == KeyState.Up && currentKeyboardState[Keys.O] == KeyState.Down) { buildText = true; if (origin != Vector2.Zero) origin = Vector2.Zero; else origin = new Vector2(texture.Width >> 1, texture.Height >> 1); } 

Attention ici, nous affichons une texture complète, donc nous pouvons nous permettre de prendre la longueur de la texture et la divisée par 2 tout comme nous pouvons prendre la hauteur de la texture et la divisée par 2 aussi. Il faudra faire attention lorsque vous utiliserez une texture avec plusieurs images à utiliser le bon rectangle source.

Maintenant si la touche C n'est pas appuyée durant la dernière image et que C est appuyée durant l'image actuelle, alors on génère une nouvelle couleur pour le filtre du sprite.

 if (lastKeyboardState[Keys.C] == KeyState.Up && currentKeyboardState[Keys.C] == KeyState.Down) { buildText = true; if (state != State.AlphaBlend) tint.A = (byte)rnd.Next(0, 255); else alpha = (float)rnd.NextDouble(); tint.R = (byte)rnd.Next(0, 255); tint.G = (byte)rnd.Next(0, 255); tint.B = (byte)rnd.Next(0, 255); } 

Ici, si nous n'utilisons pas l'AlphaBlend, nous pouvons changer directement le canal Alpha sur la couleur
générant un entier entre 0 et 255. Mais si nous utilisons l'AlphaBlend, nous générons un flottant compris entre 0 et 1.
Ensuite pour les 3 composantes RGB, nous générons un entier entre 0 et 255. (puisque ces valeurs sont définies commes des objets Byte. Byte est une structure qui contient les entiers entre 0 et 255).

Et enfin, au niveau de la gestion du clavier, il ne nous reste plus qu'à gérer les effets à appliquer au sprite.

Maintenant si la touche B n'est pas appuyée durant la dernière image et que B est appuyée durant l'image actuelle, alors on change l'effet à appliquer au sprite.
SpriteEffects est une énumération qui peut être flaggé. Cela signifie que l'on peut chaîner les variables à l'interieure de cette énumération.

Par exemple notre énumération State n'est pas "flaggable", c'est à dire que si nous avons choisit AlphaBlend, alors ce sera AlphaBlend et pas autre chose alors que cette énumération :

 [Flags] public enum SpriteEffects { None = 0, FlipHorizontally = 1, FlipVertically = 2, } 

est "flaggable", cela signifie que l'on peut écrire ceci :
SpriteEffects se = SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically
l'opérateur | est un opérateur OU bit à bit. Sa fonction ressemble à une addition; ainsi si on regarde la valeur binaire de se :
FlipHorizontally = 01 (=1)
FlipVertically = 10     (=2)

se = FlipHorizontally | FlipVertically
se = 01 | 10 = 11
Donc se a la valeur 3.     
Pour connaître si une valeur de l'énumération est présente dans se, on utilise l'opérateur &. Cet opérateur renvoie 1 si A et B valent 1, sinon il renvoie 0. Donc pour savoir si FlipHorizontally est dans se, on fait :
resultat = se & FlipHorizontally.
Si resultat est différent de 0, alors FlipHorizally est dans se.

 if (lastKeyboardState[Keys.B] == KeyState.Up && currentKeyboardState[Keys.B] == KeyState.Down) { buildText = true; if ((se & SpriteEffects.FlipHorizontally) != 0 && (se & SpriteEffects.FlipVertically) != 0) se = SpriteEffects.None; else if (se == SpriteEffects.None) se = SpriteEffects.FlipHorizontally; else if (se == SpriteEffects.FlipHorizontally) se = SpriteEffects.FlipVertically; else if (se == SpriteEffects.FlipVertically) se = SpriteEffects.FlipVertically | SpriteEffects.FlipHorizontally; }

BuildText()

Maintenant il nous faut construire le texte à rendre, pour cela créez une méthode BuildText. Dans cette méthode, il faut détecter si le texte à besoin d'être mis à jour. Si c'est le cas, il faut recréer le texte et interdire les prochaines mises à jour jusqu'à ce que le besoin s'en fasse sentir.

 private void BuildText() {  // Si le text a besoin d'être mis à jour, if (buildText) { // A la prochaine passe, il n'aura plus besoin de l'être. buildText = false; // Efface le texte. text.Clear(); // Ajoute le type de blend. text.AppendFormat("Type de blending : {0}\n", state); // Ajoute l'homothétie. text.AppendFormat("Homothetie : {0}\n", scale); // Ajoute l'angle de rotation. text.AppendFormat("Rotation (en degres) : {0}\n", angle); // Ajoute la position de l'origine. text.AppendFormat("Origine de rotation (coordonnees de texture) : {0}\n", origin); // Ajoute la couleur. text.AppendFormat("Couleur : Alpha -> {0}, Rouge -> {1}, Vert -> {2}, Bleu -> {3}\n", tint.A, tint.R, tint.G, tint.B); // Ajoute le sprite effects. text.AppendFormat("Sprite effects : {0}", se); } } 

Et enfin, le plus beau pour la fin, l'affichage de ce petit monde. Ici, nous allons définir le mode de blending en fonction de la variable state.

 protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); switch (state) { case State.Additive: spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive); break; case State.AlphaBlend: spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend); break; case State.NonPremultiplied: spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); break; case State.Opaque: spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque); break; } spriteBatch.Draw(texture, new Vector2((GraphicsDevice.Viewport.Width - texture.Width) / 2, (GraphicsDevice.Viewport.Height - texture.Height) / 2) + origin, null, state == State.AlphaBlend ? tint * alpha : tint, MathHelper.ToRadians(angle), origin, scale, se, 0f); spriteBatch.DrawString(spriteFont, text, new Vector2(20, 20), Color.White); spriteBatch.End(); base.Draw(gameTime); } 

La texture est affichée au milieu de l'écran de jeu (vous pouvez retirer la variable origine dans le premier new Vector2(...) pour voir ce que cela fait. En règle générale si l'on positionne l'origine au milieu de l'objet, il faut ajouter cette origine à la position de l'objet pour être sur qu'il est positionné correctement.
Vous voyez de même que si on est dans le mode AlphaBlend, on multiplie la valeur de transparence avec la couleur alors que sinon on utilise juste la couleur. Vous pouvez vérifier ce principe en mettant le mode AlphaBlend et en appuyant sur C jusqu'à avoir un sprite transparent. Si vous regardez la composante A de la couleur, elle restera fixée à 255 (alors qu'en mode NonPremultiplied, elle serait à 0). Cela démontre qu'en utilisant AlphaBlend, la composante Alpha de la couleur n'est pas utilisée.

Ce cours est maintenant terminé, le prochain aura pour cadre la gestion des périphériques sous XNA.

Stay tuned.

@ bientôt sur ce blog...

Publié dans XNA

Pour être informé des derniers articles, inscrivez vous :
Commenter cet article