BlueMonkMN's picture

Improvement to text drawing.

In the Usage forum I posted a question about fonts being drawn anti-aliased when they shouldn't be (only large font sizes should be anti-aliased) so I investigated and found out that it was because of the behavior of MeasureString that characters were being drawn at non-integral positions. So I did a little more coding, and I think I found a solution that makes text look much better, and could be further enhanced to relatively easily support wrapping and alignment. I just hope .NET supports these operations on all platforms. Here are the changes I've made. Please tell me if some changes like this could be included in the main code base and/or what to do next.

TextureFont.cs

        public class CharLayout
        {
           public PointF pos;
           public char character;
           public CharLayout(char character)
           {
              this.character = character;
              pos = PointF.Empty;
           }
        }
 
        public CharLayout[] GetCharLayout(string str)
        {
            List<CharacterRange> ranges = new List<CharacterRange>(32); // Win32 GDI limitation
            List<CharLayout> result = new List<CharLayout>(str.Length);
            int unprocessed_idx = 0;
            for (int idx = 0; idx < str.Length; idx++)
            {
               if (!Char.IsWhiteSpace(str[idx]))
               {
                  if (ranges.Count >= 32)
                  {
                     GetCharLayout(str, ranges.ToArray(), result, unprocessed_idx);
                     unprocessed_idx = result.Count;
                     ranges.Clear();
                  }
                  ranges.Add(new CharacterRange(idx, 1));
                  result.Add(new CharLayout(str[idx]));
               }
            }
            if (ranges.Count > 0)
               GetCharLayout(str, ranges.ToArray(), result, unprocessed_idx);
            return result.ToArray();
        }
 
        private void GetCharLayout(string str, CharacterRange[] ranges, IList<CharLayout> result, int start)
        {
           StringFormat fmt = new StringFormat(StringFormat.GenericTypographic);
           fmt.SetMeasurableCharacterRanges(ranges);
           Region[] positions = gfx.MeasureCharacterRanges(str, font, new RectangleF(0, 0, 1024, 768), fmt);
           for (int idx = start; idx < result.Count; idx++)
              result[idx].pos = positions[idx-start].GetBounds(gfx).Location;
        }

TextPrinter.cs (inside PerformLayout function)

            if (alignment == StringAlignment.Near && !rightToLeft || alignment == StringAlignment.Far && rightToLeft)
            {
                OpenTK.Fonts.TextureFont.CharLayout[] coords = font.GetCharLayout(text);
                for(int char_idx = 0; char_idx < coords.Length; char_idx++)
                {
                    x_pos = coords[char_idx].pos.X;
                    y_pos = coords[char_idx].pos.Y;
                    font.GlyphData(coords[char_idx].character, out char_width, out char_height, out rect, out texture);
                    vertices[vertex_count].X = x_pos;                // Vertex
                    vertices[vertex_count++].Y = y_pos;
                    vertices[vertex_count].X = rect.Left;            // Texcoord
                    vertices[vertex_count++].Y = rect.Top;
                    vertices[vertex_count].X = x_pos;                // Vertex
                    vertices[vertex_count++].Y = y_pos + char_height;
                    vertices[vertex_count].X = rect.Left;            // Texcoord
                    vertices[vertex_count++].Y = rect.Bottom;
 
                    vertices[vertex_count].X = x_pos + char_width;   // Vertex
                    vertices[vertex_count++].Y = y_pos + char_height;
                    vertices[vertex_count].X = rect.Right;           // Texcoord
                    vertices[vertex_count++].Y = rect.Bottom;
                    vertices[vertex_count].X = x_pos + char_width;   // Vertex
                    vertices[vertex_count++].Y = y_pos;
                    vertices[vertex_count].X = rect.Right;           // Texcoord
                    vertices[vertex_count++].Y = rect.Top;
 
                    indices[index_count++] = (ushort)(vertex_count - 8);
                    indices[index_count++] = (ushort)(vertex_count - 6);
                    indices[index_count++] = (ushort)(vertex_count - 4);
 
                    indices[index_count++] = (ushort)(vertex_count - 4);
                    indices[index_count++] = (ushort)(vertex_count - 2);
                    indices[index_count++] = (ushort)(vertex_count - 8);
                }
            }

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
Inertia's picture

A little offtopic, but not unrelated: has anyone tried to make the font smaller than requested and use bilinear texture filters to scale it up for blurring? (to workaround anti-alias)

This could make 2.b) a more interesting choice, as that would be a convenient way to provide the programmer with decals they can smack on a sign rendered in 3D, like objarni mentioned at 6)
Not sure if this is actually going to be used much, the common approach is building the whole text into the model's texture. objarni's idea is kinda interesting if you plan to localize the app for multiple languages though, this is quite problematic with the common approach if you do not have access to the texture's source image before the layers were collapsed.

the Fiddler's picture

@objarni: It's difficult to talk about performance outside specific applications, but here goes:

1) menus and static heads-up-text (does not change much ie cache-friendly)
Both should perform (almost) identically, unless a) you are pressing texture memory (method (a) will be faster then), or b) are vertex limited (method (b) will be faster).

2) fps (changes wildly, ie not cache-friendly)
These will hit the CPU (layout, rasterization) and the bus (data uploads). Using individual glyphs will save on bandwidth, but it's difficult to talk about CPU usage without benchmarking different low-level implementations first.

3) logs/console/ingame-chat (changes often but not as often as fps)
4) "message boxes" like in RPGs (layout very important, does not change much, cache-friendly)
5) score (changes often)
Mostly depends on whether you cache text or not. :)

