Vista : Les premières FAUSSES rumeurs

Comme à chaque sortie d’un produit MS, la diabolisation se fait sentir.


Les bétises lancées sur le net tel ce site : http://badvista.fsf.org/ sont stupides et ne servent que les extrémistes des autres bords en mal de combats puérils. Quand on veut comparer un produit on le fait dans les règles de l’art en argumentant ce ce qu’il y’a lieu d’être. Déjà avec Xp on avait eu ces messages débiles.


 J’ai déjà recu des mails de plusieurs lecteurs qui exprimaient leur crainte en ayant lu ça et là qu’on ne pouvait pas télécharger de contenus multimédia avec Vista. C’est bien évidemment faux. J’ai Vista et je peux le confirmer. Microsoft France a envoyé un message à tous les Mvp que je transmets ici :


 


Qu’on se le dise, Windows Vista est la solution idéale pour profiter de vos fichiers musicaux ou vidéo ! Malgré cela, vous avez peut-être lu des messages ou informations laissant entendre que Windows Vista ne permettait pas la lecture ou la copie de vidéos téléchargées sur le Web. Il s’agit en fait de rumeurs infondées, et nous vous invitons à vous rendre sur notre site pour plus de précisions. Tous les fichiers précédemment lisibles avec Windows XP le sont bien évidemment encore avec Windows Vista. Comme auparavant, seuls les documents protégés par leurs auteurs ou propriétaires peuvent éventuellement être soumis à des restrictions décidées par ces derniers. Destiné à vous faire vivre des expériences visuelles et musicales inoubliables, Windows Vista n’a clairement pas vocation à restreindre votre plaisir !


Nous avons prévu de communiquer sur ce sujet vers nos clients et partenaires dans les jours qui viennent. En attendant nous pensions utile de vous en faire part pour que vous puissiez dès à présent adresser vos réseaux respectifs dans le but de rétablir la vérité quant aux affirmations fausses propagées sur le Net.


 


Rendez vous sur la page :


Les questions fréquentes sur Windows Vista 


l’une des questions porte sur cette fausse polémique et y répond fort bien. Qu’on se le dise, le Multimédia sous Vista offre plus de possibilités que sous Xp sans restrictions.

Tutoriaux Xna Game Studio : sommaire général

(Tutoriaux adaptés au Xna Game Studio 3.1 et 4.0)

Première partie : Apprentissage

 XNA Tutorial 1 : Installation et Configuration de l’environnement

 XNA Tutorial 1 Windows Phone : Installation et Configuration de l’environnement

http://msmvps.com/photos/valentin/images/486730/619x480.aspx XNA Tutorial 2 : Premier programme

 XNA Tutorial 3 : Introduction aux vertices et effets

 XNA Tutorial 4 : Les matrices

 XNA Tutorial 5 : Les matrices et les Transformations

 XNA Tutorial 6 : Les indices

 

Deuxième partie : 3D réaliste

 

 XNA Tutorial 8 : Les textures

 

Annexes

 

 Annexe : Intégration de Xna dans WPF

 Annexe : Transformations et Matrices

 Annexe : Billboard en Xna

 Annexe : Progressive Mesh en Xna

 

Livres Blancs

 

  Livre blanc : Affichage de terrains intelligents à l’aide de la technique Billod

  Livre blanc : Le XNA Framework Content Pipeline

 

Criminalité…

Ca me désolé de plus en plus. On est vraiment dans un état de non droit. Je vis dans le 93 dans l’une des pires villes du département. On voit de moins en moins la police et de plus en plus les bandes de fous.


Je me demande vraiment où on va quand je regarde les infos et que je vois ce qui s’est passé à la gare du nord hier… Des deux cotés (police et débiles) on a tord, la police interpelle sans aucun respect, meme si la personne en question était violente, les jeunes frappent et détruisent par plaisir… Où est le droit ? Où est ce qu’on va ?


 Mon beau frêre me racontait hier une anecdote qui fait froid dans le dos : il travaille dans un magasin de jeux vidéos (il ne développe pas lui…). La police se trouvait devant chaque magasin de jeu du centre commercial où il se trouve et accompagnait les clients qui avaient acheté une PS3 jusqu’à leur voiture…


J’ai vraiment peur de lâcher mon fils dans la nature

La Xbox s’invite à la soirée de lancement de la Ps3…

… qui a franchement été un gros bide.


Sur la fanc des champs : un acheteur pour la soirée, sur la péniche Sony spécialement louée pour l’occasion 100 acheteurs au lieu de 1000. Un article du Monde relate cela très bien avec en bonus deux vidéos, dont une très marrante :)

http://www.lemonde.fr/web/article/0,1-0@2-651865,36-886936@51-886104,0.html

 Ca fait quand même mal au coeur pour Sony


(a noter que Sony a tenté le même coup lors du lancement de la PS3, c’est de bonne guerre :) )

Livre blanc : Le XNA Framework Content Pipeline

Le XNA Framework Content Pipeline est la clé maitresse du XNA Game Studio Express. Il permet la gestion contrôlée du “contenu” des applications tournant avec Xna (par contenu ici nous entendons “données de jeux”). Le contenu dans le monde des jeux vidéos est un paradoxe malheureux : s’il s’agit sans aucun doute de la partie la plus importante d’une application multimédia, mais c’est aussi un ensemble d’aspects difficiles à manipuler. Importer et gérer des données dans un jeu n’est vraiment pas aisé : que ce soit la recherche d’un outil pour créer du contenu, ou des outils pour les gérer,  ou l’importation de ce contenu, en passant par sa manipulation et son affichage de manière correcte à l’écran, le développeur de jeux doit faire face à un très grand nombre de difficultés et de contraintes. En fait, la plupart des grands studios de création de jeux possèdent une équipe de développement exclusivement dédiée à cette tâche.

Avec le XNA Framework Content Pipeline, la donne est différente et surtout optimisée : nous sommes en présence d’un framework extensible et paramétrable en fonction de vos besoins que ce soit pour la création de votre contenu ou bien pour sa gestion dans un moteur de jeu. Il est souvent rébarbatif d’avoir à consacrer beaucoup de temps à l’infrastructure du contenu dans son application alors qu’on préférerait utiliser ce temps exclusivement au développement du jeu. Le XNA Framework Content Pipeline vous permet justement de ne faire que ça !

Présentation du Content Pipeline

 Le XNA Framework Content Pipeline est un ensemble de composants ayant chacun un rôle bien précis à jouer dans la gestion de contenu. Il intervient deux fois dans la vie d’une application. Tout d’abord, à la compilation : il exploite les fichiers de contenus directement sortis des outils de créations (fichiers x, Bitmap, Fbx, Png, Fx, 3Ds …) afin d’adapter leurs données à un format maléable par le développeur. Ensuite, au moment du runtime, il est responsable du chargement de ces données à l’intérieur de classes métiers (classes Model, Texture, Texture2D, CompiledEffect …).

 La gestion du contenu avec le XNA Framework Content Pipeline est différente de ce que le développeur C# “classique” peut connaître ; C’est votre outil de développement, à savoir Visual Studio Express (VSE) qui va gérer ces données. Ainsi on ajoute du contenu sous VSE de la même manière qu’on ajoute un fichier source. Opérer de façon similaire pour n’importe quel élément d’un projet permet une consistance et une organisation de vos solutions Xna bien plus optimisée. A chaque ajout de contenu, le développeur doit spécifier deux “outils”. L’importer, qui axe plus son travail sur la première partie compilation de contenu et le processor qui intervient à la fois à la compilation mais aussi au runtime.

 “L’Importer”

 Lorsque vous ajoutez du contenu, il est nécessaire de spécifier un “importer”. L’Importer est responsable de l’importation des données, mais aussi du respect de leur cohésion. Il prend les fichiers que vous avez sauvegardés ou exportés depuis votre outil de création et les importe dans Visual C# Express. Nous verrons cela plus en détail au fil de la lecture. A ce stade, il est important de comprendre que ce qui nous interesse, ce sont les données importées et non l’outil qui les a créées.

 Plusieurs Importers de base existent dans le framework Xna. Le tableau suivant les énumère :
 

Figure a.

Tous ces importers sont construits sur le même schéma. C’est le XNA Framework Content Pipeline qui permet cette cohésion. L’équipe Xna de Microsoft a choisi de créer ceux qui seraient les plus utiles aux développeurs. Elle a donc choisi les formats les plus répandus mais aussi ceux qui seraient les plus utiles comme le FBX d’Autodesk qui est reconnu par la grande majorité des outils professionnels 3D mais aussi par les outils shareware et freeware. L’une des raisons du présent document est de nous apprendre à créer nos propres importers.
Lorsque l’importer a effectué sa tâche, les données existent dans un espace de contenus DOM. Le terme DOM est utilisé parce qu’il représente un ensemble de classes assimilable à un schéma (tout comme un fichier Xml). Les données importées sont en fait un ensemble de données fortement typées qu’il est possible de manipuler en tant que modèle objet à l’aide du langage C# ;  Une série de vertices ou de textures se manipule et se charge de la même manière, quel que soit le fichier à partir duquel on les a importés. On passe donc d’une infinité de format de données, à un modèle objet standard et unique.

