Progressive Mesh (PMesh) / MultiResolutionMesh (MRM) in Managed DirectX

L’utilisation de MRM est une véritable merveille pour accroître de manière significative les performances de ses applications. Le principe est simple : transformer un model 3D détaillé et formé de très nombreux polygones en une version allégée utilisant moins de faces mais aillant un aspect aussi similaire que possible.


Cette technique est la plupart du temps couplée avec la camera pour réduire proportionnellement la qualité du modèle 3D en fonction de la distance à laquelle il se trouve de la camera.


Au final, nous pouvons ainsi grappiller des FPS et libérer le GPU en lui évitant d’afficher des modèles détaillés au loin, là ou une version grandement allégée donne le même résultat à l’écran.


L’image suivant explicite cela :


 


 


 


La porche a 25% donne le même affichage a grande distance que le modèle a 100%.


 


Microsoft nous a gratifiés d’une classe ProgressiveMesh efficace qui répond spécifiquement à ce besoin. Cette classe possède pourtant quelques lacunes :


 


  • Elle est lourde à charger.
  • Elle ne peut pas se charger de manière asynchrone.
  • Elle ne permet pas un contrôle de l’opération de « réduction » de vertices.
  • Elle ne permet pas de gérer les vertices forts (vertices qui ne doivent pas être supprimés lors d’une opération de réduction).

 


Je vous invite d’ailleurs à regarder le sample fourni avec le SDK DirectX.


 


Les MRM occupent une place prépondérante dans mon moteur 3D : Je les utilise  pour libérer du traitement GPU sur l’affichage des objets lointains  et aussi dans l’occlusion culling (l’occlusion consiste à supprimer de l’affichage les objets qui sont cachés par d’autres objets nous y reviendrons dans un autre post). Avant l’utilisation des MRM mon jeu affichait vaillamment 60 FPS pour à peu près 40000 faces affichées. C’est loin d’être mauvais, mais proche d’être bon… Je n’utilisais pas encore l’affichage de lacs, d’effets spéciaux, d’IA, d’effets météorologiques… Au final je serais descendu en deçà de la barre fatidique des 30 FPS. Après cette technique et avant l’utilisation du culling je suis revenu à 80fps.


 


 


Avant de continuer il convient de tester votre motivation à utiliser un algorithme autre que ceux qui existent déjà sur le “marché”. Je pense là bien entendu à la classe ProgressiveMesh de DirectX. Pour ma part plusieurs raisons m’ont poussées à utiliser mon propre code plutôt que celui de Microsoft.


 


  • Le résultat de la classe ProgressiveMesh pour une portion de terrain ne m’allait pas du tout (mes portions sont carrés, la réduction réduisait la surface de ces carrés mais aussi leur forme !).
  • Je voulais avoir un contrôle absolu sur mon code.
  • Je voulais être capable de pouvoir ajouter des effets sans contraintes à la réduction de faces.
  • Enfin je voulais avoir un chargement asynchrone

  


L’algorithme


  


 J’ai donc cherché un moyen de réduire les meshes que j’affichais. J’ai d’abord porté mon choix sur les régions de mon terrain de jeu. Un terrain de jeu dans mon moteur est découpé en X * Y régions carrées. Une région est composée de 32*32 cases soit 1024 faces. Je suis donc parti sur une réduction du nombre de cases. Le premier niveau de réduction m’affichait ainsi 16*16 carrés pour une région. Et ainsi de suite. L’algorithme pour faire ce travail n’était pas trop dur. Il souffrait pourtant de grosses lacunes : les carrés qui disparaissaient pouvaient correspondre au sommet d’un monticule à l’écran, le monticule semblait ainsi translaté par la réduction de faces.  Je n’avais pas de niveaux intermédiaires entre 16*16 et 32*32. La région semblait ainsi soudainement détériorée. Enfin, cet algorithme ne fonctionnait que pour des régions, le résultat pour un arbre ou un personnage était inexploitable.


