Current version

v1.10.4 (stable)


Main page
Archived news
Plugin SDK
Knowledge base
Contact info
Other projects



01 Dec - 31 Dec 2013
01 Oct - 31 Oct 2013
01 Aug - 31 Aug 2013
01 May - 31 May 2013
01 Mar - 31 Mar 2013
01 Feb - 29 Feb 2013
01 Dec - 31 Dec 2012
01 Nov - 30 Nov 2012
01 Oct - 31 Oct 2012
01 Sep - 30 Sep 2012
01 Aug - 31 Aug 2012
01 June - 30 June 2012
01 May - 31 May 2012
01 Apr - 30 Apr 2012
01 Dec - 31 Dec 2011
01 Nov - 30 Nov 2011
01 Oct - 31 Oct 2011
01 Sep - 30 Sep 2011
01 Aug - 31 Aug 2011
01 Jul - 31 Jul 2011
01 June - 30 June 2011
01 May - 31 May 2011
01 Apr - 30 Apr 2011
01 Mar - 31 Mar 2011
01 Feb - 29 Feb 2011
01 Jan - 31 Jan 2011
01 Dec - 31 Dec 2010
01 Nov - 30 Nov 2010
01 Oct - 31 Oct 2010
01 Sep - 30 Sep 2010
01 Aug - 31 Aug 2010
01 Jul - 31 Jul 2010
01 June - 30 June 2010
01 May - 31 May 2010
01 Apr - 30 Apr 2010
01 Mar - 31 Mar 2010
01 Feb - 29 Feb 2010
01 Jan - 31 Jan 2010
01 Dec - 31 Dec 2009
01 Nov - 30 Nov 2009
01 Oct - 31 Oct 2009
01 Sep - 30 Sep 2009
01 Aug - 31 Aug 2009
01 Jul - 31 Jul 2009
01 June - 30 June 2009
01 May - 31 May 2009
01 Apr - 30 Apr 2009
01 Mar - 31 Mar 2009
01 Feb - 29 Feb 2009
01 Jan - 31 Jan 2009
01 Dec - 31 Dec 2008
01 Nov - 30 Nov 2008
01 Oct - 31 Oct 2008
01 Sep - 30 Sep 2008
01 Aug - 31 Aug 2008
01 Jul - 31 Jul 2008
01 June - 30 June 2008
01 May - 31 May 2008
01 Apr - 30 Apr 2008
01 Mar - 31 Mar 2008
01 Feb - 29 Feb 2008
01 Jan - 31 Jan 2008
01 Dec - 31 Dec 2007
01 Nov - 30 Nov 2007
01 Oct - 31 Oct 2007
01 Sep - 30 Sep 2007
01 Aug - 31 Aug 2007
01 Jul - 31 Jul 2007
01 June - 30 June 2007
01 May - 31 May 2007
01 Apr - 30 Apr 2007
01 Mar - 31 Mar 2007
01 Feb - 29 Feb 2007
01 Jan - 31 Jan 2007
01 Dec - 31 Dec 2006
01 Nov - 30 Nov 2006
01 Oct - 31 Oct 2006
01 Sep - 30 Sep 2006
01 Aug - 31 Aug 2006
01 Jul - 31 Jul 2006
01 June - 30 June 2006
01 May - 31 May 2006
01 Apr - 30 Apr 2006
01 Mar - 31 Mar 2006
01 Feb - 29 Feb 2006
01 Jan - 31 Jan 2006
01 Dec - 31 Dec 2005
01 Nov - 30 Nov 2005
01 Oct - 31 Oct 2005
01 Sep - 30 Sep 2005
01 Aug - 31 Aug 2005
01 Jul - 31 Jul 2005
01 June - 30 June 2005
01 May - 31 May 2005
01 Apr - 30 Apr 2005
01 Mar - 31 Mar 2005
01 Feb - 29 Feb 2005
01 Jan - 31 Jan 2005
01 Dec - 31 Dec 2004
01 Nov - 30 Nov 2004
01 Oct - 31 Oct 2004
01 Sep - 30 Sep 2004
01 Aug - 31 Aug 2004


Powered by Pivot  
XML: RSS feed 
XML: Atom feed 

§ Drawing text in a 3D program