Remarque : Avec un peu d’imagination on peut comparer ce processus  au passage de n’importe quel langage .Net (VB.Net, C#, Managed C++) à un code MSIL unique.

La figure b situe plus en détail l’importer dans ce processus : 

 

 

Xna Content Pipeline : Importer
Figure b.

 

On peut facilement imaginer ici le premier avantage de Xna ; nous avons dans cette image plusieurs types de fichiers de modèles 3D (X, Fbx et Ply). Chacun des formats associés à ces types expriment la notion de vertices, de normales ou encore de coordonnées de texture de manières différentes : si Ply et X peuvent être en ascii ou binaire, Fbx est uniquement binaire. Ply exprime les données de manière linaire alors que 3ds et X sont complètement hiérarchisés, de même chaque format exprime de manière différente les vertices, les normales, les couleurs et pour généraliser toutes données 3D. Pour résumer, ces formats n’ont rien à voir. Ainsi, sans le Xna Content Pipeline il faudrait pour le développeur créer à la fois : autant de classes que de formats à supporter, un modèle objet pour supporter les données extraites (un par format ?) et un ensemble de classes métiers pour exploiter ces données au runtime (affichage, son, …). Au final c’est une opération très fastidieuse et coûteuse en temps… Avec le Xna Content Pipeline, la donne est tout autre : le développeur ne crée qu’un importer par type de fichier. Chaque importer sait parfaitement lire les données au format du fichier auquel il est associé. Comment stockent-ils leurs données ? La réponse est simple. Quel que soit le format dont ils sont issus, les modèles 3D partagent des “composantes” communes. Ils sont notamment définis par un ensemble de vertices, de coordonnées de textures, de couleurs ou de normales. Xna propose donc un format structuré et hiérarchique dans lequel chaque importer peut stocker les données d’un modèle 3D. Au final, à partir de formats hétéroclites on obtient un ensemble de données hiérarchisées et uniformisées. Ce format générique fait partie intégrante du DOM. On trouve de base plusieurs formats de stockages génériques le tableau suivant les énumère :

 

Xna Content Pipeline : Types de stockage de données inclus de base dans le Framework
Figure c.

 

Deux informations à noter ici : Premièrement, MeshContent hérite de NodeContent et se spécialise plus dans le stockage hiérarchique de données liées aux modèles 3D. Deuxièmement, le développeur peut bien entendu étendre ce modèle et développer lui-même un format de stockage de contenu pour le DOM (nous verrons celà).

 

L’avantage du Content Pipeline ici est clair : nul besoin de développer une classe par types de fichiers pour exploiter ces données : puisque ces dernières sont toutes stockées de la même manière dans un format connu, une seule et même classe métier peut être utilisée pour les exploiter. Cette dernière doit simplement savoir lire l’une des classes de stockage énumérées dans le tableau ci-dessus.

 

C’est le processor qui va exploiter ces données et charger les classes métier.

 

Le Processor

 

Un Processor prend les données à partir du contenu DOM et crée un objet clé en main (métier) utilisable au moment de l’exécution. Cet objet peut être une simple forme 3D ou un assemblage de plusieurs objets issus de plusieurs Processor.

 

Le XNA Framework Content Pipeline contient de base des processors qui, à partir d’un contenu situé dans le DOM, peuvent créer un Model (objet simple texturé), un Effect, un Material ou une Texture2D (pour les sprites ou pour les Model). Il n’est plus nécessaire à partir d’un de ces objet de se soucier de problématiques liées au VertexBuffer, aux texels d’une texture ou à l’agencement de triangles pour l’affichage : Xna s’en charge pour nous.

Les Processors ont été conçus de telle manière qu’ils peuvent être implémentés, utilisés et partagés facilement. Le Processor chargeant les données ne se préoccupe pas de l’origine de ces données (.X, .FBX, .TGA …) dans la mesure où le DOM lui donne un accès à un modèle objet pré formaté et unifié. Le Processor offre en outre un ensemble de fonctionnalités permettant une manipulation puissante et aisée de ces données (fonctionnalités souvent issues de D3DX). Nous parlions de la génération de mipmaps, cette fonctionnalité est accessible au moment du processing.

Le schéma suivant (figure d) récapitule ce que nous venons de voir :

Xna Content Pipeline : Processor
Figure d.

 Le processor extrait les données dont il a besoin : un processor générant un modèle 3D prendra entre autres choses, les vertices et les indices à partir du DOM, un processor pour texture prendra plutôt les agrégats de pixels. Toutes les données contenues dans le DOM ne sont pas placées de manière anarchique dans celui-ci. Lorsqu’elles sont extraites d’un fichier (.x, .ply.tga …) elles sont stockées dans le DOM en partageant une identité commune liée au fichier d’origine. A partir de ces données, le Processor charge une classe directement utilisable par le développeur dans ses applications. Il est important de bien distinguer la classe, des données qu’elle possède. La classe se trouve dans les dll associées à l’application (dll du framework ou vos propres dlls si vous avez développé votre propre modèle). Les données seront “sérialisées” en un fichier XNB. C’est là le travail réalisé par le compilateur de contenu. A l’exécution (runtime) le content manager va charger les données des fichiers XNB dans les classes.

Remarque : Les fichiers Xnb sont binaires et ne sont absolument pas conçus pour être interropérables avec un acteur extérieur. Leur usage est dédié exclusivement au Content Manager.

  Les données que lisent les processors ont une importance capitale. Un processor ne lit et ne produit qu’un type de données bien précis. Nous avons vu dans le tableau précédent les différents types de base pour le stockage de données. Le type d’objet en entrée doit correspondre à un type présent de base dans le framework du Content Pipeline ou bien être défini par le développeur. Dans ce dernier cas, le développeur devra développer un processor capable de lire ce type dans le DOM. Les processors inclus dans le framework utilisent donc des types ce même framework. Le tableau suivant les énumère :

  • ModelProcessor : prend en entrée un NodeContent (classe représentant une information hiérarchisée – pratique pour les formes 3D) et renvoie un objet de type ModelContent.
  • EffectProcessor : prend en entrée un EffectContent et renvoie un objet de type CompiledEffect.
  • ModelTextureProcessor prend en entrée un TextureContent et renvoie un objet de type TextureContent.
  • TextureProcessor prend en entrée un TextureContent et renvoie un objet de type TextureContent.
  • MaterialProcessor  prend en entrée un MaterialContent et renvoie un objet de type MaterialContent.
  • SpriteTextureProcessor prend en entrée un TextureContent et renvoie un objet de type TextureContent.

Les classes ModelContent, CompiledEffect, TextureContent, MaterialContent ne sont pourtant pas des classes métier directement utilisables par le développeur comme le sont les classes Model, Effect ou bien encore Texture. Pourquoi ne pas utiliser ces classes directement en sortie du Processor ? Il y’a à celà deux raisons : d’abord pour économiser de la place, si vous regardez la classe Effect vous remarquerez qu’elle possède une demi-douzaine de propriétés qui n’ont aucun interet à être sérializées ; il est largement suffisant de mettre le code binaire dans le fichier Xnb en lieu est place d’un objet Effect créé, chargé et lourd à manipuler en sachant que cette étape peut être faite au runtime. Ensuite il faut savoir que les objets métiers sont souvent liés au device qui les utilise, un son est lié à la carte sonore qui va le jouer, une texture à la carte graphique qui va l’afficher. Ces objets ont ainsi souvent besoin lors de leur création de posséder un accès vers le device. Hors ce device n’est evidemment pas accessible au moment de la compilation mais bien au runtime.

ContentTypeWritter et ContentTypeReader 

 Ces deux acteurs vont spécifiquement travailler sur le fichier Xnb. Le writter va etre appellé par le compilateur de contenu lorsque le processor aura renvoyé son résultat. Il va serialiser les propriétés de ce resultat et les inscrire dans le fichier Xnb. Il est la dernière étape de la compilation de contenu. A l’opposé, au runtime, le reader va lire les données inscrites dans le fichier Xnb et construire un objet métier.

Build de contenu
 
La construction du contenu est donc une partie non négligeable qui doit être prise en compte lors du build. Parce que ce contenu se trouve dans l’environnement de Visual Studio Express, lorsque vous lancez un build il se trouve lui aussi “compilé” et sauvegardé sur le disque, prêt à être utilisé lors de l’exécution. Ce build passe par quatres étapes :


Figure e.

  1. Lecture du fichier par l’importer. Il agence, ordonne et classe les données puis les stocke dans un modèle objet (DOM). Toutes les données sont liées par un même identifiant ou “asset name”.
  2. Le processor prend les données dans le DOM et crée un objet formaté qui sera serialisé facilement en stockant les données essentielles.
  3. Le compilateur de contenu sérialise l’objet formaté à l’intérieur d’un fichier xnb.
  4. Au run time les données stockées dans ce fichier sont utilisées pour charger un objet métier par l’intermédiaire du Content Manager.

Les étapes 1, 2 et 3 font partie intégrante de la compilation de contenu. Seule l’étape 4 s’exécute lors de l’exécution. 

La compilation de contenu est intelligente, si vous changez un seul élément en entrée (comme une texture), le processus de Build ne reconstruit que les items liés à cette texture.

  
Le Content Manager s’occupe de charger les données depuis le disque au moment de l’exécution. Il effectue cette tâche de manière rapide et discrète en offrant une interface au développeur on ne peut plus simple. L’instruction suivante en est la preuve :
 

 

 

ContentManager myManager = new ContentManager(GameServices);

model = myManager.Load<Model>(“ship”);

 

où “ship” est l’identité de l’objet à charger.

 

 

La pratique

 

 

Nous verrons quatre exemples pour s’interroperer avec le content pipeline :

 

Le premier exemple nous fera créer notre propre importer. Le second nous fera découvrir le développement d’un processor en nous faisant travailler sur les 4 étapes énumérées ci-dessus. Nous verrons comment surcharger un Processor existant et comment modifier la manière dont il s’exécute et enfin comment améliorer le débuggage lors de la compilation de contenu.

 

 

Developper un Importer (Supporter un type existant)

 

Si les Importers de base inclus dans le framework Xna permettent de charger les types de contenus les plus courants, il est bien souvent nécessaire de développer soit même un importer pour supporter un type de fichiers non reconnu. Ce point aborde la procédure à suivre en prenant pour exemple le chargement de fichiers de type ply.

Remarque : ce tutoriel correspond à l’étape 1 de la figure e ci-dessus.

 

Les fichiers ply    

 

Bien que ce ne soit pas le sujet de cet article, il est tout de même nécessaire de présenter succintement le format ply. Nous n’allons pas développer un reader ply poussé mais juste de quoi nous permettre d’afficher à l’écran un modèle 3D. 

Les fichiers ply possèdent un format relativement simple qui explicitent un modèle 3d en énumérant les faces qui le contiennent. Les fichiers ply sont découpés en deux parties : le header qui donne diverses informations et en premier lieu les propriétés des vertices du modèle et le nombre de faces et une seconde partie qui contient les données à utiliser pour afficher le modèle. La définition et la structure de ces données sont déclarées dans le header. Pour plus de clarté voici un fichier ply simple définissant un cube :

 

ply
format ascii 1.0
comment author: Valentin Billotte
comment object: Mon cube à moi que j’ai
element vertex 8
property float x
property float y
property float z
property red uchar

property green uchar
property blue uchar
element face 12
property list uchar int vertex_index
end_header
0 0 0 255 0 0
0 0 1 255 0 0
0 1 1 255 0 0
0 1 0 255 0 0
1 0 0 0 0 255
1 0 1 0 0 255
1 1 1 0 0 255
1 1 0 0 0 255
3 0 1 2
3 0 2 3
3 3 2 4
3 3 4 5
3 5 4 7
3 5 7 9

3 6 7 1
3 6 1 0

3 6 0 3
3 6 3 5

3 1 7 4
3 1 4 2

 

Compliqué au premier abord et pourtant trivial quand on a compris le principe. Tout d’abord le header :

 

ply
format ascii 1.0
comment author: Valentin Billotte
comment object: Mon cube à moi que j’ai
element vertex 8
property float x
property float y
property float z
property red uchar

property green uchar
property blue uchar
element face 12
property list uchar int vertex_index
end_header

 

Il contient obligatoirement les trois caractères ‘p’, ‘l’, ‘y’ en entrée. Il indique ici le format du fichier est ascii (fichier texte, il y’a aussi un format binaire possible). Viennent ensuite deux commentaires (un commentaire est une ligne qui commence par “comment”). Une ligne commençant par “element” définit un type de données. Ici le type de données se nomme vertex et est contenu 8 fois dans le document. Les lignes commençant par proprerty explicitent le type précédement déclaré. Ici, un vertex est donc l’assemblage de trois flottants nommés x, y et z et de trois uchar nommés red, green et blue. Un autre type est déclaré (face) répété 12 fois il se présente comme une liste d’entier int. Le type uchar dans la ligne

 

 property list uchar int vertex_index

 

 représente en fait le nombre d’éléments dans la liste. Les données qui viennent dans le header sont donc maintenant parfaitement compréhensibles :

 

0 1 1 255 0 0

 

ici, (x, y, z) vaut (0, 1, 1) et (red, green, blue) (255, 0, 0). De même :

 

3 1 7 4

 

Indique que notre liste contient trois éléments et que la face représentée par cette ligne se compose du second vertex (1), du huitième vertex et du cinquième vertex.

Bien évidemment nous n’allons pas développer ici un parseur de fichiers ply complexe mais juste de quoi extraire les différents vertex et les différents indices des faces.

 

 

Première étape : développement de l’Importer

 

 

Où stocker le code de notre importer ? Sans trop réfléchir nous pourrions imaginer le mettre dans l’un des projets contenant le code de notre application Xna. Ce serait une erreur pour deux raisons :

 

  • Tout d’abord un importer n’est pas propre à une application ou à un ensemble de fonctionnalités mais à un type de fichier. Il convient donc de lui donner un projet propre.
  • Ensuite comme le montre le schéma précédent, l’importer officie en aval de la compilation des fichiers sources, il a donc terminé sa tâche lorsque le code est analysé par le compilateur. Sa présence dans un fichier de code source “métier” peut être qualifée d'”anachonique”.

 Il convient donc de créer un projet propre à l’importer et aux classes directement liées. D’ailleur, si vous vous rendez dans le répertoire d’installation du framework Xna. Vous trouverez un dossier nommé References dont le contenu est le suivant :

 

 

Chaque importer inclus de base dans le framework Xna (voir première image de cet article) possède sa propre dll.

 

Passons aux choses sérieuses. Commencez par créer un nouveau projet Xna :

 

 

Cliquez droit sur la solution qui vient de s’afficher pour ajouter un nouveau projet de type bibliothèque de classe pour Xna (Windows Game Library).

 

 

Donnez au projet le nom “PlyImporter” et validez.

 

 

Votre solution contient à présent deux projets. Le premier correspond à votre jeu. Nous l’utiliserons pour afficher le modèle importé depuis un fichier ply. Le second projet va contenir la définition de notre importer.

Renommez la classe créée par défaut dans le projet PlyImporter en “PlyImpoter.cs” (cette classe porte le nom “Class1.cs”). Cette action provoque généralement le renommage de la classe contenue dans le fichier. Si ce n’est pas le cas, renommez vous-même la classe en PlyImporter. Avant de continuer plus en amont il est nécessaire de rajouter une référence vers la dll correspondant au framework du Xna Content Pipeline. Cliquez droit sur le node References et selectionnez “Add new Reference”. Dans l’onglet “.Net” de la fenêtre qui s’ouvre alors sélectionnez la dll du Xna Content Pipeline :

 

 

puis validez. Nous sommes prêts à écrire notre importer. Commencez par ajouter deux using pour utiliser les classes du framework au début du fichier PlyImporter.cs :

 

using Microsoft.Xna.Framework.Content.Pipeline.Graphics;

using Microsoft.Xna.Framework.Content.Pipeline;

 

 

Modifiez ensuite le code de la classe PlyImporter comme ceci :

  

[ContentImporter(“.ply”, CacheImportedData = true, DisplayName = “Fichiers Ply”, DefaultProcessor = “ModelProcessor”)]

public class PlyImporter : ContentImporter<NodeContent>

{

 

}

 

 L’attribute ContentImporter  fournit des métadonnées à Visual Studio pour exploiter correctement l’importer dans la phase d’extration des données du fichier ply vers le modèle DOM. Nous indiquons ici plusieurs informations. Tout d’abord, l’importer se destine aux fichiers avec l’extention “.ply”. Il est tout à fait possible de spécifier d’autres types ; par exemple, la classe TextureImporter incluse de base dans le framework et qui importe les formats d’images les plus connus se présente de cette manière :

 

[ContentImporter(new string[] { “.bmp”, “.dds”, “.dib”, “.hdr”, “.jpg”, “.pfm”, “.png”, “.ppm”, “.tga” }, DisplayName = “Texture – XNA Framework”, DefaultProcessor = “SpriteTextureProcessor”)]

public class TextureImporter : ContentImporter<TextureContent>

{

    //…

}

 

chaque extension supportée par l’importer est séparée par une simple virgule. Vient ensuite la propriété CacheImportedData. Mise à true, les données extraites sont temporairement sauvegardées dans un fichier Xml (facilement exploitable donc). Le content pipeline utilise ce fichier pour accélerer le processus de compilation de contenu. Il peut être aussi utilisé pour le débugging. Par défaut cette valeur vaut false. La propriété DisplayName spécifie le nom de l’importer dans l’outil de développement. Pour  nous avons :

 

DisplayName = “Texture – XNA Framework”

 

Ainsi, sous Visual Studio 2005 si vous sélectionnez un contenu, dans la fenêtre de propriété se trouve un champs “Importer”. Si vous expandez la combo box associée vous retrouvez ce libellé :

 

 

 Enfin pour terminer à l’aide de la propriété DefaultProcessor le processor associé par défaut dans le content pipeline est spécifié. C’est lui qui va prendre les données placées dans le DOM par l’importer pour charger une classe utilisable par le développeur. Les fichiers ply contenant la définition de modèles, le processor sera bien évidemment un processor pour modèles (“ModelProcessor”).

 

 Notre classe PlyImporter hérite de ContentImporter<NodeContent>. Cette dernière définit les membres et méthodes que doivent posséder les Importers pour être utilisés par le Content Pipeline. Cliquez droit sur ContentImporter et sélectionnez “Implémenter une classe abstraite”.

 

 

 

 Cette action ajoute à notre classe PlyImporter les membres de la classe abstraite ContentImporter qu’elle doit obligatoirement implémenter. Elle se présente maintenant ainsi : 

 

[ContentImporter(“.ply”, CacheImportedData = true, DisplayName = “Fichiers Ply”, DefaultProcessor = “ModelProcessor”)]

public class PlyImporter : ContentImporter<NodeContent>

{

 

    public override NodeContent Import(string filename, ContentImporterContext context)

    {

        throw new Exception(“The method or operation is not implemented.”);

    }

}

 

 

La méthode Import est appelée automatiquement lorsque que le Build de contenu désire extraire les données du fichier concerné pour les placer dans le DOM. Elle prend en entrée deux paramètres. Une string contenant le path vers le fichier de données et un objet lié à la journalisation du processus d’import. La tâche que va accomplir la méthode est évidente : elle va lire le fichier, en extraire des informations et charger un ensemble objet structuré. La classe ContentImporter est générique. Sa spécificité est précisée par le type NodeContent. Il représente la structure objet de stockage qui sera renvoyée après lecture du fichier de données. C’est cette structure objet que le processor va lire pour créer une classe prête à l’emploi par le développeur. Ce dernier n’aura donc jamais à manipuler un objet de type NodeContent mais plutot une instance de Model, de Texture2D ou de n’importe quel type haut niveau (voir l’énumération des processors plus haut). Il est possible de donner une specificité avec n’importe quel type possible. Les processors sont conçus pour lire des types intermédiaires bien précis. La classe ModelProcessor représente un processor capable de lire un NodeContent en entrée et de produire un Model en sortie. Notre Importer devra donc être capable d’injecter dans le DOM un objet NodeContent rempli avec les données extraites du fichier ply. La méthode Import se présente comme ceci :

 

public override NodeContent Import(string filename, ContentImporterContext context)

{

    if (!File.Exists(filename))

    {

        object[] objArray1 = new object[] { filename };

        throw new FileNotFoundException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.FileNotFound, objArray1), filename);

    }

 

    StreamReader sr = null;

    NodeContent content1 = null;

    FileInfo info1 = new FileInfo(filename);

    ContentIdentity m_ContentIdentity = new ContentIdentity(info1.FullName, Properties.Resources.PlyImporterName);

 

    if (!IsFileFormatGood(info1))

    {

        object[] objArray1 = new object[] { filename };

        throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.FormatNotGood, objArray1));

    }

 

    try

    {

        this.ResetInternalVariables();

        sr = this.OpenPlyFile(info1);

 

        this.AnalizeHeader(sr, info1);

 

        content1 = this.ReadData(sr, info1);

    }

    finally

    {

        this.ClosePlyFile(sr);

        this.Cleanup();

    }

    return content1;

}

 

 

 

