La derniere fois, je vous ai montré comment utiliser un z-buffer pour afficher 2 triangles afin qu'ils se cachent entre eux d'une façon réaliste. Mais qu'est-ce qui est réaliste à propos d'une paire de triangles? Pas grand chose.
Si nous voulons vraiment aller au delà des scènes simplistes qu'on a vu jusqu'à maintenant, nous devons oublier un peu les VertexBuffers. Etant donné que Direct3D fonctionnera toujours de manière interne avec des FVFs et Device.DrawPrimtives, nous avons besoin d'un plus haut niveau d'abstraction si nous voulons être productifs. Cette abstraction est le mesh (ndt : filet en anglais).
A l'intérieur, un mesh est simplement un ensemble prédéfini de sommets et un ensemble de textures et matériaux qui viennent avec. A la base, c'est juste un moyen de nous épargner de s'occuper de chaque triangle et texture, alors que tout ce que nous voulons faire est dessiner un tank ou un ogre. En l'occurence, Direct3D vous permet d'avoir des choses bien plus sophistiquées que cela, mais même juste avec cette définition on peut faire un paquets de trucs.
Le coeur de cette nouvelle couche d'abstraction est la classe Mesh fournie par l'assemblage Microsoft.DirectX.Direct3DX. Notez le X à la fin - l'assemblage Direct3DX est un assemblage utilitaire pour Direct3D et dispose de tonnes de goodies qui fournissent le genre de fonctionnalités avancées qu'on utilise généralement dans de vraies applications.
Nous pourrions créer un Mesh en appelant son constructeur, y fourrer un paquet de sommet dedans et y ajouter des textures et matériaux. Cependant, en général, nous chargerons des objets complexes qui ont été conçus avec des outils de modélisation 3D.
Deux choix s'offrent à vous lorsque vous avez à charger un modèle : créer le votre ou utiliser celui d'un autre. Il s'avère qu'il y en a plein de gratuits sur le net, à des endroits comme 3D Cafe . Ils ont des tanks, des flingues, des arbres, des buissons et des tas d'autres choses que vous pourrez utilisez dans vos expériences.
Si vous ne pouvez pas trouver ce que vous voulez, vous devez créer vos propres modèles. Il y a quelques outils de modélisation, des gratuits comme gMax et des payants comme 3D Studio Max ou LightWave. Ils ont tous leur points forts et points faibles, bien que 3D Studio Max est un standard dans l'industrie.
Peu importe que vous créiez vos modèles ou non, ils doivent être stoqués quelquepart sur le disque. Des outils comme LightWave ont leur propres formats de fichiers pour stoquer les formes et informations de texture, mais la plupart peuvent être convertis dans un format que Direct3D sait utiliser : le format de fichier .x, bien que certains outils comme MeshX sauvegardent en format .x directement.
Un des outils les plus populaires pour convertir les formats 3D est PolyTrans. Si vous vous apprêtez à faire un travail 3D sérieux, vous aurez besoin de l'ajouter à votre boîte à outils. Il gère la conversion à partir de n'importe quel format vers n'importe quel format que vous rencontrerez. Malheureusement, c'est assez cher pour des gens comme moi qui font juste du graphisme pendant leurs temps libres. Le SDK DirectX était livré avec un outil appelé "conv3ds" qui convertit le format 3D Studio en format Direct3D. L'outil est toujours disponible sur le site de DirectX de msdn , donc vous aurez à le télecharger séparément. Les packages "Extras" sont également fournis avec des add-ins pour plein d'outils de modéilsation 3D connus - jetez y un oeil.
Un fichier .x contient tout ce que vous avez besoin de savoir pour afficher un objet. Bien que ça puisse contenir bien plus, dans notre cas on peut le considérer comme contenant des informations de sommets, de textures et de matériaux. Etant donné qu'on a déjà travaillé avec ces choses là, je pense que vous voyez qu'on se dirige dans la bonne direction : nous allons utiliser le Mesh pour gérer la plupart des détails impliqués dans la coordination de ces choses.
Avant de regarder le code permettant de charger et afficher un mesh, nous devons parler des "subsets" (ndt : sous-ensembles). Chaque mesh est divisé en 1 ou plusieurs subsets. Un subset est un ensemble de sommets du mesh qui partagent la même texture et le même matériau. Rappelez vous que la texture et le matériau sont globaux au Device. Vu qu'un objet mesh peut être fait de plusieurs textures et matériaux différents, c'est alors très important de faire attention à laquelle est active, et d'essayer de passer de l'un à l'autre aussi rarement que possible pour des raisons de performances ; d'où les subsets.
Charger un mesh est un peu moins évident que charger une texture. Car nous avons à gérer plusieurs textures et matériaux - probablement un pour chaque subset du mesh - nous devons charger un mesh au travers d'une boucle, stockant l'information à propos des matériaux et textures qui correspondent à chaque subset. Le code pour réaliser cela ressemble à ceci :
// Champs privés pour contenir nos info de mesh // pour les utiliser pendant le rendu private Mesh mesh; private Material[] materials; private Texture[] textures; protected void CreateMesh(string path) { ExtendedMaterial[] exMaterials; mesh = Mesh.FromFile(path, MeshFlags.SystemMemory, device, out exMaterials); if (textures != null) { DisposeTextures(); } textures = new Texture[exMaterials.Length]; materials = new Material[exMaterials.Length]; for (int i = 0; i < exMaterials.Length; ++i) { if (exMaterials[i].TextureFilename != null) { string texturePath = Path.Combine(Path.GetDirectoryName(path), exMaterials[i].TextureFilename); textures[i] = TextureLoader.FromFile(device, texturePath); } materials[i] = exMaterials[i].Material3D; materials[i].Ambient = materials[i].Diffuse; } }
Il y a une jolie montagne de code ici, alors détaillons le ligne par ligne.
ExtendedMaterial[] exMaterials;
Cette ligne met en place un tableau d'objets ExtendedMaterial. ExtendedMaterial est un type que nous allons utiliser pour trouver les textures et matériaux appliqués à chaque subset. Donc si nous chargions un objet tank possédant un canon, une tourelle, un corps et deux chenilles, nous aurions besoin de différents matériaux et textures pour chacune de ces parties. Le tableau d'ExtendedMaterial contiendra ces informations.
mesh = Mesh.FromFile(path, MeshFlags.SystemMemory, device, out exMaterials);
Cette ligne charge le mesh en fait. Nous fournissons un chemin vers un .x file (inserez votre blague préférée de Mulder & Scully ici), des MeshFlags (que je laisse pour une discussion ultérieure), notre Device, et un paramètre en sortie qui est le tableau d'objets ExtendedMaterial qu'on a déclaré plus haut.
Lorsque cette méthode retourne, nous aurons l'objet Mesh lui même, aussi bien qu'une description des matériaux et textures pour chacun des subsets. Mais il reste du travail à faire. Car la méthodeMesh.FromFile ne crée pas en fait les objets Texture et Material dont nous avons besoin, ça dit juste à peu près à quoi ils ressemblent. Alors nous avons besoin du code suivant :
textures = new Texture[exMaterials.Length]; materials = new Material[exMaterials.Length]; for (int i = 0; i < exMaterials.Length; ++i) { if (exMaterials[i].TextureFilename != null) { string texturePath = Path.Combine(Path.GetDirectoryName(path), exMaterials[i].TextureFilename); textures[i] = TextureLoader.FromFile(device, texturePath); } materials[i] = exMaterials[i].Material3D; materials[i].Ambient = materials[i].Diffuse; }
Le code crée un tableau de Textures et un autre de Materiaux et ensuite parcours une boucle remplissant ces tableaux en se basant sur l'information fournie par le tableau d'ExtendedMaterials. Remarquez qu'on a à charger chaque texture explicitement.
if (exMaterials[i].TextureFilename != null) { string texturePath = Path.Combine(Path.GetDirectoryName(path), exMaterials[i].TextureFilename); textures[i] = TextureLoader.FromFile(device, texturePath); }
Ce qui laisse supposer que les textures ne sont en fait pas stoquées dans le fichier .x lui même. Elles sont stoquées dans des fichiers à part. Ici, je considère que les fichiers sont dans le même repertoire que le fichier .x.
Bien que ça n'a rien à voir avec DirectX, laissez moi préciser que vous devriez toujours utiliser Path.Combine afin de s'assurer que les chemins sont construits de la bonne façon. Ca vous épargne également de vous demander si le nom du répertoire finit par un antislash ou non.
L'autre bout de code étrage que vous aurez propablement remarqué est ceci :
materials[i] = exMaterials[i].Material3D; materials[i].Ambient = materials[i].Diffuse;
Ce que nous faisons ici est simplement de copier le matériau du fichier .x dans notre tableau de matériaux, et ensuite de faire en sorte que la couleur ambiante soit la même que la couleur de diffusion. Ce n'est que pour s'assurer que les objets réagissent à la lumière ambiante et de diffusion de la même façon (souvenez vous du tutoriel sur l'éclairage). Vous ne voudriez pas toujours faire cela, mais d'habitude si.
Avec nos objets Mesh, Material et Texture créés sans embuche et bien rangés, nous pouvons nous attarder sur comment afficher le mesh. Ca s'avère être un peu plus facile que le charger :
protected void RenderMesh() { for (int i = 0; i < materials.Length; ++i) { if (textures[i] != null) { device.SetTexture(0, textures[i]); } device.Material = materials[i]; mesh.DrawSubset(i); } }
Par rapport à la façon dont nous avons chargé le mesh, nous savons que pour chaque subset appartenant au mesh, il ya une entrée correspondante dans le tableau de matériaux. Ainsi nous bouclons simplement sur le tableau de matériaux en activant la bonne texture (si il y a) et le matériau, et ensuite nous éxecutons l'instruction clé :
mesh.DrawSubset(i);
Ca ressemble à notre appel à Device.DrawPrimitives de nos exemples précédents, mais en plus puissant. Cette instruction prendra bien soin de sélectionner quels sommets parmis les milliers de sommets potentiels constituant le modèle ont besoin d'être dessinés, et elle les dessinera. En fait nous pourrions certainement écrire notre propre classe Mesh qui ferait la même chose en faisant les appels àDevice.DrawPrimitives nous même. Bien sûr, nous ferions jamais cela, vu qu'on a d'autres chats à fouetter qu'implémenter quelquechose déjà écrit avec des tonnes d'optimisations dedans, mais vous aurez compris : DrawSubset est ce qui nous permet de programmer à un plus haut niveau d'abstraction, en se focalisant sur les objets et sur la scène plutot que sur les triangles et les sommets.
Oh, et exactement comme quand nous travaillions avec des sommets individuels, si nous voulions bouger l'objet, nous le ferions en affectant le transformateur monde sur le device avant d'afficher.
Et voila l'histoire! C'est un gigantesque pas en avant pour nous : en utilisant les meshes et les fichiers .x, nous venons de nous libérer de la géométrie des sommets. A la place nous pouvons manipuler la géometrie des objets, ce qui est bien plus productif.
Maintenant vous vous êtes probablement habitué a me voir parler de ce que nous verrons la prochaine fois. Et j'ai quelques trucs en tête pour le prochain article. (Mise à jour : la prochaine fois nous parlerons de la gestion de la perte du device). Mais il y a plein de détails qui comblent les trous sur nos discutions - en fait j'ai atteind la limite de ce que j'ai vu en Direct3D. Bientôt, j'espère continuer mes recherches sur des hierarchies basées sur l'image (ndt : ?) et l'animation, ce qui nous permet de faire des choses comme faire marcher un monstre d'une façon réaliste. Jusque là, n'hesitez pas à me contacter pour une question, je ferai de mon mieux pour vous donner une réponse.
Comme d'habitude, j'ai inclus un listing complet ci-dessous pour vous permettre de faire des expériences. Avant de le lancer, assuez vous de mettre un fichier .x et toutes les textures correspondantes dans le répertoire où se situe l'executable. Vous pouvez trouver quelques fichiers .x dans le répertoire d'exemples du SDK DirectX.
Update
Si vous utilisez le SDK d'Octobre 2004 ou supérieur, vous devrez chantez l'appel à Commit enUpdate dans SetupLights.
using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using System.IO; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace WinDev.Candera.Direct3D { public class Game : System.Windows.Forms.Form { private Device device; private PresentParameters pres; // Champs privés pour contenir les infos sur le mesh // utilisées pour le rendu private Mesh mesh; private Material[] materials; private Texture[] textures; static void Main() { Game app = new Game(); app.Width = 800; app.Height = 600; app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } app.DisposeGraphics(); } protected bool InitializeGraphics() { pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; pres.EnableAutoDepthStencil = true; pres.AutoDepthStencilFormat = DepthFormat.D16; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); device.RenderState.CullMode = Cull.None; CreateMesh(@"tank.x"); return true; } protected void CreateMesh(string path) { ExtendedMaterial[] exMaterials; mesh = Mesh.FromFile(path, MeshFlags.SystemMemory, device, out exMaterials); if (textures != null) { DisposeTextures(); } textures = new Texture[exMaterials.Length]; materials = new Material[exMaterials.Length]; for (int i = 0; i < exMaterials.Length; ++i) { if (exMaterials[i].TextureFilename != null) { string texturePath = Path.Combine(Path.GetDirectoryName(path), exMaterials[i].TextureFilename); textures[i] = TextureLoader.FromFile(device, texturePath); } materials[i] = exMaterials[i].Material3D; materials[i].Ambient = materials[i].Diffuse; } } protected void DisposeTextures() { if (textures == null) { return; } foreach (Texture t in textures) { if (t != null) { t.Dispose(); } } } protected void SetupMatrices() { float yaw = Environment.TickCount / 500.0F; float pitch = Environment.TickCount / 312.0F; device.Transform.World = Matrix.RotationYawPitchRoll(yaw, pitch, 0); device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0, -6), new Vector3(0, 0, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4.0F, 1.0F, 1.0F, 10.0F); } protected void SetupLights() { device.RenderState.Lighting = true; device.Lights[0].Diffuse = Color.White; device.Lights[0].Specular = Color.White; device.Lights[0].Type = LightType.Directional; device.Lights[0].Direction = new Vector3(-1, -1, 3); device.Lights[0].Commit(); device.Lights[0].Enabled = true; device.RenderState.Ambient = Color.FromArgb(0x00, 0x00, 0x00); } protected void RenderMesh() { for (int i = 0; i < materials.Length; ++i) { if (textures[i] != null) { device.SetTexture(0, textures[i]); } device.Material = materials[i]; mesh.DrawSubset(i); } } protected void Render() { device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.BlueViolet, 1.0F, 0); device.BeginScene(); SetupMatrices(); SetupLights(); RenderMesh(); device.EndScene(); device.Present(); } protected void DisposeGraphics() { DisposeTextures(); device.Dispose(); } } }