Je me suis donc tourné vers Google à l’aide des mots clés “MRM algorithme“. J’ai étudié un grand nombre de travaux de chercheurs et de passionnés et j’ai retenu la méthode de Stan Melax parce qu’à priori la meilleure et, cerise sur le gâteau, l’une des plus simples. Melax se base lui-même sur les travaux de H.Hoppes, un développer de MSR (Microsoft Research) dont est issue vraisemblablement l’algorithme se trouvant derrière la classe ProgressiveMesh de Direct3D. Cet algorithme vise a réduire la complexité d’un model 3D par l’intermédiaire de fusions de vertices. S’ensuit :


 


  • La suppression des triangles qui possèdent à la fois les vertices u et v.
  • La mise à jour des triangles qui utilisaient u sur l’un de leurs trois points afin qu’ils utilisent v à la place.
  • La suppression du vertex u.

 


 


A chaque réduction de complexité, deux vertices u et v sont sélectionnés et l'un deux est "fusionné" avec l'autre (ici respectivement u et v).


 


 


 


Le processus est répété jusqu’à atteindre le nombre de vertices ou de faces voulu.


 


Choix de u et v


 


Cette méthode est relativement simple sur le papier et réduit effectivement sans grandes difficultés un model 3D. Il ne faut pourtant pas crier victoire trop vite : j’ai indiqué au départ qu’il fallait que le model “réduit” garde une apparence similaire au model complexe originel. Il convient donc de choisir avec intelligence les sommets u et v à fusionner avec de provoquer la plus fine modification visuelle à l’écran.


 


 


 Le choix du vertex est important !


Le choix du sommet à supprimer est important pour garder une apparence aussi similaire que possible à l’original (image extraite de l’article de Stan Melax avec l’aimable autorisation de l’auteur). 


 


 


C’est là ou le bat blesse : la plupart des algorithmes propose pour ce choix des calculs lourds et inutilisables en temps réel. Sur ce point, Stan Melax a montré son savoir faire. Il part d’un principe élémentaire : les surfaces coplanaires ne demandent pas autant de vertices pour être rendue à l’écran que les surfaces avec un relief prononcé (comme les courbes). Il est plus facile de réduire la complexité des premières, que celle des secondes. Le coût pour la disparition d’un sommet pourrait se traduire ainsi : c’est le résultat de la multiplication de la taille du sommet par l’importance dans la courbe sur laquelle il se trouve.


 


 


Pour la fusion uv, visant à déplacer u vers v nous avons l’algorithme :


 


  • Calculer la taille du sommet (distance u à v).
  • Déterminer l’ensemble des triangles qui possède à la fois u et v.
  • Pour chacun des triangles connectés à u :
    • Calculer le produit orthonormé de la normal du triangle courant avec chacun des triangles qui possèdent à la fois u et v. En extraire le plus petit produit.
  • Calculer le plus gros des plus petits produits : C’est la coùt de la fusion.

La formule mathématique pour calculer ce cout est la suivante est la suivante :


 


 


where Tu is the set of triangles that contain u and Tuv is the set of triangles that contain both u and v.
où Tu est l’ensemble des triangles qui possèdent u and Tuv l’ensemble des triangles qui contiennent à la fois u et v (image extraite de l’article de Stan Melax avec l’aimable autorisation de l’auteur). 


 


 


En fait on parcours tous les triangles connectés à u (u va disparaitre donc ces triangles vont être modifiés pour être connecté a v). Pour chacun de ces triangles on analyse le cout de la fusion avec v. Ce coùt se calcul en analysant la transformation de deux triangles en un seul triangle afin de déterminer l’ampleur de la modification (l’un connecté à u seulement et l’autre connecté à uv qui disprait).


 


 


Le code


 


