16 Fonts

Last time, we talked about how to build a mesh, an important step in building more advanced functionality into your Direct3D application. Along those same lines, I’d like to introduce another topic: how to render and manipulate text.

There are fundamentally two ways to display text in a Direct3D application: in two dimensions and in three. This picture demonstrates both types:

You can see that at the top of the screen, in maroon, the frame rate is displayed in a two-dimensional font. That is, no matter how the camera moves or rotates, the text will always appear in the upper part of the screen, and will always face the viewer. Contrasted with that are the words “Rotating Text”, which appear in blue, slightly tilted. This phrase is a fully 3D object that can be moved, stretched, or manipulated like any other 3D object.

Your choice between 2D and 3D text will be driven by your application – both are appropriate in different scenarios. Obviously, text that needs to appear as an object in the scene needs to be 3D. But for rendering things like a frame rate or perhaps a game score, 2D text can be simpler to work with. In this article, I’ll show you how to work with both types of text.

Let’s start with 2D text. To produce two-dimensional text in a Direct3D application, we rely on the Microsoft.DirectX.Direct3D.Font class. This class is a part of the Microsoft.DirectX.Direct3DX extensions assembly, so don’t forget to include a reference to it in your project.

We need to create an instance of this class in order to work with it. I’ve put the code to do so in the CreateObjects method. Remember that this method gets called whenever the Device is reset (remember Device Reset?). This way our Font object will be recreated in the event of the Device becoming lost. Here’s the code to create the 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); // other code we’ll deal with later }

First, notice that I’ve aliased the Direct3D Font class to D3DFont, and the WinForms Font class to WinFont using the using statements

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

This is to save me from having to use the full namespace to qualify which of the two font classes I need. Instead of saying

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

I can just say

frameRateD3DFont = new D3DFont(device, frameRateWinFont);

As usual, this constructor takes a reference to the Device. The other argument is a reference to a WinForms Font object. This is the object that tells Direct3D what typeface we’d like to use when rendering – how big the text should be, whether it should be italic, etc. etc. Of course, we need to actually create this object before we can pass it in to the Direct3D Font constructor. I do so in my InitializeGraphics method:

