La dernière fois, on a parlé des IndexBuffers et de comment ils pouvaient afficher des objets d'une façon plus efficace. Ils contribuent également à la compréhension des objets Mesh, qui ont un role majeur dans la génération de la scène. En particulier, vous devez comprendre les IndexBuffers si vous voulez construire vos propres Meshes.
Mais pourquoi je voudrais créer mon propre Mesh? N'est ce pas plus simple de juste charger un fichier .x? N'y a-t-il point des milliers de Meshes déjà disponibles gratuitement, et ne puis-je pas utiliser des convertisseurs pour en exporter à partir de mon outil 3D préféré ?
Bien sûr que c'est plus simple de charger un Mesh existant depuis un fichier. Mais il y a des fois où ce que vous voulez afficher ne peut pas être enregistré de manière statique. Par exemple, si vous voulez faire un plugin de visualisation 3D pour un lecteur multimedia, vous voudriez peut être générer des formes basées sur la musique en train d'être jouée. Ou pour une application scientifique, vous voudriez peut être dessiner une certaine courbe basée sur des données mesurées. Dans ces cas là, c'est en général plus facile de créer un objet Mesh à la volée que de créer un fichier .x correspondant et de le charger.
Une autre questions que vous pourriez poser est "Ok, si les données sont dynamiques, pourquoi je ne les affiche pas moi meme avec DrawIndexedPrimitives plutôt que de s'appuyer sur un Mesh?". Encore une fois vous avez raison : tout ce qu'on peut faire avec les Mesh, on peut le faire soi meme avec des VertexBuffers et IndexBuffers directement. En fait, c'est vrai pour toutes les fonctionnalités de l'assemblage Microsoft.DirectX.Direct3DX (D3DX), elles sont toutes basées sur les fonctionnalités de Microsoft.DirectX.Direct3D, il n'ajoute rien de "magique". Bien sur, ça nous fournit un paquet de code que nous aurions du écrire nous même sinon, ce qui est une Bonne Chose.
Donc, maintenant qu'on est persuadés que ça pourrait être intéressant de générer un Mesh par programmation, regardons ce que ça implique.
La première chose à faire est de créer un objet Mesh. Comme vous pouvez vous y attendre, on le fait en appelant le constructeur de Mesh, comme ceci :
// Crée un mesh de cube avec : mesh = new Mesh( 12, // 12 faces 8, // 8 sommets 0, // pas de flags VertexFormats.Position, // informations de position seulement device);
Les paramètres ici sont assez explicites : le nombre de faces (ndt : le nombre de triangles, pas les faces du cube), le nombre de sommets constituants ces faces, des flags qu'on expliquera plus tard, un format de sommet et bien sur le device. Ici, nous mettons que des données de position pour rester simple, mais on aurait pu y mettre des vecteurs normaux, des textures, des matériaux, où autres attributs de notre forme.
Ensuite nous allons avoir besoin d'un IndexBuffer et d'un VertexBuffer pour décrire la forme de notre Mesh. Nous allons reprendre notre exemple de cube de la dernière fois ce qui signifie qu'on aura 8 sommets (les coins) et 36 entrées dans notre index buffer (6 faces, 2 triangles pour chacune, 3 sommets par triangle). On a déjà vu comment créer un cube indexé, donc je ne détaillerai pas trop ce code là, de toute manière il y a le listing complet en bas de l'article.
Après avoir créé le VertexBuffer et l'IndexBuffer, la prochaine chose à faire est de les associer au Mesh qui les gèrera. Le code ressemble à ça :
mesh.SetVertexBufferData(vertices, LockFlags.None); mesh.SetIndexBufferData(indices, LockFlags.None);
Encore, c'est un code assez évident : on passe juste le bon objet. Le deuxième argument a à voir avec la gestion de la mémoire et ne s'applique pas à nous pour le moment.
Très bien. Et en fait notre objet Mesh est maintenant utilisable. Mais il y a encore une chose à faire qui est aussi facile que bénéfique : on peut optimiser le Mesh. Il s'avère que c'est plus efficace pour Direct3D d'afficher notre objet Mesh si les sommets et indices sont bien ordonnés.
Permettez moi d'expliquer. Chaque sommet de notre cube est utilisé pour afficher au moins 3 des 6 faces. Si un sommet est utilisé dans plusieurs faces comme cela, ça serait pas bête de faire les affichages de ces faces les plus proches possibles, car on pourrait profiter du fait que les données de ce sommet se retrouveraient dans la mémoire cache. Si nous vagabondions aléatoirement autour de l'objet affichant une face ici et une face là bas, les sommets non utilisés depuis un moment pourraient être virés de la mémoire cache et auraient besoin d'être re-récupérés. Ainsi certains ordres d'affichage sont plus rapides que d'autres.
La bonne nouvelle est que vous n'avez pas à faire ce calcul potentiellement compliqué vous même. La classe Mesh a des fonctionnalités intégrées qui le feront à notre place. Voici à quoi ressemble le code d'optimisation :
mesh.OptimizeInPlace(MeshFlags.OptimizeVertexCache, adjacency);
Il y a une autre variante à cette méthode - Mesh.Optimize - qui retourne un nouveau Mesh. Ici nous n'optimisons que celui qu'on a en place.
Les 2 arguments de cette méthode indiquent combien d'optimisation on voudrait (un sujet avancé dont je ne voudrais pas parler maintenant ; MeshFlags.OptimizeVertexCache est la valeur recommandée par la doc), et quelquechose appelée l'information d'adjacence (ndt : je néologise un peu.. ça a un rapport avec l'adjectif "adjacent".. on pourrait peut être traduire ça par "consécutivité", je sais pas). L'information d'adjacence indique juste au Mesh quelles faces partagent des arrêtes avec les autres. C'est sous forme de tableau d'entiers, groupés par 3. Chaque groupe de trois représente un triangle du Mesh et donne le numéro des trois triangles avec qui ce triangle partage des arrêtes, avec lesquels ce triangle est adjacent, en d'autre termes. Le numéro -1 indique une arrête partagée avec aucun autre triangle.
Si ça a l'air compliqué de calculer l'adjacence, ne vous inqiuetez pas, ce n'est pas le cas. Il ya une méthode utilitaire pratique - Mesh.GenerateAdjacency - qui le fait pour vous. Vous l'appelez comme ceci :
int[] adjacency = new int[mesh.NumberFaces * 3]; mesh.GenerateAdjacency(0.01F, adjacency);
Le tableau qu'on utilise doit être suffisament grand pour contenir 3 entiers par face. Heureusement, la propriété Mesh.NumberFaces rend cela facile a calculer. L'appel à GenerateAdjacency prend le tableau et le remplit avec les bonnes informations d'adjacence.
Remarquez que cette méthode prend un paramètre additionnel. Ceci est l'epsilon pour l'information d'adjacence, il est là car les nombres flottants sont connus pour être imprécis quand il s'agit de petites différences. Ainsi plutôt que de tester une égalité exacte pour la position de 2 sommets, GenerateAdjacency testera que la différence est comprise dans l'epsilon fourni (ndt : c'est comme ça qu'on fait pour tester l'égalité de 2 flottants même en programmation classique). Si c'est le cas, les 2 sommets sont considérés comme étant les mêmes. Ceci est pratique lorsque vous calculez des positions basées sur des formules où des données externes, lorsque 2 sommets se retrouvent à des postions très très légèrement differentes à cause d'une erreur d'arrondi, ils sont quand même considérés comme étant à la même place.
Ici j'ai choisi 0.01F comme epsilon étant donné que j'ai mis en place mon cube à la main et je sais que je n'ai pas 2 sommets qui sont au même endroit. Vous devriez choisir une valeur d'epsilon par rapport à ce que vous savez de vos données.
A ce point, on a à peu près fini. Nous avons généré et optimisé notre Mesh. La seule chose qui nous reste à faire est de trouver combien il a de subsets. Souvenez vous, les subsets sont des parties d'un Mesh qui sont toutes dessinées ensemble car elles ont de l'information en commun, comme une texture ou un matériau. Obtenir le nombre de subsets de notre mesh est assez facile :
numSubSets = mesh.GetAttributeTable().Length;
Nous utiliserons ce nombre dans notre méthode Render lorsqu'on boucle sur chaque subset du mesh.
Le code pour afficher le mesh qu'on a créé est essentiellement le même que le code pour afficher un Mesh chargé depuis un fichier. C'est à dire, on utilise Mesh.DrawSubset pour obtenir les triangles à l'écran. Ce que j'ai rajouté de différent est une instruction pour dessiner aussi une version fil de fer du cube, afin qu'on puisse voir les triangles qui le constituent. Il n'y a qu'a afficher le cube deux fois, une fois avec :
device.RenderState.FillMode = FillMode.WireFrame;
et une autre avec
device.RenderState.FillMode = FillMode.Solid;
ce qui est le paramètre normal par défaut. Par ailleurs, pour afficher la version solide du cube, je lui ai donné un z-bias avec cette instruction :
device.RenderState.DepthBias = 0.1F;
Un z-bias est juste un petit nombre ajouté au z de chaque pixel de l'espace vue. Souvenez vous, dans un système de coordonnées main gauche, une plus grande valeur de z signifie "loin du spectateur". C'est nécessaire dans notre cas car les versions fil de fer et solide du cube sont désinnées au même endroit. Vu qu'on a le z-buffering d'activé et, étant donné que les calculs de virgule flottante peut entrainer des variations sur des nombres supposés être les mêmes, sans un z-bias, des parties du cube fil de fer disparaîtraient "derrière" le cube soldie. Enlevez l'instrucion du z-bias et vous verrez ce que je veux dire.
Comme vous pouvez le voir, créer vos propres objets Mesh dynamiquement n'est pas beaucoup plus dur que de les charger depuis le disque. En fait, le plus gros travail est dans le renseignement des données des sommets, quelque chose que vous auriez du faire de toute façon sans utiliser de Mesh. Comme d'habitude, j'ai inclus un programme complet à la fin. Le même code (ndt : sans traduction des commentaires) est téléchargeable ici.
La prochaine fois, nous verrons comment travailler avec du texte, aussi bien avec des polices 2D que 3D.
using System; using System.Drawing; using System.Windows.Forms; using System.Diagnostics; 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.Text = "Creating a Mesh"; app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } } private Device device; private Mesh mesh; private int numSubSets; // Le device a t il été perdu et non réinitialisé? private bool deviceLost; // Nous en aurions besoin pour réinitialiser comme il faut, donc laissons les ici private PresentParameters pres = new PresentParameters(); protected bool InitializeGraphics() { // Configurons nos paramètres de présentation comme d'habitude pres.Windowed = true ; pres.SwapEffect = SwapEffect.Discard; pres.AutoDepthStencilFormat = DepthFormat.D16; pres.EnableAutoDepthStencil = true; device = new Device(0, DeviceType.Hardware, this , CreateFlags.SoftwareVertexProcessing, pres); // Attache la méthode OnDeviceReset à l'évenement DeviceReset // afin qu'elle soit appelée à chaque appel de device.Reset() device.DeviceReset += new EventHandler( this .OnDeviceReset); // De la même façon, OnDeviceLost sera appelée à chaque appel // à device.Reset(). La différence est que DeviceLost est appelée // au début, nous donnant la chance de faire le nettoyage nécessaire // avant qu'on puisse appeler Reset() avec succès device.DeviceLost += new EventHandler( this .OnDeviceLost); // Met en place nos objets graphiques SetupDevice(); return true ; } protected void OnDeviceReset( object sender, EventArgs e) { // On utilise le même code pour initialiser que pour réinitialiser SetupDevice(); } protected void OnDeviceLost( object sender, EventArgs e) { } protected void SetupDevice() { SetupLights(); device.RenderState.ZBufferEnable = true; // Et crée les objets graphiques CreateObjects(device); } protected void SetupLights() { device.RenderState.Lighting = true; device.RenderState.Ambient = Color.White; } protected void SetupMaterials(Color color) { Material mat = new Material(); // Vu qu'on a pas positionné de vecteurs normaux pour l'objet // on se contentera de la lumière ambiante mat.Ambient = color; device.Material = mat; } protected void CreateObjects(Device device) { // Crée un nouveau mesh de cube avec : mesh = new Mesh( 12, // 12 faces 8, // 8 sommets 0, // pas de flags VertexFormats.Position, // information de position seulement device); // Met en place les 8 coins du cube float front = -1; float back = 1; float left = -1; float right = 1; float top = 1; float bottom = -1; CustomVertex.PositionOnly[] vertices = new CustomVertex.PositionOnly[] { new CustomVertex.PositionOnly(left , bottom, front), // 0 new CustomVertex.PositionOnly(right, bottom, front), // 1 new CustomVertex.PositionOnly(left , top , front), // 2 new CustomVertex.PositionOnly(right, top , front), // 3 new CustomVertex.PositionOnly(left , bottom, back ), // 4 new CustomVertex.PositionOnly(right, bottom, back ), // 5 new CustomVertex.PositionOnly(left , top , back ), // 6 new CustomVertex.PositionOnly(right, top , back ) // 7 }; short leftbottomfront = 0; short rightbottomfront = 1; short lefttopfront = 2; short righttopfront = 3; short leftbottomback = 4; short rightbottomback = 5; short lefttopback = 6; short righttopback = 7; // Met en place les informations d'indice pour les 12 faces short[] indices = new short[] { // Faces gauches lefttopfront, lefttopback, leftbottomback, // 0 leftbottomback, leftbottomfront, lefttopfront, // 1 // Faces avant lefttopfront, leftbottomfront, rightbottomfront, // 2 rightbottomfront, righttopfront, lefttopfront, // 3 // Faces droites righttopback, righttopfront, rightbottomfront, // 4 rightbottomfront, rightbottomback, righttopback, // 5 // Faces arrières leftbottomback, lefttopback, righttopback, // 6 righttopback, rightbottomback, leftbottomback, // 7 // Faces de dessus righttopfront, righttopback, lefttopback, // 8 lefttopback, lefttopfront, righttopfront, // 9 // Faces de dessous leftbottomfront, leftbottomback, rightbottomback, // 10 rightbottomback, rightbottomfront, leftbottomfront // 11 }; mesh.SetVertexBufferData(vertices, LockFlags.None); mesh.SetIndexBufferData(indices, LockFlags.None); int[] adjacency = new int[mesh.NumberFaces * 10]; mesh.GenerateAdjacency(0.01F, adjacency); mesh.OptimizeInPlace(MeshFlags.OptimizeVertexCache, adjacency); numSubSets = mesh.GetAttributeTable().Length; } protected void SetupMatrices() { float angle = Environment.TickCount / 500.0F; device.Transform.World = Matrix.RotationYawPitchRoll(angle, angle / 3.0F, 0); device.Transform.View = Matrix.LookAtLH( new Vector3(0, 0.5F, -1000), new Vector3(0, 0.5F, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH(( float )Math.PI/400.0F, 1.0F, 900.0F, 1100.0F); } protected void Render() { if (deviceLost) { // Essaye de récupérer le device AttemptRecovery(); } // Si nous avons pas pu ravoir le device, n'essaye pas de dessiner if (deviceLost) { return ; } // Efface le back buffer device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.Black, 1.0F, 0); // Prépare Direct3D à dessiner device.BeginScene(); // Met en place les matrices SetupMatrices(); // Dessine le cube en fil de fer et en blanc device.RenderState.FillMode = FillMode.WireFrame; SetupMaterials(Color.White); // Dessiner le cube en fil de fer for (int i = 0; i < numSubSets; ++i) { mesh.DrawSubset(i); } // Dessine le cube en bleu device.RenderState.FillMode = FillMode.Solid; SetupMaterials(Color.Blue); // Vu que le cube et le cube fil de fer sont exactement à // la meme profondeur en z, c'est pas sur qu'un pourra être // affiché - alors nous trichons en ajoutant un peu de profondeur z // à la valeur z du cube assurant que le fil de fer s'affichera // toujours par dessus le cube plein device.RenderState.DepthBias = 0.1F; // Dessine le cube for (int i = 0; i < numSubSets; ++i) { mesh.DrawSubset(i); } device.RenderState.DepthBias = 0F; // Indique à Direct3D qu'on a fini de dessiner device.EndScene(); try { // Copie le back buffer à l'écran device.Present(); } catch (DeviceLostException) { // Indique que le device a été perdu deviceLost = true ; } } protected void AttemptRecovery() { int res; device.CheckCooperativeLevel(out res); ResultCode rc = (ResultCode) res; if (rc == ResultCode.DeviceLost) { } else if (rc == ResultCode.DeviceNotReset) { try { device.Reset(pres); deviceLost = false ; } catch (DeviceLostException) { // Si il est toujours perdu ou a été re-perdu // ne fais rien } } } } }