Vous trouverez un sample DirectX à la fin de cet article. Pour l’heure la classe ProgressiveMesh que vous pouvez reprendre est industrialisable mais est encore améliorable :


 


  • Je n’ai pas terminé l’association d’un poid à chaque vertex pour la transformation.
  • Je n’ai pas faire de trie sur la liste de vertices pour classer par ordre de coùt de transformation.
  • Je n’ai pas fait de méthodes d’optimisation comme dans la classe Mesh de Direct3DX (fusionner les vertices qui sont assez proches pour ne faire qu’un).

 


Trève de bavardage voyons le code.


 


J’ai placé le code que vous pouvez reprendre dans un répertoire du projet nommé “HereAreTheClassesToAddToYourProject” (si ca c’est pas explicite :) ). On y trouve 4 types :


 


  1. ProgressiveMesh<T>
  2. Resolution
  3. VertexAttributeType
  4. VertexAttribute

 


La création d’un mesh progressif et son affichage se fait en trois étapes.


 


Tout d’abord la création de l’objet (étape asynchrone ) en passant au constructeur la liste des vertices qui composent le mesh, la liste des indices pour relier les vertices et enfin les poids associés aux vertices (un tableau de boolèéns pour l’heure) :

ProgressiveMesh<ObjectVertex> mesh = new ProgressiveMesh<ObjectVertex>(vertices, ints, null);


Puis le chargement des buffers 3D lorsque le device a été créé

mesh.Load(this._device);


Enfin l’affichage du mesh

this.mesh.Render();


Comme on le voit la gestion des MRM est beaucoup plus simple qu’avec la classe de Direct3DX.


Revenons sur la généricité de la classe. Le développeur doit indiquer un argument de spécificité a la classe générique ProgressiveMesh (ici ObjectVertex). Il s’agit en fait de la classe utilisée pour le type de vertices.


A partir de cet argument, la classe pourra extraire les positions de chaque vertices et, avec les indices, calculer les différentes résolutions. Elle pourra en outre extraire le format des vertices pour l’affichage, elle pourra aussi renvoyer à l’utilisateur pour chaque résolution es buffers et array de vertices actuellement affichés.


Pour être capable de lire cette structure parfaitement, le développeur doit ajouter a certains membres un attribute qui va indiquer si le champ est l’abscisse, l’ordonnée ou la profondeur de la position du vertex, s’il s’agit du format du vertex ou si il ne s’agit pas d’un champs interessant pour le calcul des résolutions.


Il est donc néccessaire de se créer une custom structure de vertices en lieu et place du sempiternel CustomVertex.PositionNormalTextured.


