16 Polices

La dernière fois, nous avons parlé de comment construire un mesh, une étape importante dans la fabrication d'une application Direct3D plus poussée. Cette fois ci j'aimerais présenter un autre sujet : comment afficher et manipuler du texte.

Il y a fondamentalement deux façons d'afficher du texte dans une application Direct3D : en 2 dimensions et en 3. Cette image montre les 2 types :

Vous pouvez voir en haut de l'écran, en marron, que le frame rate (ndt : nombre d'images par sécondes) est affiché dans une police 2D. C'est à dire, peu importe comment bouge ou tourne la caméra, le texte apparaîtra dans la partie supérieure de l'écran, et restera toujours face au spectateur. A côté de ça il y a la phrase "Rotating Text" qui apparaît en bleu, légèrement inclinée. Cette phrase est un objet complètement 3D qui peut être bougée, étirée, ou manipulée comme n'importe quel autre objet 3D.

Votre choix entre du texte 2D ou 3D dépendra de votre application, les 2 sont appropriés pour différents scénarios. Evidemment, un texte ayant besoin de faire partie de la scène doit être en 3D. Mais pour afficher des choses comme le frame rate ou peut être un score de jeu, il serait plus simple de travailler avec du texte 2D. Je vous montrerai comment travailler avec les 2 types de texte.

Commençons par le texte 2D. Pour produire du texte 2D dans une application Direct3D, on se base sur la classe Microsoft.DirectX.Direct3D.Font. Cette classe appartient à l'assemblage Direct3DX alors n'oubliez pas d'inclure la référence correspondante dans votre projet.

Nous devons instancier cette classe pour travailler avec. J'ai ajouté le code pour faire cela dans la méthode CreateObjects. Souvenez vous que cette méthode est appelée à chaque fois que le Device est réinitialisé. De cette façon notre objet Font sera recréé après la perte du Device. Voici le code pour créer le Font :