Performance in cases 2-5 will differ significantly between implementations. Fortunately it is an area that can be tweaked a lot to reduce potential bottlenecks. Laying out individual glyphs generally helps performance (as it offloads potentially costly rasterization to the GPU), but makes it more difficult to support complex scripts.

Also, another case which I'm not sure if OpenTK intends to support at OpenTK.dll-level:
6) In-perspective text like signs in an RPG

No, this won't be provided as an option. However, you can alter the modelview matrix between a TextPrinter.Begin-End to transform text in any way you like.

@Inertia:
IMHO, any scale higher/lower than ~115%/85% makes text look bad, when you throw more than a casual glance at it.

Neither method (a) nor (b) is especially suited to decals, as the you don't have access to the cached texture. The best you could do is render the text into a texture, which is frustrating enough in both cases.

BlueMonkMN's picture

My opinion on [1 - Rasterizing Text] is that Graphics.DrawString is the clear winner, and my opinion on [2 - Managing and Displaying the Text] is less clear, but I do lean toward caching whole strings in a texture and drawing them in single blocks. Here's my reasoning:

1) I don't see OpenTK as a text drawing engine but as a thin wrapper around OpenGL. So I don't expect it to be doing too much work for me, just removing some of the tedium of drawing basic text.
2) As such, OpenTK shouldn't have to take so much responsibility for being able to render complex scripts. If the feature support was good enough for the general-purpose Graphics.DrawString function in the framework, it should certainly suffice for OpenTK's text implementation. I for one rarely use non-ASCII characters (in my game development) let alone complex scripts. Even at work (for an international company that releases ERP software in China among other locales) the worst I deal with is Chinese characters and not complex scripts that have to combine characters.
3) I lean slightly toward caching whole strings instead of using MeasureCharacterRanges to position glyphs for one reason: I have one additional concern about MeasureCharacterRanges that I didn't mention. As I may have already mentioned, it can only handle 32 characters at a time on Windows. But my concern then is, if you have to call it 10 times to draw a 320 character string, do you end up with a O(n^2) algorithm as each successive call has to lay out all the text from before in addition to the new 32 characters being calculated? I can think of some possible optimizations (such as removing lines as they are processed) but it may just not be worth the effort of trying to optimize that (there are a couple ways that word wrap complicates how much text you need to provide to make sure the text ends up in the same place for each calculation).

To to summarize my current opinion, I'm liking Graphics.DrawString directly to cached textures with no intermediate glyph management. But I'm not totally turned off of MeasureCharacterRanges if you don't think there's a performance concern.

objarni's picture

I think OpenTK has a lot to gain if it has some simple, reasonably performant way of rendering a Unicode string, as opposed to 80's smelling ASCII strings.

OpenTK is not going to be used to build Word2010 -- but it may very well be used by people from all over the world for building local games (japan, china, russia and greece springs to mind ..).

So if it is not too much additional work, please keep the Unicode ambition..!

BlueMonkMN's picture

Unicode's not an issue though. Graphics.DrawString doesn't have any problem drawing Unicode text in general, just complex scripts like Arabic where (as I understand it) characters look different depending on what order they come in. Chinese and Japanese are no problem for any of the solutions being discussed here I think.

Edit: I'm seeing some reports that even Arabic text renders correctly with Graphics.DrawString, so it's hard to come up with examples of what Graphics.DrawString can't render.

the Fiddler's picture

[Graphics.DrawString and complex scripts]
Let's not worry about complex scripts right now. We can always replace Graphics.DrawString with something else, if it becomes a problem in the future.

The issues with Graphics.DrawString is as follows: it relies on GDI+, which hasn't been updated since Windows XP SP2 was released. As such, it doesn't support many new languages/scripts added since. Check Micahel Kaplan's blog for more information (this is a goldmine on text rendering on Windows). Note that Mono's Linux/Mac OS X implementation of GDI+ does not suffer from this.

[MeasureCharacterRanges and speed]
I can't see how MeasureCharacterRanges can become O(n^2). It's O(n) as laying out 320 characters would require exactly 10 calls (processing 32 characters per call), and each call is independent (call n+1 will not touch characters layed out in calls 1-n).

The question on performance is which is faster:

  1. create an VBO of individual glyphs layed out by n/32 calls to MeasureCharacterRanges, or
  2. upload a bitmap rendered through Graphics.DrawString with GL.TexSubImage?

Difficult to say without testing. :)

[Unicode]
Unicode is here to stay, so no worries! TrueType/OpenType fonts make it impossible to *not* support unicode, anyway. :)

BlueMonkMN's picture

[MeasureCharacterRanges and speed]
I think MeasureCharacterRanges has to consider more characters than just the requested range. How could it possibly know where characters 32 through 63 are without knowing how much characters 0 through 31 offset them and how many newlines were in there? I'm assuming MeasureCharacterRanges is stateless and doesn't cache information about previous calls, so it has to lay out all the text each time (at least all the preceding text). Furthermore, if you're performing word wrap, how could MeasureCharacterRanges know that a particular character in a word can fit on the current line without positioning the rest of the characters in the word (regardless of whether they are included in the current range being measured).

BlueMonkMN's picture

What's the status of text drawing now? I haven't heard mention of it for a while.

the Fiddler's picture

The upcoming version, 0.9.1, will only contain bugfixes to the current system. The results of this discussion will appear in 0.9.2+.

0.9.1 has taken a long time to bake due to the large number of internal restructuring - future releases will appear in shorter intervals.