Every once in a while I hear someone say that 2D APIs are old and deprecated and that 3D APIs are the way to go for 2D rendering. This drives me nuts. Sure, in a 3D API like OpenGL or Direct3D, it's easy to render lines and boxes and to blit images. You even get alpha blending and rotation almost for free.

Unfortunately, another essential drawing primitive in a 2D layer, and one that is a pain in the posterior to implement, is drawing text.

By this, I don't mean drawing monospace 8x8 bitmap characters that look like they were extracted using int 10h / AX=1130h and prerendered into a 16x16 grid in a texture. Nor do I mean the ugly line based “vector fonts” that seemed to ship with the graphics library for every C compiler for DOS. I mean hinted, antialiased, properly spaced, proportional text written in a Unicode-supporting font as you'd see in any modern program.

When faced with a problem like this, an appropriate response is to try to get someone else to solve it for you, and that's actually what I would recommend first: the complexity of modern font rendering combined with the scary size of the Unicode standard book should give anyone pause. There are a few readily available libraries for Windows worth calling out here:

And then, there are a whole bunch of third-party libraries too numerous to list.

When faced with this issue myself, I decided to see how much I could leverage ye olde Graphics Device Interface (GDI), the foundation of 2D graphics in Win32. This approach has a couple of advantages. The first is its universal availability. A much more important one is that by leveraging GDI it's much easier to support any font GDI does and render text the same way as GDI, which I consider important. In particular, many approaches I've seen either don't support bitmap fonts, don't use hinting, or don't do ClearType antialiasing, which I consider showstoppers for rendering UI.

As it turns out, this was a lot more painful than I had expected due to the usual number of quirks in Win32. Here's the story of all the approaches I went through....

The GetGlyphOutline() function

A simple way to leverage GDI for drawing text in a 3D API is to have GDI draw the text into an offscreen buffer and upload that to a texture. This lets GDI handle both positioning and rendering. Downside is that performance sucks due to the constant texture uploads, so I opted for a glyph-based rendering approach instead. That means finding a way to get glyph images out of GDI.

Many examples you'll find for drawing text in Direct3D 9 use GetGlyphOutline() to get images of individual glyphs. D3DXFont uses this method, for instance. It works, but has a couple of limitations. The first is that it simply doesn't work with bitmap fonts. Most bitmap fonts are not worth using nowadays and this isn't a problem if you get to choose the fonts. It's more of an issue if you're making a tool that either adapts to the system font settings or allows the user to choose a font. I wouldn't recommend shipping a text editor or terminal emulator with this limitation, for instance.

A more serious limitation is in output format. GetGlyphOutline() at least supports hinted bi-level output – not the unhinted garbage that some font frameworks try to pass off as a non-antialiased mode – and grayscale antialiased images up to 6-bit depth. It does not support subpixel (ClearType) antialiasing. This means that on a Windows XP system or above a GGO-based text renderer cannot produce comparable quality output. ClearType isn't for everyone, but I consider support for it a requirement for a production quality text renderer, and so I dismissed a GGO-based approach.

There is one other quirk with GetGlyphOutline(): it will not render underlining even if the font was created with that option. The solution is to retrieve the outline font metrics to get the underline position and to draw it yourself.

How to get ClearType-antialiased output

There are two requirements for getting rasterized glyph output with ClearType antialiasing out of GDI. The first is that the font must be enabled for it, either by default or by specifying the CLEARTYPE_QUALITY setting to CreateFont(). The second is that it must be rendered to a screen-compatible display context. This is pretty easy – get a screen DC, create a compatible DC, bind a 32-bit DIB to it, and do ExtTextOut(). Then, do GdiFlush() and read out the bits.

Somewhat more tricky is determining the bounds of the glyph. The initial ways you'd think of to do this like GetTextExtentPoint32() and DrawText(DT_CALCRECT) all give you the wrong answer. The reason is that they only give you the advance width of the text, or how far the text advances the positioning origin, and not the actual width of the text. In particular, this does not include overhangs at the ends which are most serious with italic fonts. This means that the text can render outside of the given bounds, which makes it useless for telling how much space we need to capture the glyph image. It's possible to forego this and just use the worst-case width for all glyphs, but this results in excessive wasted space and overdraw: Tahoma 11, for instance, reports a maximum width of 19 pixels.

