• Advertisement
Sign in to follow this  
  • entries
  • comments
  • views

Making my own SpriteFont implementation

Sign in to follow this  


Hi everyone.

So it's been a little while since my last journal entry. Various things about the internals of Firework Factory were annoying me so in true programmer fashion i had to spend some time ripping them out and rewriting them. Refactor, refactor, refactor!

Over the past couple of weeks i have removed every last trace of D3DX from my code, and fully got rid of any dependency upon the July 2010 DirectX SDK, making sure the entire game will build just using the Windows Platform SDK.

This has been a huge success, and now I hopefully have a cleaner, and more future proof codebase. I do not want to end up in the same rut as before, with a game written in a dead technology.

As part of this I had attempted to integrate DirectXTK into the game so that i could use its texture loader, and sprite font capability. This did not all go according to plan. I spent several days trying to identify why whenever i drew fonts with it's implementation of SpriteFont, it would mess up my rasteriser state, drawing just elongated triangles all over the screen. After temporarily moving most of the scene initialisation into per-frame initialisation to troubleshoot i was still having issues, and decided that as a learning experience and to be in full control of the source code, i would write my own SpriteFont implementation on top of my existing Sprite2D implementation, instead of using DirectXTK SpriteFont on top of Texture2D.

I decided that there were some features (font rotation, font scaling etc) i did not need as i could simply do these by drawing my sprites onto a texture and rotating that if i found i needed these facilities later, and that these did not need to be part of the spritefont class itself.

This did not take as long as I had expected, and my own cut-down implementation of SpriteFont was a lot simpler and easier to read than the one that comes as part of DirectXTK.