Voici par exemple une partie de la structure ObjectVertex :


 


    /// <summary>


    /// <para>Custom vertex types.</para>


    /// </summary>


    [StructLayout(LayoutKind.Sequential)]


    public struct ObjectVertex


    {


        /// <summary>


        /// <para>Vertex position on X-axis.</para>


        /// </summary>


        [VertexProperty(VertexPropertyType.X)]


        public float X;


        /// <summary>


        /// <para>Vertex position on Y-axis.</para>


        /// </summary>


        [VertexProperty(VertexPropertyType.Y)]


        public float Y;


        /// <summary>


        /// <para>Vertex position on Z-axis.</para>


        /// </summary>


        [VertexProperty(VertexPropertyType.Z)]


        public float Z;


       


        /// <summary>


        /// <para>Vertex’s normal x value.</para>


        /// </summary>


        public float Nx;


 


        /// <summary>


        /// <para>Vertex’s normal y value.</para>


        /// </summary>


        public float Ny;


 


        /// <summary>


        /// <para>Vertex’s normal z value.</para>


        /// </summary>


        public float Nz;


 


 


        /// <summary>


        /// <para>TerrainTexturedVertex’s format.</para>


        /// </summary>


        [VertexProperty(VertexPropertyType.Format)]


        public const VertexFormats Format =(VertexFormats)0x112;


 


 


J’ai juste utilisé reflector sur sur la structure PositionNormalTextured, j’ai copié collé la classe dans mon code et j’ai rajouté les attributes.


Pour changer la résolutions deux choix. Soit vous utilisez la propriétés NumberOfVertices pour indiquer directement le nombre de vertices a afficher. Soit vous utilisez la property Resolution en donnant un membre de l’énumération :


 

   public enum Resolution

    {

        /// <summary>

        /// <para>No resolution : no mesh rendered.</para>

        /// </summary>

        None = 0,

 

        /// <summary>

        /// <para>No resolution : no mesh rendered.</para>

        /// </summary>

        Dynamic = -1,

 

        /// <summary>

        /// <para>Ten percents of the mesh’s vertices used to render it.</para>

        /// </summary>

        Lowest = 10,

 

        /// <summary>

        /// <para>Twenty five percents of the mesh’s vertices used to render it.</para>

        /// </summary>

        Low = 25,

 

        /// <summary>

        /// <para>Fivety percents of the mesh’s vertices used to render it.</para>

        /// </summary>

        Medium = 50,

 

        /// <summary>

        /// <para>Seventy five percents of the mesh’s vertices used to render it.</para>

        /// </summary>

        High = 75,

 

        /// <summary>

        /// <para>One hundred percents of the mesh’s vertices used to render it.</para>

        /// <remarks>Full vertices are used.</remarks>

        /// </summary>

        Full = 100

 

    }


Le changement de la résolutiond’un mesh demande un leger temps de calcul à priori invisible à l’écran.


 


 


Les liens


 


Evidemment en première place le site de Stan Melax 


http://www.melax.com/polychop/


 


Ensuite le site de Hugues Hoppes


http://research.microsoft.com/~hoppe/


(déprimant parcequ’on voit qu’il fait des trucs super, mais on comprend rien)


 


Le site de reflector que j’utilise pour extraire le code de CustomVertex.PositionNOrmalTextured 


http://www.aisto.com/roeder/dotnet/


 


 


Conclusion

 

L’application au final se présente ainsi :

 

Application de test


 


Je me base sur les samples de DirectX. Détail croustillant j’ai pris le projet ProgressiveMesh de H.Hoppes j’ai supprimé tout le code et j’ai rajouté le miens pour que l’interface soit similaire :)


 


La barre de scroll ou la combobox de resolution changent le détail du mesh courant.


La checkbox permet de voir en fil de fer ou non (en mode solide vous pouvez voir à quel point certains meshs ne changent pratiquement pas de forme).


Enfin la combobox Mesh vous permet de choisir entre 5 modèles de meshs : Porche, Terrain, Lapin, Vache, Fourmi (ce dernier montre les limitation de la réduction de résolution sur un mesh fin).


 


 


A noter que le modèle terrain sert à voir l’utilité du tableau de poids passé au constructeur. Sans ce poid, la portion de terrain “s’arrondit” au fil de la réduction de résolution…


 


N’hesitez pas à me faire des retours surtout ! Pour ma part je vais peaufiner encore le code pour faire fonctionner cette classe dans mon moteur.


 


[Soon]


 


Valentin Billotte



Sample 


telecharger Vous pouvez télécharger le sample ici.

Les Regular Expression & la "Find And Replace" dialog box de Visual Studio

J’étais en train de préparer le sample de mon prochain post sur ce blog (sur les progressive Mesh) lorsque je me suis rendu compte que je n’avais qu’un exemple à donner pour mettre en évidence le LOD (Level Of Detail).


J’ai donc cherché sur Internet des models 3D à incorporer rapidement dans mon programme. Je me suis tourné tout naturellement vers les programmes de MRM (MultiResolutionMesh) qui existent déjà et notamment vers celui de Stan Melax (www.melax.com) le grand nom du MRM. Il possède tout un tas de fichiers très interessants au format PLY. Fichtre ! Kesaco ?


En fait je m’en f…iche. Continuons. J’ouvre le fichier, il est aisément lisible : au format Txt il énumère dans un header le nombre de vertices, le nombre de faces, deux trois informations sans importances, puis contient toutes les positions des vertices du model et une énumération des indices des vertices de chaque face. Du genre :


 


