07 Rendu en Trois Dimensions

La dernière fois nous avions parlé des systèmes de coordonnées et des transformateurs entre systèmes de coordonnées. Avec toutes ces notions fermement imprégnées dans notre cerveau, nous pouvons enfin commencer à afficher des objets en 3D.

La 1ère chose que nous avons besoin de faire est de modifier notre façon de definir notre objet. Vu qu'on souhaite déléguer à Direct3D la lourde tâche de rendre les choses en 3D, nous allons arrêter de donner nos sommets en coordonnées écran et commencer à les spécifier dans un espace objet tri-dimentionnel. Voici notre nouvelle version de CreateVertexBuffer

protected VertexBuffer CreateVertexBuffer(Device device) { device.VertexFormat = CustomVertex.PositionColored.Format; VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionColored), // Le type des sommets 3, // Combien de sommets device, // Le device 0, // Utilisation par defaut CustomVertex.PositionColored.Format, // Format des sommets Pool.Default); // Pooling par default CustomVertex.PositionColored[] verts = (CustomVertex.PositionColored[]) buf.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionColored( 0, 1, 0, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, 0, 0, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0, 0, Color.Blue.ToArgb()); buf.Unlock(); return buf; }

Ca n'a pas beaucoup changé par rapport à l'ancienne version. Il ya quelques différences cependant. Premièrement, nous n'utilisons plus des sommets TransformedColored, maintenant nous utilisons des PositionColored. La différence est que les "TransformedColored" sont exprimés en coordonnées écran alors que les "PositionColored" sont exprimés en espace objet. Remarquez que les valeurs x y z qu'on a donné pour les sommets du triangle sont 0, 1 et 0.5 ; ce sont des nombres pratiques mais evidemment n'ont aucun rapport direct avec les pixels!

Il y a une autre différence subtile. Cette fois nous avons utilisé une surcharge de Lock qui retourne un tableau de sommets plutot qu'un GraphicsStream. Je trouve cette façon de faire plus commode que de s'embêter avec GraphicsStream, j'ai donc pensé que je devais vous la montrer.

OK, nous avons défini les sommets du triangle dans l'espace objet, mais nous avons besoin de l'obtenir sur l'écran. Bein ca ne prendra que quelques lignes de code supplémentaires - et nous l'animerons pendant que nous y sommes afin d'avoir un triangle qui pivote doucement autour de son axe vertical.

Souvenez vous de la méthode Render. La dernière fois, elle avait cette tête :

protected void Render() { device.Clear(ClearFlags.Target, Color.Black, 1.0F, 0); device.BeginScene(); device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); device.EndScene(); device.Present(); }

Nous avons besoin de la mettre un peu à jour. Allons faire quelques changements mineurs :

protected void Render() { device.Clear(ClearFlags.Target, Color.Bisque, 1.0F, 0); device.BeginScene(); SetupMatrices(); device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); device.EndScene(); device.Present(); }

Remarquez que j'ai effacé le fond dans une autre couleur que le noir. Vous saisirez pourquoi en arrivant à la fin de cet article. L'autre modification est l'appel à SetupMatrices. SetupMatrices ressemble à ceci :

protected void SetupMatrices() { float angle = Environment.TickCount / 500.0F; device.Transform.World = Matrix.RotationY(angle); device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0.5F, -3), new Vector3(0, 0.5F, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4.0F, 1.0F, 1.0F, 3.25F); }

Premièrement nous calculons l'angle :

float angle = Environment.TickCount / 500.0F;

Environment.TickCount est une fonction pratique qui s'incrémente toutes les millisecondes (ndt : en fait la précision de "TickCount" varie selon les systèmes de 10ms à 55ms, dixit la doc microsoft. Mais ce n'est pas important ici). En divisant par 500, nous définissons un angle qui change de 2 radians par seconde, ou grossièrement de un tour toutes les 3 secondes, une vitesse correcte. Nous utilisons cet angle dans la ligne immediatement après :

device.Transform.World = Matrix.RotationY(angle);

