La dernière fois nous avons vu comment récupérer un Device après qu'il soit perdu - un petit mais important détail dans la création de votre application Direct3D. Pour continuer dans les petites choses qui peuvent faire une grosse différence, j'aimerais parler des IndexBuffers (buffers d'indices).
Pour comprendre le problème que peut résoudre un index buffer, on peut se contenter de regarder un cube. Si vous reflechissiez à comment modéliser un cube, vous verrez qu'il est fait de 12 triangles : 6 carrés chacun fait de 2 triangles.
Si vous aviez l'intention d'afficher un tel cube en utilisant qu'un VertexBuffer, vous auriez besoin de fournir les informations de 36 sommets, vu que chacun des 12 triangles est fait de 3 points. Multipliez chaque sommet par le taille des informations contenu dans un sommet (3 flottants pour la position, 2 flottants pour la texture, 3 flottants pour le vecteur normal, etc..) et vous pouvez constater que notre cube demanderait un total de mémoire non négligeable. Et si votre scène est constituée de centaines ou milliers de ces cubes, alors l'utilisation de la mémoire monte rapidement en flèche.
Vous êtes probablement en train de vous dire "Et alors? La mémoire ça coute rien de nos jours. J'ai 1Go de RAM sur mon ordi". Mais souvenez vous qu'avec Direct3D, on travaille en fait à bas niveau avec le matériel vidéo. Ainsi, on écrit des programmes qui tournent à la fois sur le CPU et sur le GPU (Graphics Processing Unit, la puce qui donne la puissance à votre carte graphique). Bien que les GPUs sont puissants ces temps ci, ils sont (probablement) pas aussi puissants que le CPU, et les cartes graphiques n'ont pas autant de mémoire que l'ordinateur. De plus, toute information qui doit être amenée à la mémoire de la carte graphique passe par le bus système. Que ça soit du PCI ou de l'AGP, ce n'est pas infiniment rapide et moins y aura d'information à faire transiter, mieux ça sera.
Maintenant on peut se dire "Pas de problème, il y a les TriangleStrip qui sont utiles dans ces cas là". Avec un TriangleStrip on a à envoyer les infos complètes sur le premier triangle et ensuite, juste un sommet supplémentaire pour chaque triangle suivant. Vu que chaque triangle consécutif partage 2 sommets avec son prédécesseur, nous économisons de la transmission, du stoquage, et du traitement d'un tas d'information redondante.
TriangleStrip
Cependant, on ne peut pas faire une bonne utilisation des TriangleStrip sur notre cube. Quand on y pense, on s'aperçoit qu'il n'y a aucun moyen de créer le cube avec un TriangleStrip unique. Le mieux qu'on puisse faire c'est avec 2 TriangleStrip. Bien que cela semble pas une économie considérable, souvenez que parfois on dessine des centaines ou milliers d'instances d'une forme particulière, et les économies grandissent. Et il ya plein de formes qu'on voudrait peut être dessiner avec lesquelles les TrianglesStrips seraient bien plus utiles qu'avec le cube.
La solution à notre problème sera familière à n'importe qui ayant déjà tâté de la programmation de bases de données. Dans une base de données, si il s'avère qu'une information se trouve à deux endroits différents, vous pouvez vous débarrasser de cette redondance en utilisant une technique appeléenormalisation. L'idée de base est que vous stoquez l'information qu'une fois et ensuite y faites références avec un identifiant (ndt : j'ai écrit deux paragraphes si ça vous intéresse, en bas de page).
Dans notre cube, on a en fait que 8 points : les coins du cube. Chaque triangle constituant le cube est fait de 3 de ces points. Donc ce que nous désirons vraiement est un moyen de définir les coins qu'une fois et ensuite fournir une liste de triangles comme ceci "Celui ci est fait du coin 1, 2 et 3. Celui la est fait du coin 5, 8 et 2.." etc. C'est exactement ce qu'il se passe avec un index buffer.
Voici une image exprimant l'idée. En bleu nous avons un IndexBuffer qui n'est à la base qu'une suite d'indices dans un VertexBuffer. Un IndexBuffer peut référencer un certain sommet plus d'une fois, c'est là où l'économie intervient : c'est moins lourd de stoquer le nombre "3" deux fois que de stoquer les informations du sommet 3 plein de fois.
Travailler avec un IndexBuffer est très proche de travailler avec un VertexBuffer. Vous créez simplement un objet IndexBuffer, le verrouillez, le remplissez de données et le déverouillez. Cependant ce que nous remplissons dans ce cas sont les indices des sommets de l'objet qu'on veut dessiner. Ces sommets sont stoqués dans un VertexBuffer, comme toujours, mais on a plus à stoquer les sommets redondants, nous les référençons plusieurs fois en stoquant leur indice plusieurs fois dans l'IndexBuffer.
Un exemple pourrait éclairer cela. Voici le code modifié pour créer un VertexBuffer :
protected VertexBuffer CreateVertexBuffer(Device device) { device.VertexFormat = CustomVertex.PositionColored.Format; VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionColored), // Quel type de sommets 8, // Combien device, // Le device 0, // Utilisation par défaut CustomVertex.PositionColored.Format, // Format de sommets Pool.Default); // Pooling par défaut CustomVertex.PositionColored[] verts = (CustomVertex.PositionColored[]) buf.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionColored( -0.5F, 0.5F, -0.5F, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0.5F, -0.5F, Color.Orange.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, -0.5F, -0.5F, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, -0.5F, -0.5F, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, 0.5F, 0.5F, Color.Blue.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0.5F, 0.5F, Color.Violet.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, -0.5F, 0.5F, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, -0.5F, 0.5F, Color.Orange.ToArgb()); buf.Unlock(); return buf; }
Ceci devrait être assez familier maintenant. La seule différence est que je crée 8 sommets pour le cube cette fois au lieu des 3 sommets du triangle habituel.
Du nouveau code intervient avec la création de l'IndexBuffer. Voici le code pour ça :
protected IndexBuffer CreateIndexBuffer(Device device) { IndexBuffer buf = new IndexBuffer( typeof(short), // Comment seront les indices? 36, // Combien il y aura d'indices? device, // Le device 0, // Paramètre avancé Pool.Default // Paramètre avancé ); short[] arr = (short[]) buf.Lock(0, 0); int i = 0; // Face arr[i++] = 0; arr[i++] = 3; arr[i++] = 1; arr[i++] = 0; arr[i++] = 2; arr[i++] = 3; // Dessus arr[i++] = 0; arr[i++] = 1; arr[i++] = 4; arr[i++] = 1; arr[i++] = 5; arr[i++] = 4; // Gauche arr[i++] = 0; arr[i++] = 4; arr[i++] = 2; arr[i++] = 2; arr[i++] = 4; arr[i++] = 6; // Droite arr[i++] = 1; arr[i++] = 7; arr[i++] = 5; arr[i++] = 1; arr[i++] = 3; arr[i++] = 7; // Derrière arr[i++] = 4; arr[i++] = 5; arr[i++] = 6; arr[i++] = 5; arr[i++] = 7; arr[i++] = 6; // Dessous arr[i++] = 2; arr[i++] = 6; arr[i++] = 3; arr[i++] = 3; arr[i++] = 6; arr[i++] = 7; buf.Unlock(); return buf; }
Comme je l'ai dit, ça ressemble beaucoup à la création d'un VertexBuffer. Nous commençons par le constructeur :
IndexBuffer buf = new IndexBuffer( typeof(short), // Comment seront les indices? 36, // Combien il y aura d'indices? device, // Le device 0, // Paramètre avancé Pool.Default // Paramètre avancé );
Les arguments ici indiquent quel type de donnée on va stoquer, le nombre d'indices qu'on prévoit de mettre, l'objet Device (bien sûr), et une paire de paramètres qui dépassent le cadre de cet article. Ici nous utilisons des entiers courts 16-bit pour stoquer les indices, l'autre choix possible sont les entiers long 32-bit.
Une fois que l'IndexBuffer est créé, on le verrouille, on y écrit des données et on le déverrouille.
short[] arr = (short[]) buf.Lock(0, 0); int i = 0; // Face arr[i++] = 0; arr[i++] = 3; arr[i++] = 1; arr[i++] = 0; arr[i++] = 2; arr[i++] = 3; // Autres côtés ici buf.Unlock();
Ce que ça dit est "Le premier point est le sommet n°0, le second point est le sommet n°3, le troisième point est le sommet n°1, etc..". Notez qu'on a pas défini quel sommet était le n°0 - cela est défini plus tard, pendant le rendu.
A propos, voici notre méthode Render mise à jour :
protected void Render() { // Efface le back buffer device.Clear(ClearFlags.Target, Color.Bisque, 1.0F, 0); // Prépare Direct3D à dessiner device.BeginScene(); // Met en place les matrices SetupMatrices(); // Dessine la scène - Instructions 3D ici device.SetStreamSource(0, vertices, 0); // Nouveau code : device.Indices = indices; device.DrawIndexedPrimitives( PrimitiveType.TriangleList, // Type de primitives qu'on dessine 0, // Sommet de base 0, // Indice de sommet minimum 8, // Nombre de sommets utilisés 0, // Indice de départ 12); // Nombre de primitives // Indique à Direct3D qu'on a fini de dessiner device.EndScene(); // Copie le back buffer à l'écran device.Present(); }
J'ai indiqué le nouveau code, mais j'aimerais souligner aussi qu'on continue d'appeler SetStreamSource. Souvenez vous : un IndexBuffer nous permet de diminuer le nombre de sommets qu'on fournit, mais nous avons qd meme besoin de sommets et ainsi besoin d'un VertexBuffer.
La première ligne du nouveau code
device.Indices = indices;
associe simplement l'IndexBuffer qu'on a créé au Device. C'est important car lorsqu'on fait des opérations demandant un IndexBuffer, le Device a besoin de savoir lequel on utilise.
La seconde ligne du nouveau code est là ou se situe l'action.
device.DrawIndexedPrimitives( PrimitiveType.TriangleList, // Type de primitives qu'on dessine 0, // Sommet de base 0, // Indice de sommet minimum 8, // Nombre de sommets utilisés 0, // Indice de départ 12); // Nombre de primitives
Rappelez vous de DrawPrimitives, la méthode pour dessiner à l'écran des TriangleList, TriangleStrip ou toute autre primitive dont on a parlé. DrawIndexedPrimitives est l'équivalent mais demande la présnce d'un IndexBuffer.
Des arguments de DrawIndexedPrimitives sont les mêmes que les argumetns de DrawPrimitives. Par exemple, on spécifie toujours un PrimitiveType en premier argument et pouvons fournir la plupart des valeurs (TriangleList, TriangleStrip, etc..) qu'on aurait fourni avec DrawPrimitives. A l'exception dePrimitiveType.PointList qui n'est pas valide pour DrawIndexedPrimitives. Et le dernier argument est toujours le nombre de primitives, c'est à dire, le nombre de triangles, de lignes qu'on dessine. Ce n'est pas le nombre de sommets/indices, comme avec DrawPrimitives.
Le deuxième argument à DrawIndexedPrimitives (je l'abregerai en DIP à partir de maintenant) est l'indice du sommet de base. Ca a un peu la signification d'un point de départ dans le VertexBuffer, afin de nous déplacer à l'interieur du buffer en utilisant des régions différentes à des appels différents. Pour des cas simples comme le notre, donner 0 est suffisant - ça veut juste dire qu'on commence au début du vertex buffer. Si nous avions passé un entier non nul ici, ça voudrait dire qu'on ne veut pas utiliser les sommets du début du VertexBuffer mais plutot commencer quelque part plus loin.
La paire d'arguments suivants indique à DIP la fourchette d'indices qu'on va utiliser. L'indice de sommet minimum est le plus petit indice qu'on prévoit d'utiliser. C'est en relatif par rapport à l'indice du sommet de base passé à l'argument précédent. Ainsi si nous donnons 4 pour cet argument et 6 pour le sommet de base, le matériel sait qu'on utilisera pas les sommets avant le 10ème (6 + 4) dans le vertex buffer. C'est utile pour une optimisation de performance de telle sorte que le matériel puisse faire des prédictions sur quels sommets seront utilisés et zapper le traitement des autres sommets dans le VertexBuffer.
Le nombre de sommets utilisés est un argument qui va de pair avec l'indice de sommet minimum. Ca indque à Direct3D la grosseur du morceau de VertexBuffer qu'on va utiliser. Ce n'est pas valide d'avoir des indices dans l'IndexBuffer inférieurs au sommet de base + indice de sommet minimum, ou plus grands que sommet de base + indice de sommet mini + nombre de sommets utilisés. Là aussi c'est une optimisation de performance.
Le cinqième argument de DIP indique à Direct3D où on commence dans l'IndexBuffer, au cas où on ne voudrait pas commencer au début. Notez que c'est diferent de l'argument du sommet de base, c'est un offset dans l'IndexBuffer, pas dans le VertexBuffer.
Avec tous ces indices, des schémas pourraient aider un peu. Voici le premier :
Cette image montre le cas "normal" dont on a parlé : un indice de sommet de base de 0 (commence au début du VertexBuffer), un indice de sommet minimum de 2 (on utilisera aucun sommet avant le 3ème), 3 pour le nombre de sommets (on utilise les sommets 2 à 4), un indice de départ de 0 (commence au début de l'IndexBuffer).
L'accolade au dessus de l'IndexBuffer est là pour montrer à quelles entrées de l'IndexBuffer correspond cet appel à DIP, ce qui correspondrait à l'affichage d'un simple triangle.
Voici un exemple légèrement plus complexe :
Dans cet exemple, j'ai spécifié un indice de sommet de base non nul. Vous remarquerez que c'est presque pareil que l'exemple précédent, sauf que ça décale de 2 la section du VertexBuffer utilisée, ainsi on utilise les sommets n° 4, 5 et 6.
Pour finir notre présentation des paramètres, regardons ce qui se passe quand on donne un indice de départ non nul :
Regardez ce qui se passe : l'accolade a bougé et se trouve sur un ensemble différents d'indices, sélectionnant un ensemble complètement différent de sommets. C'est parce que l'indice de départ indique un indice dans l'IndexBuffer.
Notez qu'on a du ajuster le nombre de sommets et l'indice de sommet minimum en conséquence. Plus paraticulièrement remarquez que j'ai indiqué 5 pour le nombre de sommets, pas 3 - Direct3D veut savoir la taille de la zone de sommets qu'on va utiliser, pas le nombre qu'on référence.
Maintenant que vous comprenez comment utiliser un IndexBuffer, je devrais mentionner qu'utiliser les primitives indexées n'est pas sans désavantages. L'un des plus grands est la conséquence directe de ce que nous aimons le plus dans les IndexBuffers : ils font partager les sommets. Ce que je veux dire c'est que si vous voulez qu'un sommet ait 2 valeurs différentes sur 2 faces différentes d'un objet, vous ne pourrez pas partager ce sommet. Ca se tient, vous pouvez pas donner 2 vecteurs normaux différents à un sommet, mais vous pouvez facilement donner à 2 sommets différents ayant la même position 2 vecteurs normaux différents.
En pratique ça s'avère ne pas être un gros problème. La plupart des objets vont avoir des tas et des tas de sommets partagés par plus d'un triangle où toutes les informations sont exactement les mêmes pour chaque triangle partageant le sommet. Tant que vous faites attention aux sommets "de bord", vous ne devrez pas avoir de problème.
Hé bé! Ca a été un descriptif sacrément long pour un sujet qui est en fait assez simple. Mais la plupart fut consacrée à notre discution sur l'indice de sommet de base et l'indice de départ qu'on mettra probablement à 0 dans la plupart des cas de toute façon.
Comme d'habitude, j'ai joint un exemple complet ci-dessous. Je joins également le fichier dans un zip à télécharger disponible ici . Notez que dans mon code, je n'ai utilisé que des TriangleList. Pour vous exercer, essayez de dessiner le même cube mais en utilisant TriangleStrip. Vous devrez ajuster les indices dans l'IndexBuffer en conséquence et vous devrez afficher le cube avec 2 strips. Indice : l'IndexBuffer devrait devenir plus petit.
Commme d'habitude, n'hésitez pas à me contacter si vous avez des questions, commentaires, corrections ou suggestions pour de futures séries de tutoriels. La prochaine fois , nous allons appliquer notre nouveau savoir sur les IndexBuffer pour construire des objets Mesh à partir de rien, ce qui nous offre pas mal de possibilités sympatiques.
using System; using System.Drawing; using System.Windows.Forms; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace Craig.Direct3D { public class Game : System.Windows.Forms.Form { static void Main() { Game app = new Game(); app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } } private Device device; private VertexBuffer vertices; private IndexBuffer indices; protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); // Désactive l'éclairage - nous utiliserons directement // les couleurs stoquées dans les sommets device.RenderState.Lighting = false; // Nous reculerons la caméra plus loin derriere qu'habituellement // Notez le champ de vision plus pointu : Pi/50 au lieu du Pi/4 habituel // C'est pour rendre la déformation "oeil de poisson" moins notable. // En fait on utilise un zoom. // Essayez de changer le champ de vision à Pi/4 et // le z de la caméra à -5 pour constater la déformation device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0, -50), new Vector3(0, 0, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/50.0F, this.Width / this.Height, 1.0F, 100.0F); vertices = CreateVertexBuffer(device); indices = CreateIndexBuffer(device); return true; } protected VertexBuffer CreateVertexBuffer(Device device) { device.VertexFormat = CustomVertex.PositionColored.Format; VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionColored), // Quel type de sommets 8, // Combien device, // Le device 0, // Utilisation par défaut CustomVertex.PositionColored.Format, // Format de sommets Pool.Default); // Pooling par défaut CustomVertex.PositionColored[] verts = (CustomVertex.PositionColored[]) buf.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionColored( -0.5F, 0.5F, -0.5F, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0.5F, -0.5F, Color.Orange.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, -0.5F, -0.5F, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, -0.5F, -0.5F, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, 0.5F, 0.5F, Color.Blue.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0.5F, 0.5F, Color.Violet.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, -0.5F, 0.5F, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, -0.5F, 0.5F, Color.Orange.ToArgb()); buf.Unlock(); return buf; } protected IndexBuffer CreateIndexBuffer(Device device) { IndexBuffer buf = new IndexBuffer( typeof(short), // Que seront mes indices? 36, // Combien il y en aura? device, // Le device 0, // Paramètre avancé Pool.Default // Paramètre avancé ); short[] arr = (short[]) buf.Lock(0, 0); int i = 0; // Face arr[i++] = 0; arr[i++] = 3; arr[i++] = 1; arr[i++] = 0; arr[i++] = 2; arr[i++] = 3; // Dessus arr[i++] = 0; arr[i++] = 1; arr[i++] = 4; arr[i++] = 1; arr[i++] = 5; arr[i++] = 4; // Gauche arr[i++] = 0; arr[i++] = 4; arr[i++] = 2; arr[i++] = 2; arr[i++] = 4; arr[i++] = 6; // Droite arr[i++] = 1; arr[i++] = 7; arr[i++] = 5; arr[i++] = 1; arr[i++] = 3; arr[i++] = 7; // Derrière arr[i++] = 4; arr[i++] = 5; arr[i++] = 6; arr[i++] = 5; arr[i++] = 7; arr[i++] = 6; // Dessous arr[i++] = 2; arr[i++] = 6; arr[i++] = 3; arr[i++] = 3; arr[i++] = 6; arr[i++] = 7; buf.Unlock(); return buf; } protected void SetupMatrices() { float xangle = Environment.TickCount / 500.0F; float yangle = Environment.TickCount / 800.0F; Matrix xrot = Matrix.RotationX(xangle); Matrix yrot = Matrix.RotationY(yangle); device.Transform.World = Matrix.Multiply(xrot, yrot); } protected void Render() { // Efface le back buffer device.Clear(ClearFlags.Target, Color.Black, 1.0F, 0); // Prépare Direct3D à dessiner device.BeginScene(); // Met en place les matrices SetupMatrices(); // Dessine la scène - Instructions 3D ici device.SetStreamSource(0, vertices, 0); device.Indices = indices; device.DrawIndexedPrimitives( PrimitiveType.TriangleList, // Le type de primtives qu'on dessine 0, // Sommet de base 0, // Indice de sommet mini 8, // Nombre de sommets utilisés 0, // Indice de début 12); // Nombre de primitives // Dit à Direct3D qu'on a fini de dessiner device.EndScene(); // Copie le back buffer à l'écran device.Present(); } } }
Ca n'a un peu rien à voir, mais pour la ptite histoire, la normalisation ne sert plus que pour faire du reverse ingeneering sur des systèmes à base de fichiers où il yavait souvent de la redondance d'information. De nos jours, on ne conçoit plus de bases de données avec cette technique, on fait plutôt un MCD Merise.
La redondance d'information est génante que lorsqu'on compte mettre à jour les données (insertion, modification, suppression), car à un instant T, la même information se trouvant à l'endroit A et B peut être différente. Une incohérence source de bugs.