using D3DFont = Microsoft.DirectX.Direct3D.Font; using WinFont = System.Drawing.Font; private D3DFont frameRateD3DFont; protected void CreateObjects(Device device) { frameRateD3DFont = new D3DFont(device, frameRateWinFont); // autre code qu'on verra plus tard }

D'abord, notez que j'ai raccourci Direct3D.Font en D3DFont et le Font des WinForms en WinFont avec ces instructions :

using D3DFont = Microsoft.DirectX.Direct3D.Font; using WinFont = System.Drawing.Font;

Ceci m'épargne d'avoir à taper le namespace entier pour définir laquelle des 2 classes Font j'ai besoin. Plutôt que d'écrire

frameRateD3DFont = new Microsoft.DirectX.Direct3D.Font(device, frameRateWinFont);

j'écris juste

frameRateD3DFont = new D3DFont(device, frameRateWinFont);

Comme d'habitude, le constructeur demande une référence au Device. L'autre argument est une référence à un objet Font des WinForms. Ceci est l'objet qui indique à Direct3D le type de police qu'on voudrait utiliser pour le rendu ; la taille du texe, si c'est en italique ou non, etc.. Bien sûr on doit en fait créer cet objet avant de le passer au constructeur du Font Direct3D. Je réalise cela dans ma méthode InitializeGraphics :

private WinFont frameRateWinFont; protected bool InitializeGraphics() { // Code omis pour plus de clarté // Crée le Font qu'on utilisera pour afficher le frame rate frameRateWinFont = new WinFont(FontFamily.GenericSerif, 20); // Code omis pour plus de clarté }

Pourquoi je crée la fonte Windows Forms dans InitializeGraphics et la fonte Direct3D dans CreateObjects ? C'est simple : la fonte Windows Forms n'a besoin d'être crée qu'une fois alors que la fonte Direct3D doit être recrée à chaque réinitialisation du device. On pourrait créer la fonte Windows Forms à chaque fois mais ça serait moins efficace.

Une fois l'objet Font Direct3D créé, c'est remarquablement facile à utiliser. Voici la méthode que j'utilise pour dessiner le frame rate dans notre exemple :

protected void RenderFrameRate() { frameRateD3DFont.DrawText( null, // Paramètre avancé frameratemsg, // Texte à afficher ClientRectangle, // Découpe ce texte dans ce rectangle DrawTextFormat.Left | // Aligne le texte à la gauche de la fenêtre DrawTextFormat.Top | // et à son dessus DrawTextFormat.WordBreak, // Et saute des lignes si necessaire Color.Maroon); // En quelle couleur dessiner le texte }

Elle ne contient qu'un appel à Microsoft.DirectX.Direct3D.Font.DrawText. Cette dernière prend 5 paramètres. Le premier est une référence à un objet Sprite, quelquechose dont on a jamais parlé. Pour le moment, laissons ce paramètre à null.

Le second paramètre est le texte qu'on voudrait afficher. Ici c'est une variable appelée frameratemsg. Cela est en fait calculé dans une méthode appelée CalculateFrameRate, qui est appelée dans notre méthode Render. Elle ressemble à ceci :

private int frames; private string frameratemsg = ""; private int lastTickCount; protected void CalculateFrameRate() { ++frames; int ticks = Environment.TickCount; int elapsed = ticks - lastTickCount; if (elapsed > 1000) { int framerate = frames; frames = 0; frameratemsg = "Frames per second: " + framerate.ToString(); lastTickCount = ticks; } }

Vous pouvez constater que ça garde juste un oeil à l'horloge en lisant Environment.TickCount, incrémentant un compteur à chaque appel et capturant ce compteur toutes les 1000ms. Ensuite nous stoquons le frame rate dans la variable frameratemsg utilisée par DrawText.

Le troisième paramètre à DrawText est le rectangle à l'interieur duquel on voudrait dessiner notre texte. Ce rectangle est en coordonnées écran, et aucun texte ne sortira de la région spécifiée. Je ne fait que passer une copie de la propriété ClientRectangle de la Form, ce qui veut dire qu'on autorise à dessiner potentiellement sur toute la région de la fenêtre.

Ce rectangle va de pair avec le paramètre suivant - une combinaison de flags DrawTextFormat - pour indiquer exactement où le texte sera affiché. Dans notre exemple, j'utilise 3 flags en même temps :

    • DrawTextFormat.Left : Aligne le texte contre le bord gauche du rectangle.

    • DrawTextFormat.Top : Aligne le texte contre le bord supérieur du rectangle.

    • DrawTextFormat.WordWrap : Si le texte s'étend au delà de la limite droite du rectangle, saute une ligne sans casser les mots.

Il ya bien d'autres flags qu'on aurait pu spécifier. Voici une liste des plus intéressants :

    • DrawTextFormat.Bottom et .Right : Aligne le texte contre le bord inférieur et/ou droit.

    • DrawTextFormat.Center : Centre le texte horizontalement dans le rectangle.

    • DrawTextFormat.VerticalCenter : Aligne le texte verticalement dans le rectangle.

Le dernier argument de DrawText est la couleur avec laquelle on voudrait afficher le texte.

Comme vous le voyez, dessiner du texte 2D est assez simple : on crée un Font Direct3D basé sur un Font Windows Forms, ensuite nous utilisons sa méthode DrawText pour enfin obtenir les pixels à l'écran. Maintenant regardons ce qu'il faut faire pour afficher du texte 3D.

En fait, le texte 3D n'est pas specialement traité par Direct3D. A la place, on utilise un mesh qui a la forme des lettres qu'on désire. La bonne nouvelle est que Direct3D va créer le mesh pour nous. A partir de là, on l'affiche comme n'importe quel autre mesh.

La clé pour créer un mesh est la méthode statique TextFromFont de la classe Mesh. On peut créer un mesh représentant n'importe quelle chaîne en l'appellant avec les bons paramètres. Etant donné que le Mesh qui en resulte est un objet graphique qui devra être recréé à la reinitialisation du Device, le bon endroit pour le créer est dans notre méthode CreateObjects. Voici la nouvelle version de cette méthode :

private SizeF meshBounds = new SizeF(); private Mesh fontMesh; protected void CreateObjects(Device device) { frameRateD3DFont = new D3DFont(device, frameRateWinFont); string text = "Rotating Text"; GlyphMetricsFloat[] glyphMetrics = new GlyphMetricsFloat[text.Length]; fontMesh = Mesh.TextFromFont( device, // Le device bien sur meshWinFont, // La fonte avec laquelle on veut faire le rendu text, // Le texte desiré 0.01F, // "Bosselé" comment? 0.25F, // Epais comment? ref glyphMetrics // Information sur les meshes ); meshBounds = ComputeMeshBounds(glyphMetrics); }

Update

Si vous utilisez le SDK d'Octobre 2004, vous aurez à changer ref glyphMetrics en out glyphMetrics.

Nous avons déjà parlé de la première ligne, où nous créons notre objet Font 2D. Le reste de la méthode configure la police 3D du Mesh.

Mesh.TextFromFont prend 6 arguments. Le premier est (surprise, surprise) le Device. Nous passons un objet Font des Windows Forms répresentant le style de texte qu'on veut créer pour l'objet en deuxième argument. Ceci est un peu la même chose que ce qu'on a fait en appelant le constructeur de D3DFont dans le cas 2D. Et de la même façon on doit créer cet objet WinFont dans InitializeGraphics comme ceci :

private WinFont meshWinFont; protected bool InitializeGraphics() { // Code omis pour plus de clarté // Crée le Font qu'on utilisera pour afficher le frame rate frameRateWinFont = new WinFont(FontFamily.GenericSerif, 20); // Crée le Font qu'on utilisera pour afficher le texte 3D meshWinFont = new WinFont(FontFamily.GenericSansSerif, 36); // Code omis pour plus de clarté }

Pour revenir à TextFromFont, le troisième paramètre est le texte de l'objet qu'on veut créer. Nous ne mettrons que les mots "Rotating Text".

Les paramètres 4 et 5 contrôlent la façon dont Direct3D crée le mesh 3D en partant de la police 2D. Le quatrième argument indique essentiellement à quel point le texte sera "bosselé". Plus le nombre sera petit, plus il y aura de triangles utilisés, et plus la police semblera lisse. Bidouillez donc ce nombre et vous comprendrez. Le cinquième argument contrôle l'épaisseur du texte. C'est à dire, jusqu'où ça s'étend en z. Mettez un grand nombre et la lettre "o" ressemblera à un long tube. Mettez un petit nombre et un "o" ressemblera à un anneau plat.

Le dernier paramètre est une réference à un tableau d'objets GlyphMetricsFloat. Vous devrez allouer ce tableau avant d'appeler TextFromFont et il devra être de la même taille que la chaîne que vous donnez. La raison à cela est que, quand la méthode retourne, le tableau sera rempli d'informations sur chaque lettre de la chaîne.

Maintenant, il y a une surcharge de TextFromFont qui ne demande pas de passer ce tableau. Cependant, parce que je voudrais faire pivoter le Mesh retourné afin de mieux illustrer le fait que le texte est en 3D, cette information nous sera très utile. La raison à cela est qu'on voudra faire pivoter le texte autour de son centre. Vu que l'origine du Mesh est à l'arrière, contre le bord inférieur du début du texte, une rotation équivaudrait à un pivot autour d'un coin du texte...c'est pas ce qu'on veut.

On doit calculer le centre du Mesh. Voilà l'objet de l'appel à ComputeMeshBounds à la fin de CreateObjects. C'est une méthode que j'ai écrite, et elle ressemble à ça :

private SizeF ComputeMeshBounds(GlyphMetricsFloat[] gmfs) { float maxx = 0; float maxy = 0; float offsety = 0; foreach (GlyphMetricsFloat gmf in gmfs) { maxx += gmf.CellIncX; float y = offsety + gmf.BlackBoxY; if (y > maxy) { maxy = y; } offsety += gmf.CellIncY; } return new SizeF(maxx, maxy); }

Ce que fait la méthode est de boucler sur chaque élément du tableau de GlyphMetricsFloat fourni, accumulant les données à chaque passage, afin de calculer la longueur et la largeur du Mesh. Chaque objet GlyphMetricsFloat contient des données à propos de la distance qui sépare horizontalement la lettre courante de la précédente (CellIncX) et à propos de la hauteur de chaque lettre (BlackBoxY). En additionnant simplement ces distances et en trouvant le max de ces hauteurs, on peut facilement calculer les dimensions globales de l'objet texte. Notez qu'on garde aussi un total du décalage y (offsety) en additionnant les valeurs CellIncY. CellIncY nous indique la différence en y par rapport à la lettre précédente ; imaginez qu'on utilise quelquechose utilisant des indices supérieurs (comme "x²"). Direct3D nous donnerait les dimensions des lettres en indice en se basant sur une origine en y supérieure aux lettres "normales".

Ainsi nous avons créé l'objet Mesh et calculé ses bornes. Vu que c'est un Mesh, nous savons déjà comment l'afficher, avec Mesh.DrawSubset. Le seul truc tordu est qu'on veut faire tourner le Mesh autour d'un point et non autour de son origine, alors nous devons positionner le transformateur monde à chaque image. Vu qu'on a calculé les dimensions du texte à la création, il n'y a pratiquement plus qu'à décaler l'objet à gauche de moitié et le baisser de la moitié de sa hauteur et l'"avancer" (vers la caméra) de la moitié de son épaisseur. Il sera décalé de telle sorte que son centre sera positionné sur l'origine. Faire pivoter le monde nous laissera l'impression que le texte tourne atour de son centre. Voici le code :

protected void SetupMatrices() { float yaw = Environment.TickCount / 1200.0F; float pitch = Environment.TickCount / 800.0F; Matrix translate = Matrix.Translation(-meshBounds.Width / 2.0F, -meshBounds.Height / 2.0F, 0.125F); Matrix rotate = Matrix.RotationYawPitchRoll(yaw, pitch, 0); device.Transform.World = Matrix.Multiply(translate, rotate); }

Comme d'habitude, on calcule des rotations en se basant sur Environment.TickCount. Cependant cette fois on calcule aussi une matrice de translation basée sur les dimensions trouvées plus tôt. Multiplier 2 matrices produit une matrice qui a le même effet que d'effectuer chaque opération à la suite, alors nous appelons simplement Matrix.Multiply pour affecter le transformateur monde. Remarquez que c'est très important de garder le bon ordre dans la multiplication : pivoter puis translater n'est pas la même chose que translater puis pivoter. Pivoter d'abord signifie que, quand vous essayez de translater des choses vers la gauche, vous allez les translater vers une gauche pivotée... ce qui n'a rien à voir avec la gauche du texte non pivoté. Essayez de les intervertir et vous verrez ce que je veux dire.

Voilà vous les avez, le texte 2D et 3D. Comme toujours, un programme démontrant ces concepts est listé ci dessous, ou vous pouvez telecharger le code ici.

Nous avons fait du chemin pendant ces 16 tutoriaux, mais il y a encore des choses à traiter. Les gens m'e-mailent tout le temps avec des requêtes, des questions, des suggestions, et je vous encourage à faire de même. Une des choses qu'on me demande de temps en temps est un tutoriel sur comment animer des meshes. Je pense qu'on a assez appris maintenant pour s'attaquer à ce sujet, alors c'est ce dont nous parlerons la prochaine fois.

Le Code

Update

Si vous utilisez le SDK d'Octobre 2004, vous aurez besoin de modifier l'appel à Commit en appel àUpdate dans SetupLights. Vous devrez aussi changer ref glyphMetrics en out glyphMetricsdans CreateObjects. Ceci est du aux changements faits au SDK par l'équipe de DirectX pour la release d'Octobre 2004.

ndt : "out" en C# signifie qu'on peut passer une variable par référence sans même l'avoir initialisée, cela laissera penser qu'on a même pas besoin d'allouer le tableau glyphMetrics.. à voir

using System; using System.Drawing; using System.Windows.Forms; using System.Diagnostics; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; // Fait des aliases pour les classes Font car on en a 2 du même nom using D3DFont = Microsoft.DirectX.Direct3D.Font; using WinFont = System.Drawing.Font; namespace Craig.Direct3D { public class Game : System.Windows.Forms.Form { static void Main() { Game app = new Game(); app.Text = "Fonts"; app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } } private Device device; // Le device a t il été perdu et non réinitialisé? private bool deviceLost; // Nous aurons besoin de ceux ci pour réinitialiser correctement, donc laissons les ici private PresentParameters pres = new PresentParameters(); protected bool InitializeGraphics() { // Configure nos paramètres de présentation comme d'habitude pres.Windowed = true ; pres.AutoDepthStencilFormat = DepthFormat.D16; pres.EnableAutoDepthStencil = true; pres.SwapEffect = SwapEffect.Discard; 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 à device.Reset() device.DeviceReset += new EventHandler( this .OnDeviceReset); // De la même manière, OnDeviceLost sera appelée à chaque appel à // device.Reset(). La différence est que DeviceLost est provoqué // au début, nous donnant la chance de faire le nettoyage nécessaire // avant de pouvoir appeler Reset() avec succès device.DeviceLost += new EventHandler( this .OnDeviceLost); // Crée le Font qu'on utilisera pour afficher le frame rate frameRateWinFont = new WinFont(FontFamily.GenericSerif, 20); // Crée le Font qu'on utilisera pour afficher la police 3D meshWinFont = new WinFont(FontFamily.GenericSansSerif, 36); // Effectue la configuration initiale de nos objets graphiques SetupDevice(); return true ; } private int frames; private string frameratemsg = ""; private int lastTickCount; private WinFont frameRateWinFont; private D3DFont frameRateD3DFont; private Mesh fontMesh; private WinFont meshWinFont; private SizeF meshBounds = new SizeF(); protected void CalculateFrameRate() { ++frames; int ticks = Environment.TickCount; int elapsed = ticks - lastTickCount; if (elapsed > 1000) { int framerate = frames; frames = 0; frameratemsg = "Frames per second: " + framerate.ToString(); lastTickCount = ticks; } } protected void OnDeviceReset( object sender, EventArgs e) { // On utilise le même code pour réinitialiser que pour initialiser SetupDevice(); } protected void OnDeviceLost( object sender, EventArgs e) { } protected void SetupDevice() { // Crée les objets graphiques CreateObjects(device); device.RenderState.CullMode = Cull.CounterClockwise; device.RenderState.ZBufferEnable = true; device.Transform.View = Matrix.LookAtLH( new Vector3(0, 0.5F, -10), 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, 100.0F); SetupLights(); SetupMaterials(); } 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(0x40, 0x40, 0x40); } protected void SetupMaterials() { Material mat = new Material(); // Configure les propriétés des matériaux // L'objet lui meme sera bleu mat.Diffuse = Color.Blue; // On désire qu'il soit un peu terne, // alors peut être un peu de gris avec un vaste reflet mat.Specular = Color.LightGray; mat.SpecularSharpness = 25.0F; device.Material = mat; // Très important, sans cela il n'y a pas de spécularité device.RenderState.SpecularEnable = true; } protected void CreateObjects(Device device) { frameRateD3DFont = new D3DFont(device, frameRateWinFont); string text = "Rotating Text"; GlyphMetricsFloat[] glyphMetrics = new GlyphMetricsFloat[text.Length]; fontMesh = Mesh.TextFromFont( device, // Le device bien sur meshWinFont, // La police avec laquelle on veut l'afficher text, // Le texte désiré 0.01F, // "Bosselé" comment? 0.25F, // Epais comment? ref glyphMetrics // Information sur les meshes ); meshBounds = ComputeMeshBounds(glyphMetrics); } private SizeF ComputeMeshBounds(GlyphMetricsFloat[] gmfs) { float maxx = 0; float maxy = 0; float offsety = 0; foreach (GlyphMetricsFloat gmf in gmfs) { maxx += gmf.CellIncX; float y = offsety + gmf.BlackBoxY; if (y > maxy) { maxy = y; } offsety += gmf.CellIncY; } return new SizeF(maxx, maxy); } protected void SetupMatrices() { float yaw = Environment.TickCount / 1200.0F; float pitch = Environment.TickCount / 800.0F; Matrix translate = Matrix.Translation(-meshBounds.Width / 2.0F, -meshBounds.Height / 2.0F, 0.125F); Matrix rotate = Matrix.RotationYawPitchRoll(yaw, pitch, 0); device.Transform.World = Matrix.Multiply(translate, rotate); } protected void Render() { if (deviceLost) { // Essaye de récupérer le device AttemptRecovery(); } // Si on n'a pas pu ravoir le device, n'essaye pas de faire le rendu if (deviceLost) { return; } CalculateFrameRate(); // Efface le back buffer device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.Black, 1.0F, 0); // Prépare Direct3D à dessiner device.BeginScene(); // Positionne les matrices SetupMatrices(); RenderFrameRate(); Render3DFont(); // 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 RenderFrameRate() { frameRateD3DFont.DrawText( null, // Paramètre avancé frameratemsg, // Texte à afficher ClientRectangle, // Fais tenir ce texte dans ce rectangle DrawTextFormat.Left | // Aligne le texte vers la gauche de la fenetre DrawTextFormat.Top | // et en haut DrawTextFormat.WordBreak, // et saute des lignes si nécessaire Color.Maroon); // En quelle couleur dessiner le texte } protected void Render3DFont() { fontMesh.DrawSubset(0); } 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 } } } } }