To fix this you need to use one of the functions that gives you the overhang amounts. Annoyingly, this is different for bitmap fonts and for TrueType fonts: for bitmap fonts it comes from tmOverhang as returned by GetTextMetrics(), and for TrueType fonts you need to use a function like GetGlyphOutline() or one of the functions that returns the ABC widths.

There's one additional gotcha: ClearType antialiasing can result in an additional pixel of overhang that the functions don't tell you about. This is one cause of colored slivers at the edges of text in various applications. I don't know of any way to detect this other than to just add one pixel on both sides when capturing the glyph and trimming it back off by scanning the bitmap afterward if needed.

Rendering text using the glyphs

The next step is to actually get the text on screen, which involves uploading the glyph images to a texture and splatting out a series of quads for each glyph. Constructing the texture is left as an exercise for the reader; in this era of Unicode and 7K+ glyphs per font it's not a good idea to preload the entire font into a texture, so this requires dynamically updating a texture based on the current required subset. This works fine as long as the font isn't too big, beyond which more drastic measures are needed like a viable scaling algorithm (bilinear or bicubic do not count) or polygonal rendering.

Doing this also requires positioning the glyphs. The simplest way is to just add the advance widths of each character starting from the left. This definitely won't win you any awards for i18n support but is at least a viable start. On the GDI side, GetTextExtentExPoint() and GetCharacterPlacement() will produce positioning arrays from a string to make this easier.

Once you have the glyphs cached in a texture and all laid out, it's just a matter of blasting out some quads. Alpha test works for bi-level and blending will handle both bi-level and grayscale.

ClearType antialiased glyphs are a bit more troublesome as they require an alpha channel for each RGB channel. Since the output of the shader is only a 4-vector and six are needed this means multiple passes. The tricky part is that the glyphs can and do overlap. My first attempt involved doing a masking pass followed by an additive pass, which resulted in overlap artifacts; doing single pass blending with one RGB channel enabled in the destination color mask worked better. The RGB alpha requirement also needs to be kept in mind if the text is being prerendered into a translucent image as it will also require that image to have RGB alpha channels unless the background is opaque.

Incidentally, this method of rendering ignores the gamma correction that GDI does when rendering text and which is required of display drivers when accelerating it. In my experience the result is acceptable without it, but you'll need it if you want to match GDI's output quality. This is expensive to do as shaders can't read from the destination surface and the hardware blender isn't nearly powerful enough to do gamma correction with configurable gamma.

GetCharacterPlacement() doesn't work so well

I glossed over glyph placement earlier. As it turns out, GDI has an attractive-looking function called GetCharacterPlacement() to do this... but it doesn't work as well as you'd think.

GetCharacterPlacement() function does a lot of work for you in that it both converts characters to glyph indices and computes positions for you. It is less useful than it looks, however. The first problem I found is that it doesn't appear to handle diacritics properly despite having a flag to enable them; with fonts like Tahoma it seems to just place them in totally wrong positions.

A bigger problem with GCP() has to do with font substitution. Starting with Windows 2000, font drawing functions like TextOut() can draw characters even if they're not supported by the current font by pulling in glyphs from other linked fonts. For instance, it will draw katakana from a Unicode string with Tahoma selected in the display context. Not only does GCP() not handle font substitution, but it will also give you garbage glyph indices back instead with no error. This causes your text routine to draw a 'q' character where another character should be showing up.

This problem actually exists with any of the APIs that work in glyph indices as those are specific to each font, although some APIs are better behaved: GetGlyphIndices() at least returns the invalid character. Unfortunately, this means that if you are bypassing GDI's rendering, you need to do the font substitution yourself.

MLang to the rescue... not

