in

MSMVPS.COM

The Ultimate Destination for Blogs by Current and Former Microsoft Most Valuable Professionals.

Graphic Stream

Blog about .Net, Managed DirectX, XNA and C#

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(propertiesYes, ni), Convert.ToSingle(propertiesPerson, 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>