Pour résumer, elle s’assure que le fichier est valide, lit le header pour déterminer le nombre de vertices et le nombre de faces à lire. Elle charge ensuite un objet de type NodeContent à partir d’une méthode nommée ReadData. C’est sur cette dernière qui doit se porter notre attention :

 

private MeshContent ReadData(StreamReader sr, FileInfo info)

{

    int index = 0;

    System.Globalization.CultureInfo ci = System.Globalization.CultureInfo.InstalledUICulture;

    System.Globalization.NumberFormatInfo ni = (System.Globalization.NumberFormatInfo)ci.NumberFormat.Clone();

    ni.NumberDecimalSeparator = Properties.Resources.NumberDecimalSeparator;

    string line = string.Empty;

    MeshContent content = new MeshContent();

 

    System.Diagnostics.Trace.WriteLine(“” + content.Positions.Count);

 

    GeometryContent geometry = new GeometryContent();

    //nous lisons les vertices en premier

    bool currentlyReadVertices = true;

    while ((line = sr.ReadLine()) != NullValue)

    {

        string[] properties = line.Trim().Split(Properties.Resources.Space.ToCharArray());

 

        if (currentlyReadVertices)

        {

            Vector3 vector = new Vector3(Convert.ToSingle(properties[X], ni), Convert.ToSingle(properties[Y], ni), Convert.ToSingle(properties[Z], ni));

            content.Positions.Add(vector);

            geometry.Vertices.Add(index);

            index++;

            if (index == this._numberOfVertices)

            {

                index = 0;

                currentlyReadVertices = false;

            }

        }

        else

        {

            geometry.Indices.AddRange(new int[3] { Convert.ToInt32(properties[TriangleFirstPoint]), Convert.ToInt32(properties[TriangleSecondPoint]), Convert.ToInt32(properties[TriangleThirdPoint]) });

        }

    }

 

    content.Geometry.Add(geometry);

 

    return content;

}

 

 

 

Une nouvelle classe fait son apparition ici : MeshContent. Cette dernière hérite de NodeContent. Si NodeContent est dédiée à la représentation en mémoire d’une information hiérarchisée, sa fille, MeshContent, se spécialise dans le stockage de données liées aux formes 3D. Elle offre donc des propriétés et des méthodes utiles pour sauvegarder les données extraites de notre fichier ply tout en étant d’un type accepté par le processor ModelContent. Le chargement de cette structure de données est trivial : le document ply  est parcouru en chargeant tout d’abord l’ensemble des vertices dans la propriété Position de l’objet MeshContent. En parallèle un objet GeometryContent reçoit l’indice du vertex qui lui est affecté. Généralement un Mesh est découpé en un ensemble de parties appellées Geometry. Le MeshContent stocke donc tous les vertices du modèle 3D et les différentes parties de celui-ci référence les vertices dont ils ont besoin. Lorsque les vertices sont chargés, les faces sont lues à leur tour. De nouveau, le geometry concerné recoit par groupe de trois, les indices de chaque faces. Au final, la geometry est ajoutée au Mesh et le Mesh est renvoyé.

 

 

Seconde étape, utilisation dans un projet Xna

 

 

 A ce stade nous disposons de deux projets :

 

  

 

l’un définissant un Importer l’autre étant projet Xna classique. Notre but est d’utiliser un fichier Ply, de le charger, de le compiler et de l’utiliser pour l’affichage dans notre application.

Ajoutez un fichier ply à votre projet. Pour cela cliquez droit sur le projet Xna classique et selectionnez “Ajouter un élément existant”.

 

 

Dans la fenêtre d’exploration choissez “Tous les fichiers” afin de pouvoir voir les fichiers *.ply. Selectionnez en un et validez :

 

 

 

Remarque : Les exemples associés à cet article donnent quelques fichiers ply.

 

Le projet se présente maintenant comme ceci :

 

 

Rien de très nouveau jusque ici. Si vous regardez les propriétes de l’élément qui vient d’être ajouté (selectionnez le fichier ply et faites F4), vous remarquerez qu’il est reconnu par Visual Studio comme étant un fichier standard sur lequel aucune action n’est effectuée :

 

 

Changez la propriété  “Action de génération” en lui donnant la valeur “Contenu” (sans ça, aucune action n’est effectuée à la compilation). Le panneau de propriété change en ceci :

 

 

 

Changez la valeur false en true pour la propriété “Xna Framework Content”. Il est possible maintenant de spécifier un importer et un processor pour traiter ce fichier au moment de la compilation. Malheureusement le panel de choix pour l’importer n’offre pas celui que nous venons de développer :

 

 

et pour cause : le format ply est totalement inconnu de Visual Studio Express et du projet Xna en particulier. Nous allons remédier à cela. Cliquez droit sur le projet Xna et sélectionnez “Propriétés”. Une page à onglets verticaux s’ouvre alors. Sélectionnez l’onglet nommé “Content Pipeline” :

 

 

cette page permet de référencer les importers /processors autres que ceux présents dans le framework Xna. Nous allons bien évidemment ajouter le nôtre. Cliquez sur le bouton Add. La fenêtre suivante d’ouvre alors :

 

 

selectionnez la Dll du projet PlyImporter (vous devez avoir au préable compilé ce projet) et validez. Notre importer est mainteant référencé :

 

 

 

Sélectionnez à nouveau les propriétés du fichier Ply. Le choix de l’importer s’est maintenant etoffé :

 

 

sélectionnez Fichier Ply. Choissez maintenant un type de sortie (propriété “Content Processor”). Bien evidemment le choix se portera sur “Model – Xna Framework”. Ce choix correspondant au processor ModelProcessor. Les propriétés au final se présentent comme ceci :

 

 

 

Remarque : Visual Studio propose le choix “Fichier Ply” comme importer grâce à l’attribute :

 

[ContentImporter(“.ply”, CacheImportedData = true, DisplayName = “Fichiers Ply”, DefaultProcessor = “ModelProcessor”)]

 

de la classe PlyImpoter. 

Compilez maintenant la solution. Un fichier .xnb vient de faire son apparition dans le répertoire de sortie (\bin\x86\Debug) :

 

 

 

Le fichier Xnb étant généré, nous pouvons l’utiliser pour charger un objet de type Model et l’utiliser pour l’affichage.

 

 

Troisième étape, utilisation au runtime

 

 

Cette étape est incontestablement la plus simple et la plus rapide : le Content Pipeline nous a marché la plus grosse partie du travail. Ne nous reste plus qu’à déclarer un objet Model. A le charger à l’aide du Content Manager grâce aux données du fichier Xnb et à l’afficher !

A l’intérieur de la classe Game1 du projet Xna ajoutez en tout début l’instruction :

 

private Model model;

 

Voilà, le model est déclaré. Passons au chargement ; dans la méthode LoadGraphicsContent, ajoutez l’instruction :

 