The Old New Thing has a post about using MLang to do font substitution to handle missing glyphs ( I tried this and found it problematic.

The basic approach involves checking the charsets supported by the font and having MLang remap to provide alternate fonts for any missing ones. Well, the first trouble I hit was with the Marlett font. Marlett is a strange but useful font in Windows that provides glyphs to represent common window decoration symbols in the classic theme, such as close buttons and menu checkmarks. MLang just totally dies on Marlett: GetFontCodePages() returns a charset mask of zero, and GetFontUnicodeRanges() returns E_FAIL. The result is that your text renderer can't properly tell what is in the Marlett font and helpfully substitutes a different one to render a submenu icon as an actual 8 on screen. Doh.

Don't care about Marlett? Okay, then try the pseudo-font MS Shell Dlg instead. CreateFont() succeeds, you can draw with it, and GetFontCodePages() will work with it. GetFontUnicodeRanges() returns... you guessed it, E_FAIL. Wonderful.

The backslash problem

Okay, so perhaps you don't care about drawing window decorations or drawing characters outside of the selected font. This was the way I was leaning, and with those limitations everything seemed to be working OK with using GetCharacterPlacement() for glyph conversion and positioning. That's when I ran into a brick wall with, of all things, the backslash character.

As you may know, I have been running with the system code page set to Japanese. A historical quirk of running this way is that backslashes appear as yen signs, which makes file paths look pretty strange. This is something I've gotten used to and didn't think about when working on this until it came time to render a file path on screen and ended up with 'y' characters where the backslashes should have been. That's when I discovered the serious problem with the way this substitution occurs.

Here's what happens: when you are running with the system code page set to Japanese and only with certain fonts like Tahoma and Microsoft Sans Serif, GDI checks for the backslash character (U+005C) and draws the yen sign instead (U+00A5). So far, so good... except that for some reason, it doesn't pull U+00A5 from Tahoma, but from a different font. Strange, but with the font substitution in TextOut() it works.

What doesn't work so well is trying to draw U+005C in a custom renderer. Because GDI blocks U+005C from the font, GetGlyphIndices() fails on this character and GetCharacterPlacement() returns a garbage glyph index. If you're using MLang to check if font substitution is needed, it won't catch this because either it nor your program knows this is happening: the charset test still passes, and GetFontUnicodeRanges() still reports that U+005C is in the font, because it is. As a result, your text renderer proceeds anyway and still fails to render backslashes properly.

And where are backslashes used? Oh yeah, file paths.

You can detect and special case this situation by checking for the GetGlyphIndices() failure and calling MapFont() to remap, but then it gets even more weird. MLang still doesn't know about the substitution, so it looks for a backslash and not a yen glyph. This leads to the bizarre behavior of it pulling a backslash from a different font, so you get a backslash from Arial in the middle of your text and it doesn't match all the other programs that are rendering a yen sign. Great.

(By the way, the GetGlyphOutlines() function does handle all of these substitutions if you are using it to extract bitmaps, so you're spared this problem if you're OK with its limitations.)

The outlier font

The basic problem here is that GDI has two separate paths for handling text, one that uses glyph indices and another that uses characters, and the opaque character and font substitutions are happening only on the latter path. Therefore, the solution is to avoid glyph indices entirely and use only the character APIs. This means ditching GetCharacterPlacement() and GetGlyphIndices().

In order to correctly capture and position each glyph, we still need to know its size parameters, specifically the ABC widths. GetCharABCWidths() will do this although it has the annoying restriction of only working with outline fonts; that's OK, because bitmap fonts don't have non-zero A and C widths and we can fall back to GetCharWidth() instead. Allocate bitmap space using the B width along with a little gutter, call TextOut() to draw the glyph, copy that to the texture, and use the ABC widths to position the quads... done.

Or not.

When I tried doing this, it seemed to work across the board, except there was one font on my system for which it failed: Calibri. Inexplicably, GetCharABCWidths() fails to handle font substitutions with this specific font even though other functions like GetTextExtentPoint32() work fine. This leaves us again without a reliable way to get the ABC widths of a character. Argh!

The final solution

After almost tearing my hair out over all of these issues, I resorted to the solution I always hate using: image scanning.

Basically, I ripped out the code to try to extract ABC widths from GDI and instead wrote code to allocate a worst-case sized bitmap, render a glyph into it with TextOut(), and then scan the pixels in the bitmap to obtain a bounding rectangle. From this bounding rectangle, it then determined the A width from the offset and the B width from the rectangle width. Afterward, GetTextExtentPoint32() was used to obtain the advance width (A+B+C), from which the C width was derived. The result: a solution that finally worked with ClearType antialiasing, bitmap fonts, font substitution, and backslashes.


Comments posted:

> Incidentally, this method of rendering ignores the gamma correction that GDI does when rendering text and which is required of display drivers when accelerating it. In my experience the result is acceptable without it, but you'll need it if you want to match GDI's output quality.

Be very very careful with this - the result might be acceptable with black text on white background, but when you have the inverse, lines often become too thin and text much harder to read.

ender - 30 12 12 - 22:59

I suppose you left out a bunch of other complications, like combining characters, RTL, complex scripts and such... I find it a real shame that rendering text outside of GDI is still so much pain if one requires ClearType.

Roman - 31 12 12 - 02:30

> Be very very careful with this - the result might be acceptable with black text on white background, but when you have the inverse, lines often become too thin and text much harder to read.

Having seen the VS2010 betas, this is what I feared, but it works better without correction than I thought it would. The backup plan I had for this was to have GDI render black-on-white and white-on-black and then lerp between them depending on the foreground color.

Since you can't legally read from the same surface that you are drawing to in most 3D APIs, doing correct gamma correction here is VERY expensive... it requires a 2D copy of the background to a temporary surface, and since the glyphs can overlap it means that the entire text span also has to be precomposited onto another surface. I imagine that all of the video drivers that accelerate text rendering in WDDM 1.1+ are using hardware features beyond what you can access in Direct3D.

> I suppose you left out a bunch of other complications, like combining characters, RTL, complex scripts and such

Yeah, I did. At least a lot of this can be handled above the rendering layer, though, as D3DXFont does with Uniscribe.

Phaeron - 31 12 12 - 06:48

MichKap recommends ditching GCP:

Yuhong Bao (link) - 31 12 12 - 09:16

Yes, he does, but that's because it doesn't support more complex scripts. That's different from the situation I'm seeing here where it's basically just broken. You'd think it would do at least basic diacritics since it has a flag for it.

Phaeron - 31 12 12 - 09:29

> Having seen the VS2010 betas, this is what I feared, but it works better without correction than I thought it would.

I'm just mentioning this because Opera didn't do gamma correction when they started implementing OpenGL hardware acceleration, and since I use a dark theme (and have a dark CSS set up to apply to websites), the problem was so annoying that I had to disable acceleration to browse normally.

ender - 31 12 12 - 22:12

I know what you mean... WPF had similar problems in the VS2010 betas. I really don't want to have to precompose and readback the destination surface to a temp texture if I can help it, though... what a PITA. The Registry settings on my system indicate a ClearType gamma setting of 1.2, which means that sRGB read/write are out of the question as they use a sRGB (~2.2) gamma curve instead. I have some spare channels in the font cache texture, though, so perhaps I could put in the lerp hack.

Phaeron - 01 01 13 - 10:01

I just wanted to fill in on that backslash issue: it is basically nothing special magic, simply some broken fonts there (namely msgothic.ttc in the case of Japanese). The REVERSE SOLIDUS glyphs (and all the bitmaps) are just a copy of the currency symbol in them, to fix you "just" have to redraw them.

That is of course easier said than done, because no font editors support TTC handling, and otherwise break various parts of these fonts (I had broken vertical text in one case). In the end I just had to make my own tools to split the font file into components and properly fix them.

I have fixed versions of the XP and 7 msgothic.ttc here (and it probably wouldn't be hard to fix others too as long as they're the normal glyf/EBDT stuff), in case you might be interested.

Lord - 02 01 13 - 07:44

Sometimes I think that Microsoft makes extra effort to make it impossible to have good font rendering on Windows platform.

What I am curious about is how did you solve spacing/kerning issues? Do you render all two-character combinations first to get proper spacing between each pair, or (hopefully) something smarter and simpler?

Igor Levicki (link) - 03 01 13 - 05:12

Oh, and before I forget, did you consider using freetype2 library? If not, why not?

Igor Levicki (link) - 03 01 13 - 05:13

I'm just ignoring kerning for now. Fixing that would likely involve GetKerningPairs() -- simple, but would only work for characters in the base font -- or using GetTextExtentExPoint() to compute the advance positions.

Didn't consider freetype2, because it's rather big to drag in and goes against the "use GDI to render the same as GDI" idea. There's also the hinting issue.

Phaeron - 03 01 13 - 07:47

Why doesn't the "use GDI to draw whole string into offscreen buffer, then use that as texture" approach work? Does the text change often enough that texture upload becomes a bottleneck?

Chris - 06 01 13 - 21:45

Performance is definitely a concern. It's common for 3D-based UI systems to just draw the entire UI every frame, so if you've got something like a big textbox full of text you could see some pretty hefty CPU load from re-rendering and uploading that text, particularly if the textbox is not good at precaching text spans. Not something you'd want to do if your CPU is weak (in-order or on a mobile device).

However, doing it that way still incurs two other problems in common with the per-glyph solutions. First, you still have the annoying problem of trying to determine how big the text _actually_ is. Second, GDI doesn't see the real background, so its gamma correction still doesn't work. Basically, letting GDI render text spans helps with the layout but not with the rendering issues.

On the other hand, you do get better vertex and potentially fill throughput compared to a direct per-glyph solution since you're rendering one big quad instead of one per glyph. Triangle counts rise pretty quickly with text -- doesn't take much to hit 10K+ triangles if you have a lot of text on screen, and you start pushing a decent amount of data just in geometry. It's not really a problem on a modern PC, so I haven't looked for faster solutions like point sprites, and unfortunately I can't depend on having geometry shaders.

Phaeron - 07 01 13 - 16:04

How about using PBO?

glBindBuffer(GL_PIXEL_UNPACK_BUFFER, g_buffer);
if (pbo != NULL) {
// TODO: render into pbo using GDI
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, g_tx, g_ty, 0, GL_BGR, GL_UNSIGNED_BYTE, 0); // data = 0 -> Read from PBO which is essentially DMA transfer on device

Igor Levicki (link) - 12 05 13 - 17:09

Not sure what you're getting at -- a PBO will help with the texture uploads, but ideally you're caching and not uploading every frame. The cost of rendering the text in software with GDI will also dwarf the cost of the upload, PBOs or not. Atlasing the spans into a single cache texture to reduce texture state changes would also be more important than optimizing the upload.

Phaeron - 12 05 13 - 18:42

You are right that rendering would be a bigger problem than upload, I was just saying that upload can be really fast.

It would be cool if there was truetype font rendering acceleration... oh wait, there is:

Just not on older OS than Windows 7. Time to drop some backward compatibility and portability, eh?

Igor Levicki (link) - 22 06 13 - 00:58

Since you already dismissed DirectWrite, what about WARP?

Igor Levicki (link) - 22 06 13 - 01:01

Except for the part where I still have to write a custom rendering backend to target something other than D2D or GDI, and then go through extra steps to avoid blurry, unreadable text. No thanks. Every program I've ever seen that switched over to DirectWrite has fought with poor-quality text from the lack of whole pixel snapping that the default settings give, and it drives me nuts. And if you think I would raise system requirements to Windows 7 just to use DirectWrite you're out of your mind.

WARP is great... but it's a software polygon rasterizer, and doesn't have anything to do directly with text rendering. It's more powerful on Windows 8 where they have extended it back to the D3D9 interfaces. The two weird things about it are that it (a) doesn't support dithering and (b) doesn't support the A8L8 texture format, which are pretty much ubiquitous on DX9-class hardware.

Phaeron - 24 06 13 - 16:12

If your glyph textures have gamma (e.g. 2.0), to fix white text on dark background, you can do it in the shader. Just multiply fragment color's alpha component by pow(alpha, 0.5) when the fragment's luminosity (e.g. r * .3 + g & .59 + b * .11) is over, say, 0.8. An assumption is that bright text is going to be drawn on a dark background so it's okay to undo the gamma in the glyph texture's alpha channel.

Ray Gardener (link) - 04 06 16 - 23:50

sqrt() in the fragment shader? Yuck. Second copy in a dynamic texture atlas isn't that big of a deal, and has better precision. Part of me also thinks that 1-sqrt(1-a^2) is more correct for flipping the approximation, but I'm too lazy to test it right now.

Phaeron - 29 06 16 - 16:33

Comment form

Please keep comments on-topic for this entry. If you have unrelated comments about VirtualDub, the forum is a better place to post them.
Remember personal info?

Email (Optional):
Your email address is only revealed to the blog owner and is not shown to the public.
URL (Optional):
Comment: /

An authentication dialog may appear when you click Post Comment. Simply type in "post" as the user and "now" as the password. I have had to do this to stop automated comment spam.

Small print: All html tags except <b> and <i> will be removed from your comment. You can make links by just typing the url or mail-address.