Howdy. Today, I’m going to discuss rendering UI text using the SlimDX SpriteTextRenderer library. This is a very nifty and light-weight extension library for SlimDX, hosted on CodePlex. In older versions of DirectX, it used to be possible to easily render sprites and text using the ID3DXSprite and ID3DXFont interfaces, but those have been removed in newer versions of DirectX. I’ve experimented with some other approaches, such as using Direct2D and DirectWrite or the DirectX Toolkit, but wasn’t happy with the results. For whatever reason, Direct2D doesn’t interop well with DirectX 11, unless you create a shared DirectX 10 device and jump through a bunch of hoops, and even then it is kind of a PITA. Likewise, I have yet to find C# bindings for the DirectX Toolkit, so that’s kind of a non-starter for me; I’d either have to rewrite the pieces that I want to use with SlimDX, or figure out the marshaling to use the C++ dlls. So for that reason, the SpriteTextRenderer library seems to be my best option at the moment, and it turned out to be relatively simple to integrate into my application framework.
If you’ve used either the old DirectX 9 interfaces or XNA, then it’ll be pretty intuitive how to use SpriteTextRenderer. The SpriteRenderer class has some useful methods to draw 2D sprites, which I haven’t explored much yet, since I have already added code to draw scree-space quads. The TextBlockRenderer class provides some simple and handy methods to draw text up on the screen. Internally, it uses DirectWrite to generate sprite font textures at runtime, so you can use any installed system fonts, and specify the weight, style, and point size easily, without worrying about the nitty gritty details of creating the font.
One limitation of the TextBlockRenderer class is that you can only use an instance of it to render text with a single font. Thus, if you want to use different font sizes or styles, you need to create different instances for each font that you want to use. Because of this, I’ve written a simple manager class, which I’m calling FontCache, which will provide a central point to store all the fonts that are used, as well as a default font if you just want to throw some text up onto the screen.
The new code for rendering text has been added to my pathfinding demo, available at my GitHub repository, https://github.com/ericrrichards/dx11.git.
Adding SpriteTextRenderer Support to the Engine
The first step in adding text rendering is to add the dll for the SpriteTextRenderer library to the Core project. I downloaded the compiled library, without source, from http://sdxspritetext.codeplex.com/downloads/get/778054. Then, I copied this dll into my project directory, and added the reference to it it Visual Studio.
Next, I added some new members to the D3DApp application framework class, to contain the SpriteTextRenderer specific stuff.
// D3DApp.cs
protected SpriteRenderer Sprite;
protected FontCache FontCache;
(FontCache is my custom font-managing class, which we’ll get to shortly).
Initializing these new members is dead simple. We just need to add the following lines to our InitDirect3D() function:
InitDirect3D() {
// previous initialization stuff
Sprite = new SpriteRenderer(Device);
FontCache = new FontCache(Sprite);
return true;
}
And then we also need to add a couple lines to our Dispose() method, to clean up these new objects, like so:
Util.ReleaseCom(ref Sprite);
Util.ReleaseCom(ref FontCache);
Dead simple so far. Unfortunately, we will need to make some tweaks to the ProgressUpdate class, which draws a loading screen for us using Direct2D. If you recall, we created a DirectWrite Factory instance to allow us to create the TextFormat object that we use to render the text above the loading bar. We need to modify this call to create this factory as FactoryType.Isolated, since SpriteTextRenderer creates its own shared factory, and DirectWrite complains if we try to create more than one of these. So the constructor for ProgressUpdate now looks like (changed lines in bold):
public ProgressUpdate(WindowRenderTarget rt1) {
_rt = rt1;
_brush = new SolidColorBrush(_rt, Color.Green);
_clearColor = new SolidColorBrush(_rt, Color.Black);
_borderBounds = new Rectangle(18, rt1.PixelSize.Height/2 - 2, rt1.PixelSize.Width - 36, 24);
_barBounds = new Rectangle(20, rt1.PixelSize.Height/2, rt1.PixelSize.Width-40, 20);
_factory = new Factory(FactoryType.Isolated);
_txtFormat = new TextFormat(_factory, "Arial", FontWeight.Normal, FontStyle.Normal, FontStretch.Normal, 18, "en-us" ) {
TextAlignment = TextAlignment.Center
};
_textRect = new Rectangle(100, rt1.PixelSize.Height / 2-25, _rt.PixelSize.Width-200, 20);
}
One last tweak is that we need to flush the SpriteRenderer at the end of a frame to submit any sprites or text that we render to the graphics device. To make this easier and prevent myself from forgetting to do so in derived application classes, I’ve added a new helper function to D3DApp, named EndFrame(), which calls the Flush() method of the sprite renderer and also Presents the SwapChain. A call to this method should be the final call of any DrawScene() methods in derived classes, and will replace the previous Present calls in the demo applications.
protected virtual void EndFrame() {
Sprite.Flush();
SwapChain.Present(0, PresentFlags.None);
}
FontCache Class
The FontCache class provides us a central repository to track all of the fonts that we have created for our application, and some utility functions to allow us to easily draw text. This class thus contains a dictionary to contain the fonts that we have created, a reference to the SpriteRenderer that is used by these fonts to draw themselves to the screen, as well as a default font, for convenience’s sake. Since the font objects we create contain DirectX unmanaged resources, we’ll subclass our DisposableClass base class so that we can clean these resources up easily.
public class FontCache : DisposableClass {
private bool _disposed;
private readonly Dictionary<string, TextBlockRenderer> _fonts = new Dictionary<string, TextBlockRenderer>();
private readonly SpriteRenderer _sprite;
private TextBlockRenderer _default;
public FontCache(SpriteRenderer sprite) {
_sprite = sprite;
_default = new TextBlockRenderer(sprite, "Arial", FontWeight.Normal, FontStyle.Normal, FontStretch.Normal, 12);
}
}
Our overridden Dispose method gets rid of our default font object, and loops through the cached fonts stored in our dictionary, destroying each in turn.
protected override void Dispose(bool disposing) {
if (!_disposed) {
if (disposing) {
Util.ReleaseCom(ref _default);
foreach (var textBlockRenderer in _fonts) {
var f = textBlockRenderer.Value;
Util.ReleaseCom(ref f);
}
_fonts.Clear();
}
_disposed = true;
}
base.Dispose(disposing);
}
Creating a new Font
To create a new font, we use the RegisterFont() method. We provide a name to reference this new font object with, and then parameters controlling the font’s properties. Then we simply pass these properties to the SpriteTextRenderer TextBlockRenderer constructor, and store the created font object in our cache. I’ve provided defaults for most of the font properties, so that it is very easy to create fonts of different sizes.
public bool RegisterFont(string name, float fontSize, string fontFace = "Arial", FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal, FontStretch fontStretch = FontStretch.Normal) {
if (_fonts.ContainsKey(name)) {
Console.WriteLine("Duplicate font name: " + name);
return false;
}
try {
_fonts[name] = new TextBlockRenderer(_sprite, fontFace, fontWeight, fontStyle, fontStretch, fontSize);
} catch (Exception ex) {
Console.WriteLine(ex.Message);
return false;
}
return true;
}
Usage of this method would look like the following (from PathfindingDemo.Init()):
FontCache.RegisterFont("bold", 16, "Courier New", FontWeight.Bold);
This creates a new font, named “bold”, with a size of 16, Courier New typeface, and in bold.
Drawing Text
I’ve provided four different methods to draw text using the FontCache class. The first two draw a single string of text on one line, with the upper-left of the text starting at the position provided, using the color provided. One of these methods allows you to pass in the name of a font previously created using a call to RegisterFont, while the other uses the default font. The second two methods allow for writing out multiple lines of text, one after the other, with appropriate spacing between the lines. Again, there are two variants, one using a named font and one using the default font.
public StringMetrics DrawString(string fontName, string text, Vector2 location, Color4 color) {
if (!_fonts.ContainsKey(fontName)) {
return _default.DrawString(text, location, color);
}
return _fonts[fontName].DrawString(text, location, color);
}
public StringMetrics DrawString(string text, Vector2 location, Color4 color) {
return _default.DrawString(text, location, color);
}
public void DrawStrings(string[] strings, Vector2 position, Color color) {
var metrics = new StringMetrics();
foreach (var s in strings) {
var relPos = new Vector2(position.X, position.Y + metrics.BottomRight.Y + metrics.OverhangBottom);
metrics = _default.DrawString(s, relPos, color);
}
}
public void DrawStrings(string fontName, string[] strings, Vector2 position, Color color) {
var metrics = new StringMetrics();
var font = _default;
if (_fonts.ContainsKey(fontName)) {
font = _fonts[fontName];
}
foreach (var s in strings) {
var relPos = new Vector2(position.X, position.Y + metrics.BottomRight.Y + metrics.OverhangBottom);
metrics = font.DrawString(s, relPos, color);
}
}
Using these methods is very simple. In the example, I use the multi-line variants to output the current position of the red blob on the terrain map, along with its current pathing destination. For the sake of completeness, I’m also using a named bold font to draw some red, bold text to the upper-right of the screen as well.
// PathfindingDemo.DrawScene()
FontCache.DrawStrings(
new[] {
"Currently: " + _unit.Position,
"Destination: " + _unit.Destination.MapPosition
},
Vector2.Zero,
Color.Yellow
);
FontCache.DrawString("bold", "This is bold", new Vector2(Window.ClientSize.Width - 200, 0), Color.Red);
Next Time…
Hopefully, I’ll find the ambition to do some more work on the nascent physics engine I started previously. I’ve also been thinking about making some enhancements to the skybox, incorporating some dynamic elements, like the sun position and clouds. I think I’d like to develop that into a system for modeling the day/night cycle and possibly some other cool things, like weather. I’m still noodling on it. Another thing that I’d like to work at is implementing a Quake-style console, since I was looking through that source again the other day and thinking about it. A third thing that I’d like to work on is serializing the terrain geometry to file, since randomly generating the terrain, with geometry, blendmaps, quadtrees and pathfinding information on every run is rather time consuming. Of course, I’m heading to Vegas for another industry convention in a couple weeks for the day job, and there’s a ton of work to do in order to prep for that, so we’ll see how much time I have free to work on fun stuff.
No comments :
Post a Comment