My Sprite2D implementation is a simple 2D sprite renderer, which uses WIC to load the sprite from a PNG file stored in a zip:class Sprite2D{private: // Direct2D Bitmap ID2D1Bitmap* graphic; int* rawpixels; // Direct2D render target ID2D1RenderTarget* rendertarget; UINT width; UINT height;public: // Create a new Sprite2D from a file on disk using WIC to load the image. // Throws std::exception on failure to load the file. Sprite2D(ID2D1RenderTarget *pRenderTarget, const std::wstring &filename); // Draw the Sprite2D. DX11::Begin2D() must be called before this can be used. void Draw(const D2D1_RECT_F &destination, float opacity, const D2D1_RECT_F &source); // Free bitmap resource ~Sprite2D(); UINT GetWidth(); UINT GetHeight(); UINT GetPixel(UINT x, UINT y);};
The astute amongst you may have noticed the GetPixel() method immediately. Please do not grab your torches and pitchforks and descend upon me just yet - this is not as inefficient in terms of CPU usage as you might first think. Currently only the SpriteFont system uses this (you will see why below) and it references a copy of the actual ID2DBitmap which is copied as raw pixels into an int array.

For the uninitiated, at its basic level a SpriteFont as seen by XNA is a bitmap sprite sheet, with a bunch of metadata to identify where each rectangle is that identifies any one particular glyph. This metadata is built by the content pipeline at compile time.

In my version of SpriteFont i decided to cut out this process of pre-compiling metadata. Identifying the glyphs in the bitmap is quite fast enough to be done when loading the assets, as no player notices a couple of extra milliseconds here. In short, i am able to keep my spritefonts as just a simple sprite sheet and build the rest at runtime.

My sprite sheets for fonts look like this:


This represents each of the ASCII characters from a space (ASCII 32) to the "}" character (ASCII 125). These are loaded using the Sprite2D class above, and then passed to a Font class:// Maximum ASCII value of highest glyph in a sprite sheet, minus one#define MAX_GLYPH_ASCII 126// Representation of a sprite font.// Sprite fonts are read from a sprite sheet generated by an external application// e.g. iron Star Media Fancy Bitmap Font Generator. Usually in XNA these are parsed// as part of the content pipeline into an XML schema, some metadata and the underlying// bitmap. Parsing the bitmap ourselves isn't /that/ expensive these days and perfectly// acceptable to do at initialisation time.class Font{private: // The sprite sheet containing all the glyphs from space to '}' Sprite2D* spritesheet; // The rectangular location in the sprite sheet for each glyph D2D1_RECT_F glyphs[MAX_GLYPH_ASCII]; // Identifies if a given pixel is opaque magenta bool IsMarkerColour(UINT pixel); // The height of a line of text calculated via max(all character heights) UINT lineheight;public: // Constructor accepts a sprite object Font(Sprite2D* sprite); // Draw a string to the screen at given position and opacity void Draw(const std::string &text, D2D1_POINT_2F position, float opacity); // Measure the X and Y size of any given string D2D1_POINT_2F Measure(const std::string &text); // NOTE: Destructor does NOT delete sprite ~Font();};
The Font class constructor then examines the bitmap of the sprite, looking for the edges of the transparent areas surrounded by opaque magenta:// Fully opaque magenta in ARGB formatconst UINT32 MAGIC_MAGENTA = 0xffff00ff;Font::Font(Sprite2D* sprite) : spritesheet(sprite), lineheight(0){ // Start at ASCII value for space char index = ' '; // Clear all glyph entries so they default to a size of 0,0 ZeroMemory(&glyphs, sizeof(D2D1_POINT_2F) * MAX_GLYPH_ASCII); // Heavily based on MakeSpriteFont.cs from DirectXTK... for (UINT y = 0; y < spritesheet->GetHeight(); ++y) { for (UINT x = 0; x < spritesheet->GetWidth(); ++x) { // Look for the top left corner of a character (a pixel that is not the marker colour, but was the marker colour immediately to the left and above it) if (!IsMarkerColour(spritesheet->GetPixel(x, y)) && IsMarkerColour(spritesheet->GetPixel(x - 1, y)) && IsMarkerColour(spritesheet->GetPixel(x, y - 1))) { // Measure the size of this character, first across horizontally then down, pixel by pixel int w = 1, h = 1; while ((x + w < spritesheet->GetWidth() && !IsMarkerColour(spritesheet->GetPixel(x + w, y)))) { w++; } while ((y + h < spritesheet->GetHeight()) && !IsMarkerColour(spritesheet->GetPixel(x, y + h))) { h++; } // Bounds check if (index == MAX_GLYPH_ASCII) throw std::invalid_argument("Too many glyphs found in sprite"); // Store glyph to the array glyphs[index++] = D2D1::RectF(x + 1, y + 1, x + w - 1, y + h - 1); if (h > lineheight) lineheight = h - 1; } } }}bool Font::IsMarkerColour(UINT pixel){ // Magenta is the marker colour between glyphs. // There is normally a 6-pixel padding of marker colour between each glyph. return (pixel == MAGIC_MAGENTA);}
I based this bit of code heavily on MakespriteFont's BitmapImporter.cs, a c# program for building sprite font metadata which comes with DirecXTK.

As you can see if this was done often during the game loop it would be computationally expensive and very much a no-no, but as it stands, done at startup, this is acceptable as the time taken isn't even noticable.

Once this initialisation has run we have an array of rectangular areas, each of which represents the ASCII character glyph for a given ASCII code. We simply iterate a string's characters to draw it, displaying each in turn and iterating the X and Y coordinates to draw the glyphs at appropriately:void Font::Draw(const std::string &text, D2D1_POINT_2F position, float opacity){ // Iterate all characters in the string. Note that characters we have no glyph // for are rendered as a 0 by 0 pixel bitmap so do not appear in the output. for (std::string::const_iterator ch = text.begin(); ch != text.end(); ++ch) { // Skip carriage return if (*ch == '\r') continue; // Newline advances the position down one line using the lineheight else if (*ch == '\n') { position.y += lineheight; continue; } // Calculate width and height of current glyph const D2D1_RECT_F& glyph = glyphs[*ch]; UINT gwidth = glyph.right - glyph.left; UINT gheight = glyph.bottom - glyph.top; // Draw the section of the sprite sheet that refers to the glyph spritesheet->Draw(D2D1::RectF(position.x, position.y, position.x + gwidth, position.y + gheight), opacity, glyph); position.x += gwidth; }}
For measuring the potential dimensions of any particular string in a given font, we can use the same style of function, just with the Draw calls taken out:D2D1_POINT_2F Font::Measure(const std::string &text){ D2D1_POINT_2F ret = D2D1::Point2F(); ret.y = lineheight; // The code here is practically identical to Font::Draw, except instead of drawing // we just record the current X and Y sizes of what we would have rendered. for (std::string::const_iterator ch = text.begin(); ch != text.end(); ++ch) { if (*ch == '\r') continue; else if (*ch == '\n') { ret.y += lineheight; continue; } const D2D1_RECT_F& glyph = glyphs[*ch]; ret.x += (glyph.right - glyph.left); } return ret;}
Now we have a complete font rendering system, we can use it to build a simple menu out of sprites and fonts:


The code to do this then becomes very simple:titlefont->Draw("this is some text", D2D1::Point2F(x_pos, y_pos), 1.0f);
As for potential improvements to this code, i see no reason why i couldn't pre-render the entire string onto a texture or other 2D sprite, then blend that texture or other 2D sprite into the screen when needed. However, for now, this is performant enough for what i need to do, as you can see in the video below of it running on top of the actual game:

Comments and other feedback are always welcome! smile.png
Sign in to follow this  


Recommended Comments

Nice. Good to remove the need for metadata.

I had my own font system a few years ago. It was Win32 program that basically used GDI to render each character to an offscreen DC then it spat the size and the pixels to a file, with the pixels just expressed as a value 0 to 255 for each pixel.

The loader in game then created a spritesheet that was all white, with this value as the alpha, so I could then use vertex colours to colour the output

I like the simplicity of yours. How are you generating the source image?

Share this comment

Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Advertisement