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 asynchrones : transparent pour l'utilisateur, léger pour la mémoire, rapide pour le GPU.
La solution passe par un chargement en deux étapes :
-
Tout d'abord la lecture du fichier avec extraction des données.
-
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 :
Le 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 :

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.
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).
Le 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).
A bientôt sur ce Blog !
Valentin Billotte