model = content.Load<Model>(“big_porsche”);

 

Cette instruction appelle l’objet content (le fameux Content Manager) et lui demande de charger un objet de type Model. Le Content Manager vérifie déjà si le contenu a déjà été chargé : il mutualise les ressources afin d’optimiser au maximum l’espace mémoire. Sinon, il vérifie que fichier spécifié en paramètre existe. La string “big_porsche” correspond à l’identité des données à charger. C’est un identifiant qui se définit avant la compilation dans les propriétés du fichier ply. Si vous remontez plus haut sur l’image montrant les propriétés de ce fichier vous remarquerez une propriété “Asset Name” contenant cette valeur. Le développeur est libre de spécifier ce qu’il désire. Il convient néanmoins de garder en tête deux points importants :

  • Toujours donner ici un nom explicite afin de garder une certaine clarté si vous utilisez un grand nombre de ressources.
  • Si votre fichier se trouve dans une arborescence vous devez la spécifier avant l’identité (par exemple “MonRepertoire1\\MonRepertoire2\\big_porsche”).

A ce stade, le Content Manager n’a plus qu’à désérialiser les données du fichier Xnb pour charger un objet de type Model.

L’objet étant chargé, il est utilisable comme n’importe quel autre objet. Nous l’affichons à l’écran comme ceci :

 

        protected override void Draw(GameTime gameTime)

        {

            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

 

            // TODO: Add your drawing code here

            DrawModel(model);

 

            base.Draw(gameTime);

        }

 

 

        private void DrawModel(Model m)

        {

            float size = this.model.Meshes[0].BoundingSphere.Radius * 2;

            graphics.GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;

            graphics.GraphicsDevice.RenderState.CullMode = CullMode.None;

            Matrix[] transforms = new Matrix[m.Bones.Count];

            float aspectRatio = 640.0f / 480.0f;

            m.CopyAbsoluteBoneTransformsTo(transforms);

            Matrix projection = Matrix.CreatePerspectiveFieldOfView(

                               MathHelper.ToRadians(45.0f), aspectRatio, 0.01f, 10000.0f);

            Matrix view = Matrix.CreateLookAt(

                                new Vector3(0.5f * size, 1.5f * size, 2.0f * size), Vector3.Zero, new Vector3(0, 0, 1));

 

            foreach (ModelMesh mesh in m.Meshes)

            {

                foreach (BasicEffect effect in mesh.Effects)

                {

                    effect.EnableDefaultLighting();

 

                    effect.View = view;

                    effect.Projection = projection;

                    effect.World = mesh.ParentBone.Transform;

                }

                mesh.Draw();

            }

        }

 

 

 

Il s’agit là d’un code Xna classique que nous n’expliciterons pas.

A l”affichage nous obtenons une splendide Porsche :

 

 

(l’affichage est en mode fil de fer afin de mieux apprecier les formes de l’objet)

 

 

Développer son Processor

 

Nous aborderons dans ce point plusieurs acteurs primordiaux dans le processus du Content Pipeline. Le processor bien evidemment, mais aussi un importer et deux classes permettant le stockage et l’extraction de données à partir d’un fichier Xnb. Le but de cet exemple est simple. Nous allons créer un type de fichier qui va définir un cube. A partir de là un importer sera créé pour supporter ce nouveau type de fichier. Nous développerons de même un “Writer” qui va être capable de stocker ces données dans un fichier Xnb dans un processus dit de “serialisation”. Inversement, un “Reader” doit être implémenté pour lire ces données et les passer au Processor qui va charger une classe métier utilisable dans un jeu. Un programme chargé, mais au final très simple grâce à la prise en main toujours importante du Content Pipeline Framework.

Remarque : ce tutoriel correspond à l’étape 1, 2, 3, 4 de la figure e.

 

Première étape, Importer et stocker les données lues

 

Commencez par créer un projet de jeu nommé “ContentPipelineShower” (même nomque dans l’exemple précédent). Ajoutez à la solution un projet de type bibliothèque de classe pour Xna (Windows Game Library). Nommez ce projet CubeImporter et validez.

 

Remarque : Si un point vous parait obscur dans la création de ces deux projets reportez vous à l’exemple précédent qui débute de la même manière.

Nous opererons d’abord sur le projet CubeImporter. Supprimez l’unique fichier existant (le fichier Class1.cs) et ajoutez une nouvelle classe au projet nommé “CubeDataHolder” (nom de fichier “CubeDataHolder.cs”). Cette classe va contenir les données à l’état brute extraite depuis le fichier que nous lirons. Avant de continuer plus en amont il est nécessaire de rajouter une référence vers la dll correspondant au framework du Xna Content Pipeline. Cliquez droit sur le node References et selectionnez “Add new Reference”. Dans l’onglet “.Net” de la fenêtre qui s’ouvre alors sélectionnez la dll du Xna Content Pipeline :

 

 

puis validez. Nous sommes prêts à écrire notre importer. Commencez par ajouter deux using pour utiliser les classes du framework au début du fichier CubeDataHolder.cs :

 

using Microsoft.Xna.Framework.Content.Pipeline.Graphics;

using Microsoft.Xna.Framework.Content.Pipeline;

 

Modifiez ensuite le code de la classe CubeDataHolder comme ceci :

 

    /// <summary>

    /// <para>Hold raw data extracted from a cube definition file.</para>

    /// </summary>

    public class CubeDataHolder

    {

        private string _rawData;

 

        /// <summary>

        /// <para>Gets the raw data extracted from a cube definition file.</para>

        /// </summary>

        public string RawData

        {

            get

            {

                return this._rawData;

            }

        }

 

        /// <summary>

        /// <para>Instanciate a new <see cref=”CubeDataHolder”/> object.</para>

        /// </summary>

        /// <param name=”data”></param>

        public CubeDataHolder(string data)

        {

            this.data = data;

        }

    }

 

 

 

rien de compliqué ici : la classe se contente juste de stocker les données brute extraite du fichier que nous lirons. Ajoutez maintenant une nouvelle classe nommée CubeImporter :

 

 

il s’agit de l’importer qui va etre associé au fichier contenant les données du cube à charger. Modifiez le contenu de la classe CubeImporter qu’il contient ainsi :

 

using System;

using System.Collections.Generic;

using System.Text;

using Microsoft.Xna.Framework.Content.Pipeline;

using System.IO;

 

namespace CubeImporter

{

   

    [ContentImporter(“.cub”, DefaultProcessor = “CubeProcessor – My Framework of me that i have”)]

    public class CubeImporter : ContentImporter<CubeDataHolder>

    {

        public override CubeDataHolder Import(string filename, ContentImporterContext context)

        {

            string rawData = File.ReadAllText(filename);

 

            return new CubeDataHolder(rawData);

        }

    }

}

 

 

 

Si ce code vous parait obscur, reportez vous au premier exemple de cet article. L’importer défini ici supporte les types de fichier *.cub (fichiers que nous définirons plus loin). Il est associé au processor CubeProcessor (que nous implémenterons dans quelques instants). Il renvoie après lecture d’un fichier *.cub un objet de type CubeDataHolder. Rien de bien compliqué une fois encore.

 

Seconde étape, formatter et préparer les données

 

Il nous faut maintenant inventer un nouveau type de fichier : les fichiers *.cub. Ils se présenteront de cette manière :

 

 

la première ligne correspond à la couleur du cube (composantes RGB), la seconde ligne contient la taille du cube. En complément une classe métier “Cube” va être ajoutée à la solution. Elle va être chargée à partir de ces données et utilisable au runtime pour afficher un cube. Pour cela nous allons créer un projet qui va être sensé contenir les classes métier du jeu (dans notre exemple il n’y aura que cette classe). Le projet principal (affichant le jeu) le référencera dans la mesure où il nécessite de connaitre cette classe pour être capable de l’utiliser à l’affichage. Notre dll CubeImporter devra elle aussi en posséder une référence pour être capable de l’utiliser dans le cadre du content pipeline.

Ajoutez un nouveau projet nommé BusinessXnaSample :

 

 

A l’intérieur rajoutez une classe nommée Cube avec pour contenu :

 

 

/// <summary>

/// <para>3D representation of a cube.</para>

/// </summary>

public class Cube : IDisposable

{

 

    #region Private members

 

    private GraphicsDevice _device;

    private VertexBuffer _vertexBuffer = null;

    private IndexBuffer _indexBuffer = null;

    private float _width;

    private float _height;

    private float _depth;

    private Color _color = Color.TransparentWhite;

    private float _x, _y, _z;

    private float _rotationX, _rotationY, _rotationZ;

    private static short[] indices = new short[36]{

                                 0,1,2,  //face devant

                                          0,2,3,

                                          5,4,7,  //face derrière

                                          5,7,6,

                                          8,9,10,  //face gauche

                                          8,10,11,

                                          12,13,14,//face droite

                                          12,14,15,

                                          16,17,18,//face haut

                                          16,18,19,

                                          20,21,22,//face bas

                                          20,22,23};

    private Matrix _transformationMatrix;

    private Matrix _translationMatrix;

    private Matrix _scaleMatrix;

    private Matrix _rotationMatrix;

    private Texture2D _texture;

    private BasicEffect _effect;

    private bool _isOriginInCubeCenter;

 

    #endregion

 

 

    #region Properties

 

 

    /// <summary>

    /// <para>Gets or sets a value indicating if the origin of the cartesian system is located a the same place that the cube center point.</para>

    /// </summary>

    /// <remarks>Must be set bedore Load methods.</remarks>

    public bool IsObjectOriginInCubeCenter