OK, souvenez vous que le transformateur monde est le transformateur qui permet à nos objets de se localiser les uns par rapport aux autres. En utilisant la fonction statique Matrix.RotationY, nous avons défini un transformateur qui prend l'objet et le fait pivoter autour de l'axe y pour l'angle que nous donnons. Vu que cet angle change au fil du temps, notre objet tournera à chaque image.

Les lignes suivantes configurent les matrices de vue et de projection :

device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0.5F, -3), new Vector3(0, 0.5F, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4.0F, 1.0F, 1.0F, 3.25F);

Souvenez vous de LookAtLH et PerspectiveFovLH dont nous avons déjà parlé. Nous avons fourni des valeurs relativement standard : la caméra est à 3 unités derriere l'écran (enfin "derriere" signifie entre vous et l'écran), elle regarde plus ou moins l'origine, et le "haut" de la caméra suit l'axe y. La matrice de projection est standard, les seules choses que vous auriez à changer normalement sont les plans limite dont nous avons parlé dans l'article précédent.

Techniquement, nous avons pas vraiment besoin de configurer les matrices de vue et de projection à chaque fois. Elles ne changent pas, après tout. Mais il est important de noter que, comme le transformateur monde, ce sont des paramètres globaux à l'objet Device. C'est à dire, si vous n'avez pas la main sur tout le code et que quelqu'un modifie ces paramètres, ils resterons modifiés lorsque vous irez utiliser le Device. Cela aurait pour conséquence de faire s'envoler vous objets dans des endroits bizarres et non voulus. Ainsi je les configure à chaque fois, pour être sûr.

Et voila! Si vous essayez ce code (cf plus bas), vous obtiendrez le triangle pivotant. Et vous remarquerez deux choses bizarres : 1) il est tout noir, et pas rouge vert et bleu, et 2) il disparait périodiquement. Et bien nous avons déja parlé du point 2. Rappelez vous de notre discussion a propos du rendu avec VertexBuffer où un procédé appelé Culling est mis en place et où par défaut Direct3D ne s'ennuie pas à afficher les triangles de dos en partant du principe qu'ils font parti d'un objet solide et qu'ils seraient cachés par le reste de l'objet. Biensûr, ce principe n'est pas valable ici. Ajoutons cette ligne-ci :

device.RenderState.CullMode = Cull.None;

quelquepart afin de toujours voir le triangle.

La réponse à l'autre question (pourquoi le triangle n'est pas coloré comme avant) est en fait simplement du à l'absence de lumière. Sans éclairage, tout est noir. C'est pourquoi ce n'est pas une mauvaise idée de mettre autre chose que du noir en fond, car si vous faites une erreur dans la gestion de vos lumières, vous pouvez au moins dire que vous avez affiché quelquechose. Nous verrons comment configurer l'éclairage la prochaine fois.

Le Code

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(); } //app.DisposeGraphics(); } private Device device; private VertexBuffer vertices; protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); vertices = CreateVertexBuffer(device); return true; } protected VertexBuffer CreateVertexBuffer(Device device) { device.VertexFormat = CustomVertex.PositionColored.Format; VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionColored), // Quel type de sommets 3, // Combien device, // Le device 0, // Utilisation par défaut CustomVertex.PositionColored.Format, // Format des 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, 1, 0, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, 0, 0, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0, 0, Color.Blue.ToArgb()); buf.Unlock(); return buf; } protected void SetupMatrices() { float angle = Environment.TickCount / 500.0F; device.Transform.World = Matrix.RotationY(angle); device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0.5F, -3), new Vector3(0, 0.5F, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4.0F, 1.0F, 1.0F, 5.0F); } protected void Render() { // Efface le back buffer device.Clear(ClearFlags.Target, Color.Bisque, 1.0F, 0); // Prépare Direct3D à dessiner device.BeginScene(); // Configure les Matrices SetupMatrices(); // Dessine la scène device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); // Informe Direct3D qu'on a fini de dessiner device.EndScene(); // Copie le back buffer à l'écran device.Present(); } } }