ply
format ascii 1.0
element vertex 2903
property float32 x
property float32 y
property float32 z
element face 5804
property list uint8 int32 vertex_indices
end_header
0.605538 0.183122 -0.472278
0.649223 0.1297 -0.494875
0.601082 0.105512 -0.533343
[…]


3 0 1 2
3 1 3 4
3 5 6 2
3 6 7 8
3 7 9 10
[…]


 


Formidable.


J’utilisais à présent la même façon de procéder que Stan Melax et son programme en C : j’ai deux constantes qui sont deux tableaux à deux dimensions : le premier tableau donne pour chaque vertex les trois composantes X, Y et Z d’une position orthonormée 3D. Le second donne pour chaque face, les indices de chaque vertex qui la compose. Le truc bien lourd mais au combien rapide à faire :Pour prendre son modèle Rabbit, j’ai juste eu à faire un copié collé de son fichier .c (le C# n’est il pas le fils objet du C ? :) ). En gros pour expliquer en image, pour les vertices j’ai :


 


        static float[,] rabbit_vertices = new float[RABBIT_VERTEX_NUM, 3]{


      {-0.334392f,0.133007f,0.062259f},


      {-0.350189f,0.150354f,-0.147769f},


      {-0.234201f,0.343811f,-0.174307f},


      {-0.200259f,0.285207f,0.093749f},


et pour les faces j’ai :


        static int[,] rabbit_triangles = new int[RABBIT_TRIANGLE_NUM, 3]{
     
{126,134,133},
      {342,138,134},
      {133,134,138},
      {126,342,134},


 


Je voulais donc opérer de même pour les autres modèles à ajouter à mon sample. Sauf que là : pas de fichier .c, mais .PLY. Bon ok je reviens, vais regarder à quoi ca correspond.


Ok allez voir là si ca vous interesse : http://www.cc.gatech.edu/projects/large_models/ply.html.


Donc je me vois face à trois solutions :


 


  • A.J’abandonne ou je cherche ailleur.
  • B.J’essaye, vertex par vertex, face par face, de réécrire les tableaux de vertices et de faces (au bas mot une semaine de travail par modèle).
  • C.J’utilise les regular expression à l’aide de la Find and Replace dialog box de VS.
  • D.La réponse D.

 


Evidemment, si vous avez lu le titre de ce post, vous aurez compris que je choisi la réponse C Jean Pierre. Je suis un grand fana des REGEXP depuis ma plus tendre… depuis que je développe en fait. J’ai débuté sous Linux (pas frapper svp) et j’ai donc rapidement aimé ce style de développement simple (oui c simple à écrire, impossible à relire certes…, mais simple à écrire) et efficace. Beaucoup de développeurs répugnent à utiliser les expressions régulières. Ils ont tord. Sous prétexte que le code est difficilement lisible ils préfèrent utiliser de longues suites de string.IndexOf, string.SubString etc. Rendant le code encore plus illsible et moins maintenable…


La je vais démontrer que je vais transformer un fichier Ply en deux tableaux à deux dimensions, de 5000 vertices et 10000 faces en moins de 20s chronos ! (faut imaginer le Valentin, seul dans son bureau le midi en train de se chronometrer a faire des Replaces … ca vaut le détour)..


Voici le travail :


Secondes 1 à 3 : 


Copié de la portion du fichier Ply consacrée aux vertices. 5000 lignes de :


[…]
0.605538 0.183122 -0.472278
0.649223 0.1297 -0.494875
0.601082 0.105512 -0.533343
[…]


Chaque ligne contient trois nombres correspondants a X, Y Z.


Secondes 4 à 10 :


Alt Tab pour revenir sur VS (un fichier txt vide m’attend).
Collé de la portion préalablement copiée.
Ctrl + H pour la  Find and Replace dialog box de VS.
Je coche “Use Regular Expression“.
Tout d’abord je supprime les espaces de fin de ligne :


Fin What=([ \t]*$)
Replace With=<Vide>


(Fin What et Replace With correspondent aux deux texbox de la dialog box)


En suite j’ajoute à chaque fin de ligne la string “f},“. f pour que les nombres soient lus en flottants.


Fin What=$
Replace With=f\},


Je remplace ensuite le espaces entre chaque chiffres de chaque lignes (deux espaces par lignes restant donc) par la string “f, “.


Fin What
Replace With=f,


Enfin, je fais débuter chaque ligne par une accolade.


Fin What=^
Replace With=\{


Terminé. La portion de fichier que j’ai donné un peu plus haut devient :


 


{0.605538f, 0.183122f, -0.472278f},


{0.649223f, 0.1297f, -0.494875f},


{0.601082f, 0.105512f, -0.533343f},


 



Je réitère l’opération pour le tableau de face (pareil sans avoir a mettre de f après chaque nombre). 10 * 2  = 20s. En une minute je créé 3 à 4 modèles 3D de plusieurs milliers de lignes de code.


L’image suivante explicite cela plus en détails.


Replace in action


La il s’agit juste d’une exemple assez trivial. Le replace à l’aide de regexp est réèllement puissant dans la vie du développeur et fait gagner enormément de temps. Surtout quand on arrive à maitriser le remplacement de portions de texte. Ne gardez pas cette merveilleuse technologie uniquement à l’intérieur de votre code.


Allez je retourne à mon sample MRM


Notes :


Qqs liens interessants pour les régulars expressions :


Une application de test que j’utilise énormément pour tester mes expressions:
http://www.regular-expressions.info/dotnetexample.html

 

Jettez un oeil à ce site d’ailleur :


 


 

 

Bonne lecture.

 

 

 

 


 

"Attempted to read or write protected memory. This is often an indication that other memory is corrupt."

 


Un post sur l’utilité de la méthode Clear de ICollection 


Voila une exception de type System.AccessViolationException qui peut vous gâcher la vie lorsque vous manipulez des listes. Beaucoup de développeurs DirectX débutants tombent dessus sans savoir que la réponse est toute simple.


I n c o m p r é h e n s i b l e au premier abord.


Si vous regardez sur le net les autres personnes qui sont tombées dessus, soit elles n’y comprennent rien, soit elles tombent sur des gens qui leur donnent des réponses complètement à coté de la plaque (c’est très drôle d’ailleur vous avez des mecs qui tentent de supprimer des processus, d’autres qui placent des Sleep etc. [:D]). Et comme à chaque fois, y’en a que pour ASP.Net cette technologie d’arrière garde et rien pour WinForm DirectX, ou .Net en général.


Voila ce qui se passe pourtant dans la plupart des cas.


Vous avez un VertexBuffer que vous voulez remplir à nouveau de valeurs issues d’une liste générique du type List<CustomVertex.PositionTextured>. Pour remplir c’est simple :


vertexBuffer.SetData(this.vertices.ToArray(), 0, 0);


(ici vertices est notre liste générique). C’est pourtant ici qu’est lancée l’exception. En fait l’erreur vient de la manière dont vous remplissez votre liste générique. Le VertexBuffer a été créé avec une taille fixe qui indique le nombre de vertices qu’il va acceuillir. Or nombre de développeurs DirectX en herbe utilisent les listes générique pour remplir le vertexbuffer. Lorsqu’il faut le remplir de nouvelles valeurs, ils utilisent la meme liste générique, la remplissent à nouveau et appellent SetData. Sauf que … ils n’ont pas appellé la méthode Clear en amont… Résultat, la méthode ToArray renvoie un tableau avec deux fois plus de vertices que le buffer ne peut accepter. Le moteur tente donc d’écrire en dehors du buffer, là ou nous n’y sommes pas autorisé et l’exception est inévitable.


Il faut donc bien veiller à être très stricte dans la manière dont on utilise les collection. Ce ne sont pas des tableaux.


 


 


 

Chargement asynchrone d’objets 3D

La classe Mesh et la classe ProgressiveMesh de Direct3D (D3DX pour être précis) sont d’une très grande utilité lorsqu’il s’agit de réaliser un sample pour le DSK DirectX browser ou pour réaliser soit même des tests… mais dès qu’il s’agit de développer un moteur ou d’exploiter à outrance les performances de sa machine, ces deux classes sont à proscrire :


 


  • Elles sont lentes à charger,même en lisant des .x binaires. Mesh offre de nombreuses fonctionnalités qui demandent un chargement avec plus de traitement.
  • Elles ne permettent pas un chargement à deux temps. Par chargement en deux temps j’entend pouvoir, à la manière du développement Winform, placer en asychrone tout traitements compatibles, et réduire au maximum les traitements synchrones de chargement.

En quoi ceci est il réellement handicapant ?


Je vais donner un exemple simple : dans le moteur que je développe en Managed DirectX 1.1/.Net 2.0 j’ai jusqu’à 1400 meshs affichés par frames. Utiliser la classe Mesh m’empêche de pouvoir charger un Mesh en mémoire lorsque j’ai besoin de l’afficher tout simplement parce que je vais “freezer” le jeu le temps du chargement. En effet je ne peux pas effectuer avec cette classe un chargement asynchrone (utiliser un device 3D en dehors du thread dans lequel le device a été créé est source d’instabilité). Si je n’avais que ponctuellement un Mesh à afficher cette technique serait passable (dans le cas de petits meshs toutefois) mais dans mon moteur je charge les meshs quelques instants avant qu’ils apparaissent à la camera. Dans ce cas, charger en synchrone provoquerai des court laps de freeze pour chaque meshs chargés rendant le jeu injouable.


 


 


Mon moteur charge les objets 3D en asynchrone

Mon moteur charge les objets 3D en asynchrones : transparent pour l’utilisateur, léger pour la mémoire, rapide pour le GPU. 


 


 


La solution passe par un chargement en deux étapes :


 


  1. Tout d’abord la lecture du fichier avec extraction des données.
  2. Puis le chargement des buffers utilisé pour l’affichage.

 


Si la seconde partie est très rapide, la première est relativement lente. L’astuce consiste à effectuer cette première partie en asynchrone et la seconde partie entre deux frames (c’est à dire en dehors du triplet Begin/End/Present).


C’est là ou la classe Mesh et ProgressiveMesh nous sont inutiles. Nous devons réaliser des custom classes  possédant deux méthodes : Initialize qui sera appelée à partir d’un thread asynchrone au thread d’affichage principal et Load(Device) appelée dans le thread principal.


Initialize va lire le fichier ou le flux de données et extraire (je simplifie) deux listes : l’une pour les vertices, l’autre pour les indices.


Voici un exemple de ce que pourrait être la méthode Initialize d’une classe générant un terrain de jeu : 


 


 


        private void Initialize()


        {


            // All the vertices are stored in a 1D array


            _vertices = new TerrainTexturedVertex[Region.NumberOfVertices];


 


            int vertexIndex = 0;


            // Load vertices into the buffer one by one


            for (int z = 0; z < Region.NumberOfQuadsOnZAxis + 1; z++)


            {


                for (int x = 0; x < Region.NumberOfQuadsOnXAxis + 1; x++)


                {


                    TerrainTexturedVertex vertex = new TerrainTexturedVertex();


                    vertex.X = myValueX;


                    vertex.Z = myValueZ;


                    vertex.Y = myValueY;


                    vertex.Tu = x * (1f / (float)Region.NumberOfQuadsOnXAxis);


                    vertex.Tv = z * (1f / (float)Region.NumberOfQuadsOnZAxis);


 


                    _vertices[vertexIndex] = vertex;


                    vertexIndex++;


                }


            }


 


            this.ComputeNormals();


        }


 


Tout ce que fait cette méthode se résume à la “construction” d’un tableau de vertices sans aucune référence au device. Il ne s’agit donc qu’une suite répétée de bêtes instructions d’affectation. Aucun difficulté à threader cela.


 


Load va elle créer un VertexBuffer et un IndexBuffer (encore une fois en simplifiant) et va utiliser la méthode SetData de chacun pour leur affecter les listes précédemment chargée.


Voici encore une fois un exemple de ce que pourrait être cette méthode pour notre terrain de jeu :


 


 


         private void LoadVertexBuffer()


        {


            // This is the buffer we are going to store the vertices in


            this._vertexBuffer = new VertexBuffer(


                typeof(TerrainTexturedVertex),


                Region.NumberOfVertices,


                this._device,


                Usage.WriteOnly,


                TerrainTexturedVertex.Format,


                Pool.Managed);


 


 


            // finally assign the vertices array to the buffer


            this.VertexBuffer.SetData(_vertices, 0, LockFlags.None);


        }


 


 


Ici nous créons un VertexBuffer, puis, nous le chargeons à l’aide du tableau préalablement rempli par Initialize. Cette méthode est appelée avant l’appel à BeginScene (jamais de traitements entre Begin et EndScene !!).


 



Sample :


 

 

TelechargerLe sample se trouve ici.


 


Le programme donné en exemple permet de comparer un chargement synchrone d’un chargement asynchrone.


C’est un programme très sale pour l’heure que j’ai fait en qq minutes a partir d’un sample du SDK DirectX :). Je vous prie de m’excuser pour le code …


 


Le lancement de l’application est instanné. Le premier terrain généré est chargé en arrière plan :


 


Lancement de l'application sans Freeze


 


 


Lorsque le terrain est chargé, vous avez deux choix :


 


soit en asynchrone (cochez la case), soit en synchrone (décochez). L’asynchrone est legerement plus lent, mais permet à l’utilisateur de continuer à travailler sur l’application pendant qu’un nouveau terrain se construit en arrière plan.


 


L'application répond toujours alors qu'un nouveau terrain se charge 


 


 


Pour ce sample j’utlise un BackgroundWorker. A l’intérieur de la méthode DoWork (asynchrone) j’appelle la méthode Initialize qui va créer les atitudes en tous points du terrain, créer tous les vertices du terrain, créer la liste des indices. Lorsque le worker rend la main via l’event RunCompleted, j’indique qu’un nouveau VertexBuffer et un nouveau IndexBuffer peuvent être chargés (ce qui est fait au OnFrameMove suivant). Dans le cas d’un mode synchrone j’appelle directement Initialize sur le thread principal.


 


Il s’agit ici d’un sample avec un code alourdit pour mettre en évidence l’avantage de l’asynchrone pour liberer le CPU/GPU. Couplé à un système de message (du type ReadMessage de l’API Win32) on obtient un système réellement puissant (nous verrons celà dans un futur post).


 


 


TelechargerLe sample est téléchargeable ici.


 


Conclusion 


 


Le résultat au final ne souffre de pratiquement aucune perte de FPS.


Cette technique permet aussi d’éviter à l’utilisateur un chargement d’application “inquiétant”. Souvent les samples qui demandent la création d’un grand nombre d’objets 3D freezent durant de longues secondes, le temps que cette opération se fasse.


En utilisant cette technique, nous pouvons afficher une barre de progression le temps de charger au départ tous les objets via leur méthode Initialize puis au moment de la création du device d’appeler leur methode Load. Nous aurons un freeze dont le temps correspondra au nombre de méthodes Load appelées et au traitement que celles-ci effectuent, c’est à dire un temps relativement insignifiant.


En outre :

Cette technique est aussi applicable au chargement de texture.


Cette technique permet une adaptation simplifiée à d’autre technologies multimédia puisque seule la méthode Load doit être modifiée (qui a dit XNA ? :) ).


Cette technique peut être couplée avec l’utilisation d’un système de caching pour limiter au maximum les instances de vos objets 3D pour encore plus de flexibilité et de rapidité (le caching sera traité dans un futur post).


 


[Soon]


 


Valentin Billotte