private WinFont frameRateWinFont; protected bool InitializeGraphics() { // Code omitted for clarity // Create the Font we'll use to render the frame rate frameRateWinFont = new WinFont(FontFamily.GenericSerif, 20); // Code omitted for clarity }

Why do I create the Windows Forms Font in InitializeGraphics but the Direct3D Font in CreateObjects? Simple: the Windows Forms Font only needs to be created once ever, whereas the Direct3D Font object needs to be recreated every time the Device is reset. We could create the Windows Forms Font object every time, but that would be less efficient.

With the Direct3D Font object created, it’s remarkably easy to use. Here’s the method I use to draw the frame rate in our sample application:

protected void RenderFrameRate() { frameRateD3DFont.DrawText( null, // Advanced parameter frameratemsg, // Text to render ClientRectangle, // Clip text to this rectangle DrawTextFormat.Left | // Align text to the left of the window DrawTextFormat.Top | // and to the top DrawTextFormat.WordBreak, // And break lines if necessary Color.Maroon); // What color to draw the text in }

It contains only a call to Microsoft.DirectX.Direct3D.Font.DrawText. This method takes five parameters. The first of these is a reference to a Sprite object, which is something we haven’t talked about yet. For now, it’s fine to leave this parameter null.

The second parameter is the text we’d like to render. In this case, it’s a variable called frameratemsg. This is actually calculated in a method called CalculateFrameRate, which is called inside our Render method. The method itself looks like this:

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; } }

You can see that it simply keeps an eye on the clock by reading Environment.TickCount, incrementing a counter every time it is called, and capturing the counter every time 1000 ticks (one second) goes by. Then we store the frame rate in the frameratemsg variable. Which brings us back to DrawText.

The third parameter to DrawText is the bounding rectangle inside which we’d like to draw our text. This rectangle is in screen coordinates, and no text will be drawn outside the region you specify. I simply pass in a copy of the Form’s ClientRectangle property, which specifies the whole window as the region into which we’re going to draw text.

This rectangle combines with the next parameter – a set of DrawTextFormat flags – to specify exactly how the text will be displayed. In our example, I specify three flags together:

    • DrawTextFormat.Left: Align the text against the left edge of the bounding rectangle:

    • DrawTextFormat.Top: Align the text against the top edge of the bounding rectangle.

    • DrawTextFormat.WordWrap: If the text extends beyond the right end of the bounding rectangle, wrap it to the next line, breaking at word boundaries.

There are many other flags we could have specified. Here’s a list of a few of the more interesting ones:

    • DrawTextFormat.Bottom and .Right: Align against the bottom and/or right edges.

    • DrawTextFormat.Center: Align text horizontally in the middle of the bounding rectangle.

    • DrawTextFormat.VerticalCenter: Align text vertically in the middle of the bounding rectangle.

The last argument to the call to DrawText is the Color in which we’d like to render the text.

As you can see, drawing two-dimensional text is a fairly simple matter: create a Direct3D Font based on a Windows Forms Font, then use its DrawText method to actually get the pixels on the screen. Now let’s dive into what it takes to render 3D text.

As it turns out, 3D text is not treated specially in Direct3D. Rather, it is simply rendered using a mesh that happens to be in the shape of the letters we want. The good news is, Direct3D will create the mesh for us. From there, we render it just like any other mesh.

The key to creating the mesh is the Mesh class’s static TextFromFont method. We can create a mesh representing any string by calling it with the right parameters. Because the resulting Mesh is a graphical object that will need to be recreated on Device reset, the logical place to create it is in our CreateObjects method. Here’s our expanded version of that method:

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, // The device, of course meshWinFont, // The font we want to render with text, // The text we want 0.01F, // How "lumpy"? 0.25F, // How thick? ref glyphMetrics // Information about the meshes ); meshBounds = ComputeMeshBounds(glyphMetrics); }

Update

If you're using the October 2004 DirectX SDK, you'll need to change ref glyphMetrics to out glyphMetrics.

We’ve already talked about the first line, wherein we create our 2D Font object. The rest of this method deals with setting up the 3D font Mesh.

Mesh.TextFromFont takes six parameters. The first of these (surprise, surprise) is the Device. We pass a Windows Forms Font object that represents the style of text we want to create a mesh for as the second object. This is much the same as what we did when calling the D3DFont constructor in the 2D case. And in the same way, we need to create this WinFont object during InitializeGraphics, like so:

private WinFont meshWinFont; protected bool InitializeGraphics() { // Code omitted for clarity // Create the Font we'll use to render the frame rate frameRateWinFont = new WinFont(FontFamily.GenericSerif, 20); // Create the Font we'll use to render the 3D font meshWinFont = new WinFont(FontFamily.GenericSansSerif, 36); // Code omitted for clarity }

Going back to TextFromFont, the third parameter is the text of the object we want to create. We’ll just use the words “Rotating Text”.

The fourth and fifth parameters control the way Direct3D creates the three-dimensional mesh out the inherently two-dimensional font. The fourth argument essentially controls how “lumpy” the text will be. The lower this number is, the more triangles will be used, and the smoother the font object will appear. Play with this number and you’ll see what I mean. The fifth argument controls how thick the text will be. That is, how far it extends in the z dimension. Make this number big, and the letter “o” will turn into a long, hollow tube. Make this number small, and an “o” will turn into a flat ring.

The last parameter is a reference to an array of GlyphMetricsFloat objects. You must allocate this array before calling TextFromFont, and it should be the same length as the string you pass in. The reason for this is that – when the call returns – the array will contain information about each letter in the string.

Now, there is an overload of TextFromFont that does not require us to pass this array in. However, because I want to rotate the resulting Mesh to more strongly illustrate the 3D nature of the text, this information is very useful to us. The reason is that we’d like to rotate the text around its center. Because the origin of the Mesh is at the rear, lower edge of the beginning of the text, rotating it like we normally do will make it spin around one corner of the text...not the effect we want.

What we need to do is calculate the center of the Mesh. This is the purpose of the call to ComputeMeshBounds at the end of CreateObjects. This is a method that I wrote, and it looks like this:

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); }

What the method does is to loop over each element in the GlyphMetricsFloat array that is passed in, accumulating data as it goes, to compute a length and width for the Mesh. Each GlyphMetricsFloat object contains data about the horizontal distance from the previous letter to this one (CellIncX), and about the height of each letter (BlackBoxY). By simply summing the former and finding the max of the latter, we can easily compute the overall height and width of the text object. Notice that we also keep a running total of the y offset (offsety) by summing the CellIncY values. CellIncY tells us how different the y origin of the previous letter is from this one – imagine if we were rendering something that used a superscript. Direct3Dmight give us dimensions of the superscript letters based on a y origin that was higher than the “normal” letters.