    {

        get

        {

            return this._isOriginInCubeCenter;

        }

        set

        {

            this._isOriginInCubeCenter = 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 Cube’s current transformation.</para>

    /// </summary>

    public Matrix Transformation

    {

        get

        {

            return this._transformationMatrix;

        }

        set

        {

            this._transformationMatrix = 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 texture.</para>

/// </summary>

public Texture2D Texture

{

get

{

    return this._texture;

}

set

{

    this._texture = value;

    this._effect.Texture = value;

}

}

 

    /// <summary>

    /// <para>Gets the cube’s Width.</para>

    /// </summary>

    public float Width

    {

        get

        {

            return this._width;

        }

    }

 

 

    /// <summary>

    /// <para>Gets the cube’s height.</para>

    /// </summary>

    public float Height

    {

        get

        {

            return this._height;

        }

    }

 

    /// <summary>

    /// <para>Gets the cube’s depth.</para>

    /// </summary>

    public float Depth

    {

        get

        {

            return this._depth;

        }

    }

 

    /// <summary>

    /// <para>Gets the cube’s position on x-axis.</para>

    /// </summary>

    public float X

    {

        get

        {

            return this._x;

        }

    }

 

    /// <summary>

    /// <para>Gets the cube’s position on y-axis.</para>

    /// </summary>

    public float Y

    {

        get

        {

            return this._y;

        }

    }

 

    /// <summary>

    /// <para>Gets the cube’s position on z-axis.</para>

    /// </summary>

    public float Z

    {

        get

        {

            return this._z;

        }

    }

 

    #endregion

 

 

    #region Constructors

 

    /// <summary>

    /// <para>Instanciate a new Cube objet.</para>

    /// </summary>

    public Cube()

    {

        this._transformationMatrix = Matrix.Identity;

        this._vertexBuffer = null;

 

        this._width = 3.0f;

        this._height = 3.0f;

        this._depth = 3.0f;

 

        this._x = 50.0f;

        this._y = 10.0f;

        this._z = 10.0f;

 

        this._rotationMatrix = Matrix.Identity;

        this._translationMatrix = Matrix.Identity;

        this._scaleMatrix = Matrix.CreateScale(1f, 1f, 1f);

    }

 

    public void Dispose()

    {

        if (this._vertexBuffer != null)

            this._vertexBuffer.Dispose();

 

        if (this._indexBuffer != null)

            this._indexBuffer.Dispose();

 

        this._indexBuffer = null;

        this._vertexBuffer = null;

    }

 

 

    #endregion

 

 

    #region Initialization

 

    public void Load(GraphicsDevice device)

    {

        this._device = device;

        this.InitializeEffect();

        this.InitializeIndices();

        this.InitializeVertices();

    }

 

    private void InitializeEffect()

    {

        this._effect = new BasicEffect(this._device, null);

        this._effect.VertexColorEnabled = true;

    }

 

    private void InitializeIndices()

    {

        this._indexBuffer = new IndexBuffer(

            this._device,

            typeof(short),

            36,

            ResourceUsage.WriteOnly,

            ResourceManagementMode.Automatic);

 

        this._indexBuffer.SetData(Cube.indices);

    }

 

    private void InitializeVertices()

    {

        VertexPositionColor[] vertices = new VertexPositionColor[24];

 

        //face devant

        vertices[0].Position = new Vector3(0f, 0f, 0f);

        vertices[0].Color = this.Color;

        //vertices[0].TextureCoordinate = new Vector2(0, 1);

        vertices[1].Position = new Vector3(0f, 0f, 1f);

        vertices[1].Color = this.Color;

        //vertices[1].TextureCoordinate = new Vector2(0, 0);

        vertices[2].Position = new Vector3(1f, 0f, 1f);

        vertices[2].Color = this.Color;

        //vertices[2].TextureCoordinate = new Vector2(1, 0);

        vertices[3].Position = new Vector3(1f, 0f, 0f);

        vertices[3].Color = this.Color;

        //vertices[3].TextureCoordinate = new Vector2(1, 1);

 

        //face derrière

        vertices[4].Position = new Vector3(1f, 1f, 1f);

        vertices[4].Color = this.Color;

        //vertices[4].TextureCoordinate = new Vector2(0, 0);

        vertices[5].Position = new Vector3(1f, 1f, 0f);

        vertices[5].Color = this.Color;

        //vertices[5].TextureCoordinate = new Vector2(0, 1);

        vertices[6].Position = new Vector3(0f, 1f, 0f);//

        vertices[6].Color = this.Color;

        //vertices[6].TextureCoordinate = new Vector2(1, 1);

        vertices[7].Position = new Vector3(0f, 1f, 1f);

        vertices[7].Color = this.Color;

        //vertices[7].TextureCoordinate = new Vector2(1, 0);

 

        //face gauche

        vertices[8].Position = new Vector3(0f, 1f, 0f);//

        vertices[8].Color = this.Color;

        //vertices[8].TextureCoordinate = new Vector2(0, 1);

        vertices[9].Position = new Vector3(0f, 1f, 1f);

        vertices[9].Color = this.Color;

        //vertices[9].TextureCoordinate = new Vector2(0, 0);

        vertices[10].Position = new Vector3(0f, 0f, 1f);

        vertices[10].Color = this.Color;

        //vertices[10].TextureCoordinate = new Vector2(1, 0);

        vertices[11].Position = new Vector3(0f, 0f, 0f);

        vertices[11].Color = this.Color;

        //vertices[11].TextureCoordinate = new Vector2(1, 1);

 

        //face droite

        vertices[12].Position = new Vector3(1f, 0f, 0f);

        vertices[12].Color = this.Color;

        //vertices[12].TextureCoordinate = new Vector2(0, 1);

        vertices[13].Position = new Vector3(1f, 0f, 1f);

        vertices[13].Color = this.Color;

        //vertices[13].TextureCoordinate = new Vector2(0, 0);

        vertices[14].Position = new Vector3(1f, 1f, 1f);//

        vertices[14].Color = this.Color;

        //vertices[14].TextureCoordinate = new Vector2(1, 0);

        vertices[15].Position = new Vector3(1f, 1f, 0f);

        vertices[15].Color = this.Color;

        //vertices[15].TextureCoordinate = new Vector2(1, 1);

 

        //face haut

        vertices[16].Position = new Vector3(0f, 0f, 1f);//

        vertices[16].Color = this.Color;

        //vertices[16].TextureCoordinate = new Vector2(0, 1);

        vertices[17].Position = new Vector3(0f, 1f, 1f);

        vertices[17].Color = this.Color;

        //vertices[17].TextureCoordinate = new Vector2(0, 0);

        vertices[18].Position = new Vector3(1f, 1f, 1f);

        vertices[18].Color = this.Color;

        //vertices[18].TextureCoordinate = new Vector2(1, 0);

        vertices[19].Position = new Vector3(1f, 0f, 1f);

        vertices[19].Color = this.Color;

        //vertices[19].TextureCoordinate = new Vector2(1, 1);

 

        //face bas

        vertices[20].Position = new Vector3(1f, 0f, 0f);

        vertices[20].Color = this.Color;

        //vertices[20].TextureCoordinate = new Vector2(0, 1);

        vertices[21].Position = new Vector3(1f, 1f, 0f);

        vertices[21].Color = this.Color;

        //vertices[21].TextureCoordinate = new Vector2(0, 0);

        vertices[22].Position = new Vector3(0f, 1f, 0f);//

        vertices[22].Color = this.Color;

        //vertices[22].TextureCoordinate = new Vector2(1, 0);

        vertices[23].Position = new Vector3(0f, 0f, 0f);

        vertices[23].Color = this.Color;

        //vertices[23].TextureCoordinate = new Vector2(1, 1);

 

        if (this.IsObjectOriginInCubeCenter)

            for(int i = 0 ; i < 24 ; i++)

                vertices[i].Position += new Vector3(-0.5f, -0.5f, -0.5f);

 

        this._vertexBuffer = new VertexBuffer(

        this._device,

        typeof(VertexPositionColor),

        24,

        ResourceUsage.WriteOnly,

        ResourceManagementMode.Automatic);

 

        this._vertexBuffer.SetData(vertices);

    }

 

 

    #endregion

 

 

    #region Rendering

 

    /// <summary>

    /// <para>Render the cube on the device.</para>

    /// </summary>

public void Render()

{

this._effect.Begin();

 

foreach (EffectPass pass in this._effect.CurrentTechnique.Passes)

{

    pass.Begin();

    this._device.Vertices[0].SetSource(this._vertexBuffer, 0, VertexPositionColor.SizeInBytes);

    this._device.Indices = this._indexBuffer;

    this._device.VertexDeclaration = new VertexDeclaration(this._device, VertexPositionColor.VertexElements);

    this._device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 24, 0, 12);

    pass.End();

}

 

this._effect.End();

}

 

 

 

    #endregion

 

 

    #region Public methods

 

    /// <summary>

    /// Sets a size for the cube.

    /// </summary>

    public void SetRotation(float rotationX, float rotationY, float rotationZ)

    {

        this._rotationX = rotationX;

        this._rotationY = rotationY;

        this._rotationZ = rotationZ;

 

        this._rotationMatrix = Matrix.CreateRotationX(rotationX) * Matrix.CreateRotationY(rotationY) * Matrix.CreateRotationZ(rotationZ);

        this.UpdateTransformation();

    }

 

    /// <summary>

    /// Sets a size for the cube.

    /// </summary>

    public void SetSize(Vector3 size)

    {

        this._width = size.X;

        this._height = size.Z;

        this._depth = size.Y;

 

        this._scaleMatrix = Matrix.CreateScale(size.X, size.Y, size.Z);

        this.UpdateTransformation();

    }

 

    /// <summary>

    /// Sets a position for the cube.

    /// </summary>

    public void SetPosition(Vector3 location)

    {

        this._x = location.X;

        this._y = location.Y;

        this._z = location.Z;

 

        this._translationMatrix = Matrix.CreateTranslation(location.X, location.Y, location.Z);

        this.UpdateTransformation();

    }

 

    private void UpdateTransformation()

    {

        this._transformationMatrix = this._scaleMatrix * this._rotationMatrix * this._translationMatrix;

        this._effect.World = this._transformationMatrix;

    }

 

    #endregion

 

}

 

 

 

 

Comprendre le code affiché ici n’est pas important au vu de la problématique qui nous amène ici. Il reste toutefois assez abordable pour être compris : il s’agit tout simplement de l’affichage un cube à l’aide d’un vertexbuffer et d’un indexbuffer.

Faites référencer le projet BusinessXnaSample par les projets CubeImporter et ContentPipelineShower. A ce point, la solution doit se présenter ainsi :

 

 

 

Définissons maintenant une classe qui va lire les données à l’état brute pour les formatter et les rendre plus “accessibles”. Ajoutez une nouvelle classe nommée CubeFormatedDataHolder au projet CubeImporter. Donnez lui le contenu suivant :

 

using System;

using System.Collections.Generic;

using System.Text;

using Microsoft.Xna.Framework.Graphics;

 

namespace CubeImporter

{

 

    /// <summary>

    /// <para>Holds a cube’s formated data.</para>

    /// </summary>

    public class CubeFormatedDataHolder

    {

        private Color _color;

        private float _size;

 

        /// <summary>

        /// <para>Gets the extracted and formated cube’s color.</para>

        /// </summary>

        public Color Color

        {

            get

            {

                return this._color;

            }

        }

 

        /// <summary>

        /// <para>Gets the extracted and formated cube’s size.</para>

        /// </summary>

        public float Size

        {

            get

            {

                return this._size;

            }

        }

 

        /// <summary>

        /// <para>Instanciate a new <see cref=”CubeFormatedDataHolder”/> object.</para>

        /// </summary>

        /// <param name=”color”></param>

        /// <param name=”size”></param>

        public CubeFormatedDataHolder(Color color, float size)

        {

            this._color = color;

            this._size = size;

        }

    }

 

}

 

Cette classe stocke simplement les deux données que nous auront extrait du fichier cub : la couleur et la taille du cube. Le processor que nous allons implémenter va donc prendre en entrée un objet de type CubeDataHolder issu du DOM et renvoyer une instance de

 

CubeFormatedDataHolder. Ajoutez au projet CubeImporter une nouvelle classe nommée CubeProcessor. Ecrivez à l’intérieur de celle-ci le code suivant :

 

using System;

using System.Collections.Generic;

using System.Text;

using Microsoft.Xna.Framework.Content.Pipeline;

using System.IO;

using Microsoft.Xna.Framework.Graphics;

 

namespace CubeImporter

{

    [ContentProcessor(DisplayName = “Cube – My Framework of me that i have”)]

 

    public class CubeProcessor : ContentProcessor<CubeDataHolder, CubeFormatedDataHolder>

    {

        public override CubeFormatedDataHolder Process(CubeDataHolder input, ContentProcessorContext context)

        {

            StringReader reader = new StringReader(input.RawData);

            string c = reader.ReadLine();

            string s = reader.ReadLine();

 

            Color color = new Color(byte.Parse(c.Split(“,”.ToCharArray())[0]), byte.Parse(c.Split(“,”.ToCharArray())[1]), byte.Parse(c.Split(“,”.ToCharArray())[2]));

            float size = float.Parse(s.Trim());

 

            reader.Close();

 

            return new CubeFormatedDataHolder(color, size);

        }

    }

 

}

 

Ce processor se contente de lire les données brute, de les parser et de renvoyer un objet formaté. L’attribut ContentProcessor précise que la classe liée est un processor. La propriété DisplayName permet de donner un nom qui sera visible sous Visual Studio Express dans la combo box de choix de processor (sinon le nom de la classe est donné par défaut). La méthode Process doit obligatoirement être implémentée. Elle est appellée automatiquement dans la phase de Build afin d’obtenir un objet de type CubeFormatedDataHolder.à partir d’un objet de type CubeDataHolder. Cette étape est assez similaire à la rédaction d’une classe Importer explicité dans le premier exemple de cet article.

 

L’utilité du fichier Xnb a été abordée lors de la présentation du Content Pipeline : il stocke les données renvoyées par le processor afin que celles-ci soient directement utilisables au moment de l’exécution pour charger un objet métier. Il manque à ce stade deux briques : la première doit écrire le fichier Xnb, la seconde, doit le lire. Commençons par l’écriture : ajoutez une nouvelle classe au projet CubeImporter nommée CubeWriter. Donnez lui le contenu suivant :

 

using System;

using System.Collections.Generic;

using System.Text;

using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;

using BusinessXnaSample;

 

namespace CubeImporter

{

 

    [ContentTypeWriter]

    public class CubeWriter : ContentTypeWriter<CubeFormatedDataHolder>

    {

 

        protected override void Write(ContentWriter output, CubeFormatedDataHolder value)

        {

            output.Write(value.Color.R);

            output.Write(value.Color.G);

            output.Write(value.Color.B);

            output.Write(value.Size);

        }

 

        public override string GetRuntimeType(TargetPlatform targetPlatform)

        {

            return typeof(CubeFormatedDataHolder).AssemblyQualifiedName;

 

        }

 

 

        public override string GetRuntimeReader(TargetPlatform targetPlatform)

        {

            return “CubeImporter.CubeReader, CubeImporter, Version=1.0.0.0, Culture=neutral”;

        }

 

    }

}

 

L’attribute définit une classe capable d’écrire en sortie de build dans un fichier Xnb. Une telle classe doit hériter de ContentTypeWriter. Cette dernière générique possède une spécificité sur le type CubeFormatedDataHolder que nous allons justement sérialiser. La serialisation est un jeu d’enfant : la méthode Write qui doit obligatoirement être implémentée prend deux paramètres : un handle vers le flux d’écriture du fichier Xnb et l’objet à sérialiser. Nous n’avons donc qu’à utiliser la méthode Write du premier pour sauvegarder les propriétés du second.  Les méthodes GetRuntimeType et GetRuntimeReader définissent les noms qualifiés permettant d’identifier respectivement : le type des objets qui sont serialisés (CubeFormatedDataHolder donc) et le type qui va désérialiser les données du fichier Xnb pour les insérer dans un objet Cube (ici CubeReader). Ajoutez une nouvelle classe nommée CubeReader et donnez lui le contenu suivant :

 

using System;

using System.Collections.Generic;

using System.Text;

using Microsoft.Xna.Framework.Content;

using Microsoft.Xna.Framework.Graphics;

using BusinessXnaSample;

using System.Globalization;

 

namespace CubeImporter

{

    public class CubeReader : ContentTypeReader<Cube>

    {

 

        protected override Cube Read(ContentReader input, Cube existingInstance)

        {

            IGraphicsDeviceService service = (IGraphicsDeviceService)input.ContentManager.ServiceProvider.GetService(typeof(IGraphicsDeviceService));

            if (service == null)

            {

                throw this.CreateContentLoadException(null, Properties.Resources.NoGraphicsDevice, new object[0]);

            }

            GraphicsDevice graphicsDevice = service.GraphicsDevice;

            if (graphicsDevice == null)

            {

                throw this.CreateContentLoadException(null, Properties.Resources.NoGraphicsDevice, new object[0]);

            }

 

            Cube cube = new Cube();

            cube.Color = new Color(input.ReadByte(), input.ReadByte(), input.ReadByte());

            float size = input.ReadSingle();

            cube.SetSize(new Microsoft.Xna.Framework.Vector3(size, size, size));

            cube.Load(graphicsDevice);

 

            return cube;

        }

 

        internal ContentLoadException CreateContentLoadException(Exception innerException, string message, params object[] messageArgs)

        {

            return new ContentLoadException(string.Format(CultureInfo.CurrentCulture, message), innerException);

        }

    }

}

 

Nul besoin ici d’attribute et donc de reflexion pour définir ce reader : celui-ci est déclaré à l’intérieur du Writer à l’aide d’un nom Qualifié. Le Content Pipeline sait donc où se trouve le reader qu’il doit utiliser pour lire un fichier Xnb et charger un objet Cube. Il est nécessaire de faire un héritage depuis la classe ContentTypeReader. Ceci implique la définition d’un méthode Read qui est appellée automatiquement lorsque la déserialisation du fichier Xnb est demandée. Les deux paramètres de cette méthodes sont simples : le premier donne un accès au flux directement issu de ce fichier. Le second correspond à l’objet à charger (si il existe déjà).

 

La méthode Write de CubeWriter écrivait 3 bytes et un single (R, G, B et la taille du cube), CubeReader réalise l’opération inverse en lisant 3 bytes et un single et en affectant ces valeurs à un objet cube. A noter la première partie de la méthode Read qui extrait du ContentManager un accès au device (on voit là un des rares défaut du Content Pipeline qui empeche un accès simple à cette valeur).

 

La boucle est désormais bouclée. Nous pouvons tenter d’utiliser notre nouveau jeu de classe.

 

Troisième étape, utilisation au runtime

 

 

Le système qui vient d’être développé lit un fichier cub. Il est donc obligatoire d’écrire un tel fichier et de l’ajouter au projet. Ouvrez le notepad (bloc note) et écrivez :

 

255,255,0
10

 

enregistrez le fichier sous le nom “mycube.cub” n’importe où.

 

 

Cliquez droit sur le projet principal (ContentPipelineShower) et selectionnez “Propriétés”.  A l’intérieur de l’onglet Content Pipeline ajoutez un accès vers la dll “CubeImporter” afin de pouvoir utiliser notre importer et processor pour gérer le fichier cub que nous venons d’écrire (le premier exemple de cet article explicite cette étape).

 

 

Ajoutez le fichier cub au projet. Pour cela cliquez droit sur le projet ContentPipelineShower et selectionnez “Ajouter un élément existant”. Dans la fenêtre d’exploration choissez “Tous les fichiers” afin de pouvoir voir les fichiers *.cub. Selectionnez celui que nous venons de créer et validez. La solution se présente maitenant comme ceci :

 

 

 

Selectionnez le fichier cub et faites F4. Le panneau de propriétés s’ouvre. Par défaut Visual Studio Express a pris le bon importer et le bon processor (en partie grâce aux attributes que nous avons spécifié) :

 

 

 

si ce n’est pas le cas selectionnez notre importer et notre processor. Reste maintenant a modifier la classe Game1 pour charger et afficher le cube.

Ajoutez au tout début de la classe la déclaration :

 

       private Cube model;

 

(n’oubliez pas le using). A l’intérieur de la méthode LoadGraphicsContent, chargez notre objet cube ainsi :

 

 

protected override void LoadGraphicsContent(bool loadAllContent)

{

    if (loadAllContent)

    {

        // TODO: Load any ResourceManagementMode.Automatic content

        model = content.Load<Cube>(“mycube”);

    }

 

    // TODO: Load any ResourceManagementMode.Manual content

}

 

 

Le chargement d’un objet Cube s’avère donc maintenant particulièrement trivial.

L’instruction 

 

        model = content.Load<Cube>(“mycube”);

 

appelle l’objet content (le fameux Content Manager) et lui demande de charger un objet de type Cube. Le Content Manager vérifie déjà si le contenu a déjà été chargé. Sinon, il vérifie que fichier spécifié en paramètre existe. La string “mycube” correspond à l’identité des données à charger. C’est un identifiant qui se définit avant la compilation dans les propriétés du fichier cub. Si vous remontez plus haut sur l’image montrant les propriétés de ce fichier vous remarquerez une propriété “Asset Name” contenant cette valeur. Le développeur est libre de spécifier ce qu’il désire. Il convient néanmoins de garder en tête deux points importants :

  • Toujours donner ici un nom explicite afin de garder une certaine clarté si vous utilisez un grand nombre de ressources.
  • Si votre fichier se trouve dans une arborescence vous devez la spécifier avant l’identité (par exemple “MonRepertoire1\\MonRepertoire2\\mycube”).

A ce stade, le Content Manager n’a plus qu’à désérialiser les données du fichier Xnb pour charger un objet de type Cube. Une reference vers la dll CubeImporter est nécessaire : la deserialisation se fait au runtime et non à l’exécution. A cet instant le content manager ne sait pas encore ou se trouve la classe qui permet cette deserialisation. C’est le nom qualifié indiqué dans la méthode GetRuntimeReader de la classe CubeWriter. qui lui indique ou trouver la dll et la classe voulue (CubeReader). La reference vers la dll CubeImporter va donc placer cette dernière dans le repertoire de l’exécutable. L’application va donc pouvoir lier le nom qualifié à la dll et utiliser la fonctionnalité “Read” qu’elle contient. Cliquez droit sur le projet ContentPipelineShower et ajoutez une reference vers le projet CubeImporter .

L’objet étant chargé, il est utilisable comme n’importe quel autre objet. Nous l’affichons à l’écran comme ceci :

 

 

protected override void Draw(GameTime gameTime)

{

    graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

 

    // TODO: Add your drawing code here

    float size = 10*2;

    // graphics.GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;

    float aspectRatio = 640.0f / 480.0f;

    Matrix projection = Matrix.CreatePerspectiveFieldOfView(

                      MathHelper.ToRadians(45.0f), aspectRatio, 0.01f, 10000.0f);

    Matrix view = Matrix.CreateLookAt(

                        new Vector3(1f * size, 1f * size, 1.0f * size), Vector3.Zero, new Vector3(0, 0, 1));

 

    model.Effect.View = view;

    model.Effect.Projection = projection;

 

    model.Render();

 

    base.Draw(gameTime);

}

 

 

A l’exécution vous obtenez :

 

 

ce qui correspond bien aux données du fichier cub : une couleur rose (255,255,0) et une taille de 10. Si vous modifiez maitenant le fichier cub ainsi :

 

255,0,0
5

 

Soit un cube rouge et deux fois plus petit vous obtenez après une nouvelle compilation :

 

 

Cet exemple est très important, il met en evidence toutes les étapes du Content Pipeline, de l’extraction de données à partir du fichier source jusqu’au chargement d’un objet métier et son affichage. Comprendre ce qui a été fait ici c’est maitriser le Pipeline dans son ensemble.

 

 

 

 

 

Surcharger un Processor existant (modifier le comportement par défaut)

 

Il est possible d’avoir besoin d’augmenter la taille de ses modèles avant l’affichage afin de les mettre à l’échelle du monde dans lequel ils évoluent. Cela est très utile si vous utilisez des formes 3D aux echelles différentes. De cette manière vous passez d’un traitement de mise à l’echelle effectué pour chaque modèle à chaque lancement de votre application à un traitement effectué une seule fois à la compilation. Voila à qoi pourrait remsembler le code d’un tel processor :

 

public class ScalingModelProcessor : ModelProcessor

{

    public override ModelContent Process(NodeContent input, ContentProcessorContext context)

    {

        MeshHelper.TransformScene(input, Matrix.CreateScale(10.0f));

        return base.Process(input, context);

    }

}

 

 

Nous héritons ici de ModelProcessor. Nous pouvons donc profiter des fonctionnalités de base de cette classe tout en modifiant les traitement qu’elle effectue pour effectuer notre correction. Nous surchargeons ici la méthode Process afin de cacher la même méthode de la classe parente. Par l’intermédiaire de la méthode MeshHelper.TransformScene nous effectuons un agrandissement de l’objet en entrée (input). Cet objet est alors passé à la méthode de la classe mère pour continuer le traitement. La classe mère ModelProcessor va recevoir un objet 10 fois plus gros qu’elle va transformer en un objet Model dix fois plus gros lui aussi. A l’affichage on obtient une forme 3D agrandie.

 

 

Surcharger un Processor existant (modifier un modèle)

 

L’exemple de ce point a déjà été développé par Microsoft. Celui-ci étant clair et complet, nous nous contenterons de l’expliciter pour bien le comprendre. Le but de cet exemple n’était pas de mettre en évidence le Content Pipeline mais le billboarding (panneau faisant toujours face à la caméra). L’équipe Xna de Microsoft a donc utilisé un fichier X représentant un terrain comme celui-ci :

 

 

Pour placer les différents panneau sur le terrain elle a surchargé le processor Model et, a placé des panneau pour chaque triangle de ce dernier. Au final le rendu est le suivant :

 

Ouvrez le projet BillboardSample téléchargeable à la fin de ce livre blanc. Deux projets sont présents dans la solution :

 

 

BillboardWindows correspond au projet principal qui content la classe Game. BillboardPipeline contient le processor surchargé. C’est sur ce dernier que va se porter notre attention. Ouvrez la classe VegetationProcessor.

La méthode Process est assez similaire à celle de l’exemple précédent :

 

public override ModelContent Process(NodeContent input,

                                     ContentProcessorContext context)

{

    if (input == null)

    {

        throw new ArgumentNullException(“input”);

    }

    // Create vegetation billboards.

    GenerateVegetation(input, input.Identity);

 

    // Chain to the standard ModelProcessor.

    return base.Process(input, context);

}

 

 

Elle prend en entrée un NodeContent, le modifie et le passe à sa classe mère pour continuer le traitement par défaut. C’est la méthode GenerateVegetation qui s’occupe de modifier le NodeContent :

 

 

/// <summary>

/// Recursive function adds vegetation billboards to all meshes.

/// </summary>

void GenerateVegetation(NodeContent node, ContentIdentity identity)

{

      // First, recurse over any child nodes.

    foreach (NodeContent child in node.Children)

    {

        GenerateVegetation(child, identity);

    }

 

    // Check whether this node is in fact a mesh.

    MeshContent mesh = node as MeshContent;

 

    if (mesh != null)

    {

        // Create three new geometry objects, one for each type

        // of billboard that we are going to create. Set different

        // effect parameters to control the size and wind sensitivity

        // for each type of billboard.

        GeometryContent grass = CreateVegetationGeometry(“grass.tga”,

                                                         5, 5, 1, identity);

 

        GeometryContent trees = CreateVegetationGeometry(“tree.tga”,

                                                         12, 12, 0.5f, identity);

       

        GeometryContent cats = CreateVegetationGeometry(“cat.tga”,

                                                        5, 5, 0, identity);

 

        // Loop over all the existing geometry in this mesh.

        foreach (GeometryContent geometry in mesh.Geometry)

        {

            IList<int> indices = geometry.Indices;

            IList<Vector3> positions = geometry.Vertices.Positions;

            IList<Vector3> normals = geometry.Vertices.Channels.Get<Vector3>(

                                                VertexChannelNames.Normal());

 

            // Loop over all the triangles in this piece of geometry.

            for (int triangle = 0; triangle < indices.Count; triangle += 3)

            {

                // Look up the three indices for this triangle.

                int i1 = indices[triangle];

                int i2 = indices[triangle + 1];

                int i3 = indices[triangle + 2];

 

                // Create vegetation billboards to cover this triangle.

                // A more sophisticated implementation would measure the

                // size of the triangle to work out how many to create,

                // but we don’t bother since we happen to know that all

                // our triangles are roughly the same size.

                for (int count = 0; count < billboardsPerTriangle; count++)

                {

                    Vector3 position, normal;

                   

                    // Choose a random location on the triangle.

                    PickRandomPoint(positions[i1], positions[i2], positions[i3],

                                    normals[i1], normals[i2], normals[i3],

                                    out position, out normal);

 

                    // Randomly choose what type of billboard to create.

                    GeometryContent billboardType;

 

                    if (random.NextDouble() < 0.002)

                    {

                        billboardType = trees;

 

                        // As a special case, force trees to point straight

                        // upward, even if they are growing on a slope.

                        // That’s what trees do in real life, after all!

                        normal = Vector3.Up;

                    }

                    else if (random.NextDouble() < 0.0001)

                    {

                        billboardType = cats;

                    }

                    else

                    {

                        billboardType = grass;

                    }

                   

                    // Add a new billboard to the output geometry.

                    GenerateBillboard(mesh, billboardType, position, normal);

                }

            }

        }

 

        // Add our new billboard geometry to the main mesh.

        mesh.Geometry.Add(grass);

        mesh.Geometry.Add(trees);

        mesh.Geometry.Add(cats);

    }

}

 

Celle-ci commence par s’appeller recursivement en suivant la hiérarchie de NodeContent. Elle détermine ensuite si le NodeContent courant est de type MeshContent. Si c’est le cas elle commence par créer trois objets GeometryContent correspondant à trois types de panneaux : un affichant de l’herbe, l’autre un arbre et le dernier un chat. Pour Chaque géométrie de la forme 3D (une géometrie est assimilable à une portion de forme 3D, à priori ici il n’y a qu’une seule portion) on parcours l’ensemble des triangles. Pour chaque triangles un ensemble de billboard est ajouté. Au final on ajoute trois géometrie correspondant au trois type de billboard possibles. L’ajout d’un billboard passe par trois étapes : le choix d’un point au hazard sur le triangle en cours, le choix du type de billboard (0.2 pourcent d’arbres, 0.01 pourcent pour un chat et le reste en herbe haute) et enfin un appel à la méthode GenerateBillboard. Cette dernière se présente comme ceci :

 

/// <summary>

/// Helper function adds a single new billboard sprite to the output geometry.

/// </summary>

private void GenerateBillboard(MeshContent mesh, GeometryContent geometry,

                               Vector3 position, Vector3 normal)

{

    VertexContent vertices = geometry.Vertices;

    VertexChannelCollection channels = vertices.Channels;

 

    // First, create a vertex position entry for this billboard. Each

    // billboard is going to be rendered a quad, so we need to create four

    // vertices, but at this point we only have a single position that is

    // shared by all the vertices. The real position of each vertex will be

    // computed on the fly in the vertex shader, thus allowing us to

    // implement effects like making the billboard rotate to always face the

    // camera, and sway in the wind. As input the vertex shader only wants to

    // know the center point of the billboard, and that is the same for all

    // the vertices, so only a single position is needed here.

    int positionIndex = mesh.Positions.Count;

 

    mesh.Positions.Add(position);

 

    // Second, create the four vertices, all referencing the same position.

    int index = vertices.PositionIndices.Count;

 

    for (int i = 0; i < 4; i++)

    {

        vertices.Add(positionIndex);

    }

 

    // Third, add normal data for each of the four vertices. A normal for a

    // billboard is kind of a silly thing to define, since we are using a

    // 2D sprite to fake a complex 3D object that would in reality have many

    // different normals across its surface. Here we are just using a copy

    // of the normal from the ground underneath the billboard, which can be

    // used in our lighting computation to make the vegetation darker or

    // lighter depending on the lighting of the underlying landscape.

    VertexChannel<Vector3> normals;

    normals = channels.Get<Vector3>(VertexChannelNames.Normal());

 

    for (int i = 0; i < 4; i++)

    {

        normals[index + i] = normal;

    }

 

    // Fourth, add texture coordinates.

    VertexChannel<Vector2> texCoords;

    texCoords = channels.Get<Vector2>(VertexChannelNames.TextureCoordinate(0));

 

    texCoords[index + 0] = new Vector2(0, 0);

    texCoords[index + 1] = new Vector2(1, 0);

    texCoords[index + 2] = new Vector2(1, 1);

    texCoords[index + 3] = new Vector2(0, 1);

 

    // Fifth, add a per-billboard random value, which is the same for

    // all four vertices. This is used in the vertex shader to make

    // each billboard a slightly different size, and to be affected

    // differently by the wind animation.

    float randomValue = (float)random.NextDouble() * 2 – 1;

 

    VertexChannel<float> randomValues;

    randomValues = channels.Get<float>(VertexChannelNames.TextureCoordinate(1));

 

    for (int i = 0; i < 4; i++)

    {

        randomValues[index + i] = randomValue;

    }

 

    // Sixth and finally, add indices defining the pair of

    // triangles that will be used to render the billboard.

    geometry.Indices.Add(index + 0);

    geometry.Indices.Add(index + 1);

    geometry.Indices.Add(index + 2);

 

    geometry.Indices.Add(index + 0);

    geometry.Indices.Add(index + 2);

    geometry.Indices.Add(index + 3);

}

 

La première question que l’ont peut se poser à la lecture du code de cette fonction est “qu’est ce qu’un channel” ? Un channel (“chaine” en français) est une propriété spéciale assimilable à un index sur un table qui permet de choisir dans un agréagat un ensemble précis de données. L’instruction :

 

 

VertexChannel<Vector2> texCoords;

    texCoords = channels.Get<Vector2>(VertexChannelNames.TextureCoordinate(0));

 

extrait de la liste de channels une liste de Vector2 dédié aux coordonnées de texture de premier niveau. La classe VertexChannelNames possède un ensemble de méthodes qui permettent de préciser la chaine voulue. L’instruction

 

    VertexChannel<Vector3> normals;

    normals = channels.Get<Vector3>(VertexChannelNames.Normal());

 

renvoie un accès sur la chaine “Normal”. La méthode ici rajoute la position donnée en paramètre à la liste de points de la géométrie en cours. Elle donne ensuite 4 indices qui tous pointent vers la même position. L’opération rajoute donc quatre vertices au même endroit dans l’espace. Chaque vertex ajouté est associé à la normale donnée en paramètre et à une coordonnée de texture. La chaine de coordonnées de texture de niveau 1 est utilisée pour sauvegarder des valeurs aléatoire utilisées pour l’aspect du billboard à l’affichage. Enfin on donne l’ordre des vertices à utiliser pour créer les deux triangles formant le panneau.

 

Cette façon de faire permet de créer à chaque compilation un nouveau monde à partir d’un simple modèle de paysage sans vie. Le compilateur de contenu va rajouter un ensemble de billboards de manière aléatoire sans que l’application n’ai à gérer cette phase à son exécution. On passe ainsi d’un modèle vide issu du fichier .x à un modèle complexe et riche stocké dans le fichier Xnb. Si vous lancez l’application, son affichage est instanné. Il faudrait plusieurs secondes si ce traitement était fait au runtime.

 

Remarque : la méthode CreateVegetationGeometry n’a pas été expliquée ici dans la mesure où nous y revenons dans l’exemple qui suit.

 

 

Appeller un Processor lors de la compilation de contenu 

 

 

Il peut être utile à l’intérieur du traitement d’un processor de pouvoir effectuer un appel vers un autre processor afin de répondre à un besoin technique ou dans le simple but de charger une ressource ne se trouvant pas dans le projet.

Le projet précédent utilise un grand nombre de ressources pour réaliser son affichage : le sol (fichier x), les effets pour manipuler les billboards (un fichier fx) et trois textures représentant un arbre, de l’herbe et un chat. Si on regarde le projet plus en détails, on s’aperçoit pourtant que seulement deux ressources sont présentes dans le projet associé : celle du sol et de l’effet et seul le fichier sol fait l’objet d’un chargement dans le programme par l’intermédiaire de l’instruction :

 

 

On sait que la classe VegetationProcessor s’occupe d’ajouter au modèle sol un ensemble de billboards texturé avec une image d’herbe, d’arbre ou d’un chat. Il est donc nécessaire, au moment de la compilation, d’inclure ces nouvelles ressources dans le content pipeline en plus du fichier effet. C’est la méthode CreateVegetationGeometry qui s’occupe de cela. VegetationProcessor ajoute pour chaque triangle du modèle sol un ensemble de billboards. Chacun d’eux doit ajouter au content pipeline une ressource de la texture qu’il utilise et une autre pour l’effet qui l’affiche. Sachant qu’une vingtaine de billboards est créé par triangle, la mémoire serait très vite saturée par toutes ces ressources si nous n’utilisions pas le framework du Content Pipeline. La classe ExternalReference s’apparente au ContentManager. Elle est capable d’inclure des ressources au moment de l’exécution d’un processor. Tout comme le ContentManager elle n’ajoute la ressource au content pipeline que si celle-ci n’a pas déjà été créée. Au final, notre modèle sol possèdera plusieurs milliers de billboards mais ne référencera que quatre ressources : nos trois textures et le fichier effet. Etudions plus en détail le code de cette méthode :

 

/// <summary>

/// Helper function creates a new geometry object,

/// and sets it to use our billboard effect.

/// </summary>

static GeometryContent CreateVegetationGeometry(string textureFilename,

                                                float width, float height,

                                                float windAmount,

                                                ContentIdentity identity)

{

    GeometryContent geometry = new GeometryContent();

 

    // Add the vertex channels needed for our billboard geometry.

    VertexChannelCollection channels = geometry.Vertices.Channels;

 

    // Add a vertex channel holding normal vectors.

    channels.Add<Vector3>(VertexChannelNames.Normal(), null);

 

    // Add a vertex channel holding texture coordinates.

    channels.Add<Vector2>(VertexChannelNames.TextureCoordinate(0), null);

 

    // Add a second texture coordinate channel, holding a per-billboard

    // random number. This is used to make each billboard come out a

    // slightly different size, and to animate at different speeds.

    channels.Add<float>(VertexChannelNames.TextureCoordinate(1), null);

 

    // Create a material for rendering the billboards.

    EffectMaterialContent material = new EffectMaterialContent();

 

    // Point the material at our custom billboard effect.

    string directory = Path.GetDirectoryName(identity.SourceFilename);

 

    string effectFilename = Path.Combine(directory, “Billboard.fx”);

 

    material.Effect = new ExternalReference<EffectContent>(effectFilename);

 

    // Set the texture to be used by these billboards.

    textureFilename = Path.Combine(directory, textureFilename);

 

    material.Textures.Add(“Texture”,

                    new ExternalReference<TextureContent>(textureFilename));

 

    // Set effect parameters describing the size and

    // wind sensitivity of these billboards.

    material.OpaqueData.Add(“BillboardWidth”, width);

    material.OpaqueData.Add(“BillboardHeight”, height);

    material.OpaqueData.Add(“WindAmount”, windAmount);

 

    geometry.Material = material;

 

    return geometry;

}

 

Un fichier modèle possède un ensemble de GeometryContent comme nous l’avons vu précédemment. Chaque GeometryContent est associé à un material qui défini l’aspect de cette portion de modèle lors de son affichage. Ce dernier possède en autres propriétés un ensemble de texture et un Effet. La méthode  va donc se contenter après création de l’objet  d’ajouter une texture et un fichier effet :

 

    // Create a material for rendering the billboards.

    EffectMaterialContent material = new EffectMaterialContent();

 

    // Point the material at our custom billboard effect.

    string directory = Path.GetDirectoryName(identity.SourceFilename);

    string effectFilename = Path.Combine(directory, “Billboard.fx”);

    material.Effect = new ExternalReference<EffectContent>(effectFilename);

 

    // Set the texture to be used by these billboards.

    textureFilename = Path.Combine(directory, textureFilename);

    material.Textures.Add(“Texture”,

                    new ExternalReference<TextureContent>(textureFilename));

 

Cette portion de  pointe vers le fichier effet et ajoute la ressource associée au material via l’instruction

 

material.Effect = new ExternalReference<EffectContent>(effectFilename);

 

La même opération est réalisée plus loin pour la texture :

 

material.Textures.Add(“Texture”, new ExternalReference<TextureContent>(textureFilename));

 

A l’intérieur d’un objet Content (NodeContent, MaterialContent, MeshContent) les ressources sont obligatoirement manipulées via la classe ExternalReference. Elle vérifie si la ressource n’a pas déjà été traitée, auquel cas elle fait une référence vers celle-ci. Dans le cas contraire, elle ajoute la ressource au content pipeline, s’assure que celle-ci sera sérialisée en un fichier Xnb et renvoie ensuite la référence. Après compilation on trouve dans le repertoire de l’exécutable 5 fichiers ressources :

 

 

 

Le sol, la texture d’herbe, celle de l’arbre, celle du fichier et enfin celle du fichier effet.

 

 

 

Modifier les processor appellés

 

Microsoft fourni sur la MSDN un autre exemple assez similaire. Là encore le scénario a pour sujet principal l’appel à un processor depuis l’exécution d’un autre processor.

Le processor de Model utilisé par défaut prend un NodeContent en entrée (qui represente l’objet root de la scène). Il appel alors le processor de material standard pour chaque MaterialContent utilisé. Ainsi, si il y’a deux MeshContent dans la scène possédant chacun deux GeometryContent avec un material différent, alors le processor material sera appelé quatre fois.

Le processor de material standard utilise le processor ModelTextureProcessor (qui n’est pas le processor de texture par défaut) pour traiter les textures trouvées dans la propriété Textures de l’objet MaterialContent. Ce processor applique la compression DXT1 à chaque texture. Il est par conséquent préférable de ne pas utiliser ce processor pour les ressources correspondant à des textures déjà compressées. Il vaut mieux appeler le processor de texture standard qui n’effectue aucune compression. Pour implémenter cela, il est nécessaire de développeur deux processors. Le premier nommé NoCompressionMaterialProcessor surcharge le processor de materials standard et modifie la méthode BuildTexture afin d’utiliser le processor TextureProcessor à la place du processor ModelTextureProcessor. Le code suivant explicite cela :

 

 

 

 

[ContentProcessor]

class NoCompressionMaterialProcessor : MaterialProcessor

{

    protected override ExternalReference<TextureContent> BuildTexture(string textureName,

        ExternalReference<TextureContent> texture, ContentProcessorContext context)

    {

        return context.BuildAsset<TextureContent, TextureContent>(texture, “TextureProcessor”);

    }

}

 

Dans ce code, ExternalReference représente les ressources qui sont partagées. Par exemple, plusieurs materials peuvent utiliser la même texture. Grâce à l’utilisation de cette classe, le content manager ne chargera qu’une seule copie de cette texture au runtime. Quelque que soit le nombre de référence qu’une texture possède, celle-ci ne sera chargée qu’une seule fois. La classe ContentProcessorContext est un moyen de communication avec le content pipeline. Appeler BuildAsset oblige le content à construire une ressource lors du processus de compilation du jeu.

Le second processor de cet exemple dérive de ModelProcessor et surcharge la méthode ConvertMaterial afin de convertir tous les MaterialContent passé en entrée à la méthode Process de ModelProcessor. Le code suivant explicite cela :

 

[ContentProcessor]

class NoCompressionModelProcessor : ModelProcessor

{

    protected override MaterialContent ConvertMaterial(

        MaterialContent material, ContentProcessorContext context)

    {

        return context.Convert<MaterialContent, MaterialContent>(

            material, “NoCompressionMaterialProcessor”);

    }

}

 

Ce processor diffère de NoCompressionMaterialProcessor parcequ’il n’utilise pas la classe ExternalReference. En effet, les objets MaterialContent utilisés ici ne sont pas des références externes vers des ressources. Ils sont des objets de type MaterialContent déjà chargés en mémoire. Chacun possède ses propres caractéristiques et ne peut pas être partagfé entre plusieurs modèles. La tâche ici consiste juste à transformer le MaterialContent de manière à ce que le processor appellé pour les traiter soit NoCompressionMaterialProcessor et non MaterialProcessor.

 

 

Debugguer le Content Pipeline

 

Si le debugguage du ContentTypeReader ne pose à priori pas de soucis (il s’exécute au moment du runtime), déterminer les causes d’une erreur et/ou le cheminement des classes du content pipeline lors de la compilation de contenu n’est pas aussi aisé. Il existe un moyen simple et efficace de suivre le cheminement de l’exécution d’un acteur du content pipeline lors de la compilation de contenu. Ce moyen, c’est l’outil debug viewer qui nous l’offre.

 

Pour télécharger cet outil, rendez vous sur l’url suivante : http://www.microsoft.com/technet/sysinternals/Miscellaneous/DebugView.mspx

 

 

Debug View est capable d’ecouter les sorties de debug des application Win32 et, par extension .Net. Il suffit alors pour un developpeur opérant sur une classe utilisée lors de la compilation de contenu de placer des traces à des points stratégiques de son application pour pouvoir en suivre l’exécution.

Nous prendrons comme support le premier exemple utilisé au début du point “pratique” abordant la création d’un importer pour lire les fichiers ply. Allez dans la méthode Import de la classe PlyImporter. Juste après l’instruction

 

 

this

.AnalizeHeader(sr, info1);

se trouve l’appel à ReadData qui comme nous l’avons indiqué créé l’objet ModelContent. Modifiez cet appel ainsi :

 

System.Diagnostics.Trace.WriteLine(“Nom du fichier modèle : “ + filename);

System.Diagnostics.Trace.WriteLine(“Nombre de vertices : “ + _numberOfVertices);

System.Diagnostics.Trace.WriteLine(“Nombre de faces : “ + _numberOfFaces);

System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();

content1 = this.ReadData(sr, info1);

System.Diagnostics.Trace.WriteLine(string.Format(“Traitement terminé, temps réalisé : {0} secondes”, stopwatch.Elapsed.TotalSeconds));

 

 

La classe  fourni un jeu de méthodes permettant de suivre l’exécution d’un code. Debug Viewer fait partie des applications qui sont capable de lire les informations inscrites par ces méthodes. Ce que nous faisons ici est assez trivial. Après l’appel à AnalyzeHeader, nous avons le nombre de vertices et de face du modèle. Nous affichons ces deux informations ainsi que le nom du fichier en sortie. De même nous lançons un objet Stopwatch. Cette classe offre un ensemble de méthodes permettant de calculer l’écoulement du temps entre une balise de départ (la méthode statique StartNew() ) et un moment t (moment ou on prend la valeur de la propriété Elapsed).

Nous calculons ainsi le temps pris par le traitement effectué dans ReadData.

Lancez debugviewer. Nettoyez le contenu en tapant Ctrl +X ou en cliquant sur l’icone gomme. Lancez mainteant notre version modifé du programme faisant appel à l’importer modifié. Lorsque la compilation se termine, vous voyez apparaitre sur l’outil :

 

 

 

Cette combinaisons entre Trace et le Debug Viewer permet d’étudier les actions prises par votre code et surtout de voir où et pourquoi il s’est arrété sur une erreur. Un atout primordial quand on sait que sans eux, on développe et exécute sans avoir d’idées précise sur la façon dont le code s’est exécuté…

 

 

 

Conclusion

 

Le Content Pipeline est sans aucun doute l’avancée la plus importante introduite par le Xna. Il est un moyen efficace et simple de s’interropérer avec les données rapidement  en offrant une réutilisabilité maximum. Cet article s’est attaché à le démontrer en offrant divers exemples liés à l’utilisation du Content Pipeline. Le lecteur, à la lecture de ces lignes, sait : comment surcharger et étendre les divers acteurs du pepline déja existant pour modifier les données lues ou pour ajouter un ensemble d’informations, comment supporter des types de données non reconnus et surtout comprendre les divers mécanismes et étapes qui permettent à partir d’un fichier de données directement issu d’un outil tiers de passer à une classe métier chargée utilisable au runtime.

Le Content Pieepline n’est pas de ces framework gadget qui simplifient la vie du développeur sur le papier tout en lui faisant perdre de nombreux jours de développant à l’écran. Il s’agit d’un ensemble structuré de moyens pour mettre en oeuvre une façon générique et universelle de gérer les ressources des application Xna. Quiconque maitrise ce pipeline, assure la pérénité de ses productions.

 

 

Samples 

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

 

 

 

Glossaire 

 

 

AssetName

Identifiant d’une ressource spécifiée sous Visual Studio. C’est par l’AssetName que l’importer lie les données dans le DOM au moment où il les sauvegarde. C’est par cet identifiant que le processor les rappratrie pour les formater et enfin c’est lui encore qui est utilisé par le Content Manager pour charger un objet métier au moment de l’exécution. 

 

Content Manager

Accès aux ressources d’un jeu. Correspond à la classe ContentManager. Le Content Manager permet, par l’intermédiaire d’un AssetName de charger un objet métier à l’aide de données directement issue des ressources d’un jeu.

 

ContentPipeline

Processus opérant à la fois au moment de la compilation et à l’exécution d’une application. Il manipule les ressources d’un projet 3D en offrant au developpeur la possibilité de contrôler l’utilisation, le stockage et la transformation des données issue de ces ressources. Le Content Manager pemet au bout de chaine d’accèder facilement et rapidement à ces données via une interface très haut niveau.

 

ContentTypeReader

Acteur opérant à la demande du Content Manager. Il désérialize le contenu du fichier Xnb correspondant à un AssetName donné pour charger un objet métier. Il est le dernier acteur du content pipeline et le seul à opérer au moment de l’exécution de l’application.

 

ContentTypeWriter

Acteur opérant après le traitement du processor. Il utilise son travail formaté pour le sérialiser proprement à l’intérieur d’un fichier Xnb. Le ContentTypeWriter opère au moment de la compilation de contenu.

 

DOM

Modèle objet intelligent utilisé par l’importer pour sauvegarder les données issues de son traitement. Le DOM permet une généricité dans le stockage d’informations permettant en aval un chargement simplifié d’objets. Le processor utilise le DOM dans un second temps pour effectué un traitement de formatage sur ces données.

 

Fichier Xnb

Le fichier Xnb est utilisé comme destination de la sérialization des données formatées par le processor. C’est le ContentTypeWritter qui effectue cette opération. Le ContentTypeReader quand à lui désérialize le contenu du fichier pour charger un objet métier.

 

Importer

Acteur du Content Pipeline chargé d’extraire les données à partir de ressources. Il opère au tout début de la compilation de contenu. Le résultat de son travail est stocké à l’intérieur du DOM

 

Objet Métier

Interface haut niveau obscurcifiant par des fonctionnalités simples des fonctionnalités techniques difficiles à appréhender.

 

Processor

Acteur du Content Pipeline chargé de puiser les données dans le DOM à partir d’un AssetName. Le processor formatte, classe et contrôle les données que l’importer sauvegardé afin de les préparer à un serializage par le ContentTypeWriter à l’intérieur d’un fichier Xnb.

 

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 

De retour

désolé à tous ceux qui m’ont envoyé des questions les derniers jours, je me suis fait opérer et j’étais légèrement hors d’état de me servir d’un PC :)


 


Je vais prendre le temps de répondre aux mails et aux commentaires.


 


[Soon]