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.

 

45 thoughts on “Livre blanc : Le XNA Framework Content Pipeline”

  1. Pour ma part j’aimerais bien que le puisse charger des textures 2D en 16 bits, ou 8 bits. Le format (Sprites – Textures 32 bits) n’est pas vraiment économique en mémoire.

    La première fois que j’ai voulu afficher un sprite en XNA, j’ai été trés surpris. Aprés intégration dans un projet XNA, mon image de test a vu sa taille multipliée par 6! Pas glop..

    Voici les chiffres, pour la même image (215×215 pixels contenant 4000 couleurs différentes)

    fichier .png: 31 ko
    fichier .bmp: 137 ko
    fichier .xnb: 181 ko (outch!)

    215×215 pixels en 32 bits, sans la moindre compression, occupent effectivement 181 ko.

    Mon image et ces 4000 couleurs pourrait être stockée dans une texture 16 bits (65.535 couleurs), ce qui diviserais sa taille par deux. Dans certais cas un stockage sur 8 bits (donc 256 couleurs) est suffisant.

    Une suggestion ?

  2. Les chiffres que tu donne pour les trois fichiers importent peu. Les trois fichiers, lorsqu’ils seront chargés en mémoire occuperons la même taille mémoire, tout simplement parceque l’image texturée va être ordonnée en mémoire de manière est être utilisée et lue le plus rapidement possible. Le dds est d’ailleur un format particulièrement proche de l’agencement des texels en mémoire si tu veux te faire une idée. Donc quelque soit le format que tu utilise, la taille mémoire que ta texture prendra sera la même. Où se situe la différence ? Tout simplement sur le temps de chargement. Le png sera tres long (y’a un algo derrière), le bmp très rapide (facile à lire), et le XNB aussi rapide. Pkoi as tu presque 50ko de plus que le bmp ? je pense que ton Xnb contient un mipmap (encore un gain en temps de chargement).

    Tu peux charger une image sans le content manager. Tu t’occupe alors de tout. A ce moment là charger une texture 8 bits estsimple. De mémoire le tutoriel 8 montre cela avec le sample mipmap.

  3. La documentation du XNA indique que l’on peut stocker en mémoire les textures 2D selon différents formats, selon la valeur du paramétre “SurfaceFormat”.

    “SurfaceFormat.Color” que tu utilise dans tes tutoriaux est idéal pour la 3D. la couleur de chaque pixel est stockée sur 32 bits, 8 bits pour la composante Rouge, 8 bits pour la composante Vert, 8 bits pour la composante Bleu, et 8 bits pour le facteur de transparence.

    Il existe d’autres formats permettant de stocker des pixels sur 16 ou 8 bits.

    “SurfaceFormat.Bgr233″ est un format Old School 8 bits pour des images 256 couleurs, avec 2 bits pour le Bleu, 3 bits pour le Vert et 3 bits pour le Rouge.

    “SurfaceFormat.Bgra5551″ est un format 16 bits/pixel sympa avec 5 bits par composante de couleurs, et un bit pour la transparence. Cela permet de créer des sprites en 32.000 couleurs.

    “SurfaceFormat.Bgr4444″ peut contenir des images en 4096 couleurs sur 16 bits, 4 bits pour chaque composante de couleur et 4 bits pour la transparence.

    En utilisant ces formats on peut économiser de la mémoire. Et la mémoire c’est une ressource précieuse dans un jeu vidéo!

    Effectivement, on peut charger des images sans passer par le , en utilisant la méthode FromFile de la classe Texture2D. Pratique pour charger des données graphiques 8, 16 ou 32 bits, sauf que.. cela ne fonctionne qu’avec Windows! D’aprés la documentation du XNA, le framework XBOX 360 ne connait pas FromFile, juste le . Grrr..

  4. C’est pratique un lecteur comme toi, bientot j’aurais plus rien à écrire :)

    Oui une ressource précieuse c’est vrai. Y’a d’autres moyens (complémentaires) pour récuprer de la mémoire comme le cache.

    Que le content pipeline oui, mais si dans le header du format de l’image que tu charge il est écrit “image en 8 bits” (pour simplifier :) ), je doute que le content pipeline s’amuse à modifier la texture pour la passer en 32 bits et la mettre ainsi en mémoire. A mon avis il est intelligent pour charger la texture dans son format.

  5. Ne t’en fait pas, j’ai un peu de retard dans les techniques modernes. Mes connaissances en programmation datent de l’époque où DirectX 1.0 est apparu. Je viens juste de replonger les mains dans le camboui, et je suis légérement a la traine.. Dix ans de retard.. une paille, surtout dans le domaine de l’informatique!

    Bon, il me reste plus qu’a écrire un petit programme de test pour vérifier si le est aussi malin que tu le pense. * touche du bois *

  6. je m’en fais pas c’est agréable :)

    pour le programme de test c simple, tu prend celui que j’ai fais tu met un breakpoint apres le chargement de la texture avec le content pipeline et tu regarde la valeur de la proprieté Format de l’objet Texture2D tout simplement :)

  7. Effectivement c’est tout simple a faire. Trés pratique les points d’arréts et le débugeur symbolique.

    Et la réponse est .. zut, ça marche pas! C’est toujours “SurfaceFormat.Color” qui sort du chapeau, quelque que soit le type d’image utilisée dans le projet. 8 bits, 16 bits, 32 bits .. même combat.

    L’importer (Sprite – Texture 32 bits) fait exactement ce que son nom indique, en fabriquant des textures 32 bits, sans la moindre subtilité.

    Il manque des importers genre (Sprite – Textures 16 bits) et (Sprite – Textures 8 bits).

  8. Dans la même série, il manque un Importer pour lire le JPEG sur XBOX 360. Une image JPEG passée dans la moulinette du via l’importer (Sprite – Texture 32 bits) devient une image xnb sans aucune compression. Une image de fond de 1024×768 pixels, de 200 Ko en jpeg devient un fichier xnb de 3 Mo.. Pas trés économique, d’autant plus qu’il était question d’une limitation de 50 Mo pour la totalité d’une application XNA.

  9. l’histoire des 50 mégas ce n’est même pas imaginable en jeu commercial :)

    Le 3méga pas économique oui, mais oh combien rapide à lire :)

  10. En effet, il n’y a pas de limite a 50 Mo .. depuis mardi de la semaine dernière! Maintenant c’est 150 Mo, contrainte fixée par Microsoft pour la taille maximale d’un jeu XBOX 360 vendu par l’intermédiaire du XBOX Live Arcade.

    Pourquoi cette évolution ? A cause de l’apparition des cartes mémoires 512 Mo pour XBOX 360. Jusqu’a présent, il n’y avais qu’un seul modéle de carte mémoire avec 64 Mo, d’où la première limitation a 50 Mo pour un jeu XNA. Depuis le 3 avril, il est possible de créer des jeux allant jusqu’a 150 Mo..

    Bref, le dévellopeur XBOX voulant commercialiser ces créations sur le XBOX LIVE a intéret a surveiller de prés la consommation mémoire de ces graphismes, même si cela ralentit la vitesse de chargement.. Les contraintes de dévellopement sur console sont trés différentes du monde pc, où tout le monde a un disque dur et des Go de mémoire pour s’étaler..

  11. La limitation de la taille d’une application XNA à 50 Mo n’existe que pour les jeux XBOX 360 destinés a être vendus en téléchargement, par la boutique MarketPlace du XBOX LIVE.

    Cette limite a été fixée par Microsoft de manière à ce que les jeux téléchargés tiennent sur une carte mémoire 64 Mo, les XBOX 360 d’entrée de gamme n’ayant pas de disque dur.

    La semaine dernière, le 3 avril pour être précis, Microsoft a sortis un nouveau modéle de carte mémoire 512 Mo pour XBOX 360, et étendue la limite de mémoire à 150 Mo.

  12. Il y a jeu commercial et jeu commercial.. Dans le cas du MarketPlace, il s’agit surtout de jeux anciens remis au gout du jour, et vendu pour quelques euros. Microsoft désire dévelloper le concept afin d’avoir un gros catalogue de petits jeux XBOX 360, vendu uniquement en téléchargement. J’aime bien l’idée d’un jeu sympa de 50 Mo vendu moins cher qu’une place de cinéma!

  13. Ce n’est pas eux qui l’ont inventé. Il y’a eu bcp de précurseurs pour cette idée, notamment avec les jeux de la collection oldies but goldies, mais je dois dire que le système mis en place par Nintendo avec le Wii est particulièrement bien fait. On achete des points Wii et avec ces points on télécharge un jeu issu d’une console de génération moindre. Comme tu le dis on se retrouve avec un jeu plus “petit” pas trop cher, et qui fait passer un bon moment.

    Dans ce cas les 50 megas sont compréhensibles, parceque une application Xna digne de ce nom ne fera jamais une taille aussi dérisoire.

  14. C’est clair qu’avec 50 Mo on ne fait pas grand chose.. Heureusement que la limite a été reportée a 150 Mo depuis l’apparition des cartes mémoires XBOX 512 Mo. Cela ouvre des possibilités intéressantes. A titre de comparaison, l’un des jeux les plus connus au monde, le RTS Starcraft occupe 120 Mo.

  15. Oui, je l’avais constaté par moi-même à l’époque de sa sortie avec mon vieux P75. Un bel exemple d’utilisation des techniques graphiques 2D. Un mode graphique 640×480 pixels en 256 couleurs, des tuiles rectangulaires de 32×32 pixels, des sprites, beaucoup d’imagination, de bons graphistes et quelques années de travail.. J’ai passé des mois a étudier Starcraft dans tous les détails.

  16. Je viens de retomber sur mon vieux CD de Warcraft II, sortit en 1995. Aprés installation, le jeu occupe 26 Mo sur le disque dur. Un autre grand jeu de Blizzard utilisant des techniques graphiques 2D simples et un excellent gameplay. La création d’un jeu similaire en XNA pourrait être un projet intéressant.

  17. très interessant. J’en ai déjà reproduit un assez similaire, en fait souvent j’essaye de reproduire les jeux blizzard avec les techno MS. J’ai refais un warcraft 3 et je bosse sur un Wow like.

    Warcraft 2 j’y joue toujours :)

  18. Personnellement, j’ai cessé de jouer à Warcraft 2 depuis fort longtemps. Je ne supporte plus la stupidité des unités que je suis censé contôler. Il suffit de laisser un groupe seul quelques instants pour qu’il se fasse massacrer de manière stupide. L’IA pourrait être grandement améliorée sur ce point.

  19. C’est vrai, mais je ne joue pas pour l’ia à ce jeu. J’y joe pour l’ambiance, le graphisme et la petite larme à l’oeil (j’ouvre aussi toujours war1 :) ).

    Je travail depuis qq temps à un système novateur d’IA basé sur WF (je ne sais pas si tu connais), et j’espère bien créer qq chose de novateur.

  20. Non, je ne connait pas WF. Je ne suis qu’un bricoleur autodidacte avec d’énormes trous dans mes connaissances informatiques. Tout ce que je sais sur l’IA je l’ai appris en lisant un livre sur les systèmes multi-agents, que j’ai adoré. Et comme je suis totalement rébarbatif à l’anglais (snif!) cela limite drastiquement mes sources d’informations..

  21. Si je comprend bien les Workflows sont une méthode de modélisation des interactions dans un système composé d’éléments trés différents, comme une entreprise, une administration ou une usine. Et je présume que les flows sont les “flux de communication” entre les diverses entités, pour donner des ordres, demander des informations ou informer de changement d’états. J’ai déja vu ce genre de mécanisme dans des ouvrages de sociologie. Cela me semble trés proche des systèmes organisés d’agents que je connait.

    Une bonne modélisation est necessaire à la création d’une IA de jeux vidéo. J’ai toujours pensé que les IA des jeux vidéos, tout particulièrement des RTS étaient mal pensées sur ce point.

  22. c’est le blog manager qui a ajouté ce commentaire :)

    le sommom pour l’ia merdique c’est les mights and magics
    s’en est même très drôle

  23. Je n’ai pas joué aux différents Mights and magic. Je n’ai jamais été vraiment convaincu par les jeux de rôles informatiques, alors que j’ai joué des centaines de parties de “vrai Jeu de Rôle” autour d’une table avec des amis. Le premier JdR informatique qui a trouvé grace a mes yeux est NeverWinter Night. Superbe jeu, mais l’IA des suivants du joueur est assez ridicule également..

    Enfin quand je dis IA, juste des scripts stupides s’executant mécaniquement sans tenir compte de la situation. C’est assez hallucinant de voir les réactions des lanceurs de sorts. Enfin on comprend mieux quand on découvre que leurs actions sont déterminés par des régles simples simples, “voir ennemi => attaquer”.. Et des tirages aléatoires ensuite.

  24. Might and magic, (surtout le 6 et 7) ont été les meilleurs jeux auxquels j’ai pu jouer dans ma vie.

    Des graphismes mochesn une ia déplorable (mobs bloqués par un arbre), mais une ambiance incroyable, un monde vaste et un univers démentiel.

    J’espère toujours revoir un jeu de ce genre dans ma vie :)

  25. Maintenant que le XNA 1.0 Refresh est en ligne, pourrait-tu nous donner une explication sur l’utilisation des deux nouvelles fonctions du pour les polices de caractéres ?

  26. oui j’ai prévu ca.
    Je travaille sur un sample d’ui pour créer des boutons, textbox, liste déroulante en Xna.
    J’en chie bien :)

  27. Comment fait-on pour passer d’un fichier .jpg ou .bmp à un fichier.x ?
    L’utilisation de metasequoiaLE n’a pas l’air très simple…

  28. qu’entend tu par “metasequoiaLE”.
    La transformation d’un fichier jpg à un fichier x est expliqué dans le tutorial sur les textures.
    C’est une opération qui se réalise en aval de la compilation sous Visual Studio Express.

  29. “metasequoiaLE” est un logiciel gratuit permettant la modelisation 3D.
    Oui, je sais bien qu’il y a des exemples mais le problème, c’est que c’est le même sur tous les sites, celui de l’avion et il est hyper compliqué.
    Nul part est expliqué simplement le passage d’un .jpg à un .x, ce qui est dommage car je pense que ce serait un gain de temps plutôt que de tenter de comprendre un exemple complexe alors que nous avons besoin de quelque chose de plus simple (enfin je pense).

  30. passer d’un jpg à un .x il faut m’expliquer comment. Un jpg est une image, un .x est la serialisation d’un modèle 3D. L’image contient des infos 3D ?

  31. En lisant la question de typh, je suppose que celui-ci ne s’intéresse pas a la programmation mais plutôt a la création d’objets avec un logiciel de modelage 3D. Et qu’il recherche un tutorial sur la manière de créer un modèle très simple, comme par exemple une poutre rectangulaire recouverte d’une texture de bois. C’est un sujet intéressant dont j’ignore tout, hélas ! Il me semble avoir vu des tutoriaux sur la question en survolant très rapidement des sites sur l’infographie.

    Grâce au tutorial de valentin sur les textures, il est possible de créer des « objets 3D texturés » simples en quelques dizaines de lignes de code, directement à l’intérieur d’un logiciel. Mais cela n’a pas grand-chose à voir avec l’utilisation d’un modeleur 3D.

  32. Je viens d’examiner l’exemple d’aéromodélisme. Il y a eu effectivement création d’un modèle 3D (au format .x) à partir d’une série de photos. Je pense que l’auteur a étudié les différentes photos pour comprendre la forme de l’appareil, fait quelques croquis sur papier, puis décomposé la structure en formes simples ; barres de liaisons et cylindres. Il a ensuite utilisé son logiciel de modelage 3D pour recréer chaque pièce individuellement. La phase suivante a été le « collage » des pièces les unes sur les autres, en utilisant une fonction du modeleur 3D. C’est un peu comment un artiste modelant de l’argile pour sculpter un visage. Le nez et les oreilles sont réalisés a part, puis collés sur l’œuvre.

    Une fois le modèle terminé, l’auteur a recherché une texture ressemblant à l’aspect de l’engin sur les photos. Avec un peu de chance, son modeleur 3D avait déjà une bonne texture. Sinon, il a dus explorer les sites d’infographies a la recherche d’une texture gratuite correspondante. Sa recherche terminée, il a indiqué à son modeleur 3D que les surfaces de l’engin doivent être recouvertes de la texture « machin ».

    Bon, je résume et je saute certainement des détails très importants, mais l’idée de base est là.

  33. Mmm, je me suis mal exprimé.. Cela ne me dérange pas de passer par le . Ce que je voudrais c’est générer des fichiers xnb indépendament de la phase de compilation. Pour définir des données graphiques avec un utilitaire capable de fabriquer du xnb a la volée.

    Ce sera plus précis avec un exemple.. Comme tu l’a montré dans un autre post, on peut créer des polices de caractéres avec le , en écrivant des tas d’infos dans un fichier XML, transformé par le compilateur du C# en un fichier xnb. Moi j’aimerais fabriquer mes xnb de polices avec un petit utilitaire, au lieu de compiler un xml.

  34. Vous savez s’il existe des logiciels gratuits pour convertir les fichiers créés en 3D par SolidWorks (*.sldasm) en fichier DirectX (*.x)?
    Je ne développe pas de Jeux, mais une application Windows avec C#. Dans cette application, il y aura une fenêtre qui affiche des images en 3D qui présentent quelques mouvements d’un robot.

    Merci d’avance
    Lingli

  35. Non désolée je n’ai pas entendu parler de tels logiciels.

    Tu peux toujours tenter de faire un reader pour ce format.

Leave a Reply

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


*

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