So we’ve created the Mesh object and computed its boundaries. Because it is a Mesh, we already know how to render it, using Mesh.DrawSubset. About the only twist is, because we want to rotate the Mesh around a point other than its origin, we need to set the World transformation appropriately on each frame. Because we computed the bounds of the text when we created it, this is merely a matter of shifting the object left by half its length, down by half its height, and “forward” (towards the camera) by half its thickness. This will shift it so that its center is positioned over the world origin. Rotating the world will then have the effect of spinning the text around its center. Here’s the 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); }

As usual, we calculate some rotations based on Environment.TickCount. However, this time we also calculate a translation Matrix based on the bounds we figured out earlier. Multiplying two matrixes results in a matrix that has the same effect as performing each operation in series, so we simply call Matrix.Multiply to figure out our total World transform. Note that it’s very important to get the order of the matrices right: rotating and then translating is not the same thing at all as translating and then rotating. Rotating first means that when you try to translate things to the left, you’re actually going to be translating them to the rotated left…which isn’t the left of the unrotated text at all. Try switching them and you’ll see what I mean.

So there you have it: 2D and 3D text. As always, a complete program demonstrating these concepts is listed below, or you can download the code here.

We’ve come a long way in these sixteen tutorials, but there’s a lot more to cover. People email me all the time with requests, questions, and suggestions – and I encourage you to do the same. One of the things I get requests for from time to time is a tutorial on how to animate meshes. I think we’ve learned enough now to tackle this topic, so that’s what we’ll talk about next time.

Update: Well, this is where I ran out of steam. The tutorial series ends here. It's fairly unlikely that I will add more. Still, I hope you got some value out of what you read to this point!

The Code

Update

If you're using the October 2004 SDK, you'll need to change the call to Commit in SetupLightswith a call to Update. You'll need to change ref glyphMetrics to out glyphMetrics inCreateObjects. This is due to the changes the DirectX team made to the SDK in the October 2004 release.

using System; using System.Drawing; using System.Windows.Forms; using System.Diagnostics; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; // Alias the font class because we have two of them 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; // Has the device been lost and not reset? private bool deviceLost; // We'll need these to Reset successfully, so hold them here private PresentParameters pres = new PresentParameters(); protected bool InitializeGraphics() { // Set up our presentation parameters as usual pres.Windowed = true ; pres.AutoDepthStencilFormat = DepthFormat.D16; pres.EnableAutoDepthStencil = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); // Hook the DeviceReset event so OnDeviceReset will get called every // time we call device.Reset() device.DeviceReset += new EventHandler( this .OnDeviceReset); // Similarly, OnDeviceLost will get called every time we call // device.Reset(). The difference is that DeviceLost gets called // earlier, giving us a chance to do the cleanup that needs to // occur before we can call Reset() successfully device.DeviceLost += new EventHandler( this .OnDeviceLost); // Create the Font we'll use to render the frame rate frameRateWinFont = new WinFont(FontFamily.GenericSerif, 20); // Create the Font we'll use to render the 3D font meshWinFont = new WinFont(FontFamily.GenericSansSerif, 36); // Do the initial setup of our graphics objects 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) { // We use the same setup code to reset as we do for initial creation SetupDevice(); } protected void OnDeviceLost( object sender, EventArgs e) { } protected void SetupDevice() { // Create the graphical objects 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(); // Set the properties of the material // The object itself will be blue mat.Diffuse = Color.Blue; // We want it to look slightly dull, so maybe a grey // wide highlight mat.Specular = Color.LightGray; mat.SpecularSharpness = 25.0F; device.Material = mat; // Very important - without this there is no specularity 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, // The device, of course meshWinFont, // The font we want to render with text, // The text we want 0.01F, // How "lumpy"? 0.25F, // How thick? ref glyphMetrics // Information about the 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) { // Try to get the device back AttemptRecovery(); } // If we couldn't get the device back, don't try to render if (deviceLost) { return; } CalculateFrameRate(); // Clear the back buffer device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.Black, 1.0F, 0); // Ready Direct3D to begin drawing device.BeginScene(); // Set the Matrices SetupMatrices(); RenderFrameRate(); Render3DFont(); // Indicate to Direct3D that we're done drawing device.EndScene(); try { // Copy the back buffer to the display device.Present(); } catch (DeviceLostException) { // Indicate that the device has been lost deviceLost = true ; } } protected void RenderFrameRate() { frameRateD3DFont.DrawText( null, // Advanced parameter frameratemsg, // Text to render ClientRectangle, // Clip text to this rectangle DrawTextFormat.Left | // Align text to the left of the window DrawTextFormat.Top | // and to the top DrawTextFormat.WordBreak, // And break lines if necessary Color.Maroon); // What color to draw the text in } 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) { // If it's still lost or lost again, just do // nothing } } } } }