I occasionally like to learn something I literally have no use for. It keeps me happy and entertained. This month I decided to deal with OpenGL. And no, I don't consider learn OpenGL an useless skill. OpenGL is very much useful and, despite newer technologies, still here to stay. It's just that, since I do no game development, I have no real use for it. But I wanted to dip my toes into that world regardless.
After getting few triangles on the screen, it came time to output text and I was stunned to learn OpenGL has no real text support. And no, OpenGL is not unique here as neither Vulkan or Metal provide much support. Rendering text is simply not an integral part of rendering pipeline. And, once one gives it a thought, it's clear it doesn't belong there.
That's not to say there are no ways to render text. The most common one is treating text as a texture. The less common way is rasterizing font into triangles. Since I really love bitmap fonts and square is easily constructed from two right triangles, I decided to go the rustic route.
The first issue was which font to select. I wanted something old, rather complete, and free. Due to quirks in the copyright law, bitmap fonts are generally not considered copyrightable under US law. Mind you, that holds true only for their final bitmap form. Fonts that come to you as TTF or OTF are definitely copyrightable.
The other issue with selection was completeness. While selecting old ROM font supporting code page 437 (aka US) is easy, the support for various European languages is limited, to say the least. Fortunately, here I came upon Bedstead font family which covered every language I could think off with some extra. While era-faithful setup would include upscaling and even a rudimentary anti-aliasing, I decided to go with a raw 5x9 pixel grid.
For conversion I wrote BedsteadToVertices
utility that simply takes all character bitmaps and extracts them into a Vector2 array of triangles. The resulting file is essentially a C# class returning buffer that can be directly drawn. Something like this:
Codevar triangles = BedsteadVerticesFont.GetVertices(text,
offsetX: -0.95f,
offsetY: 0.95f,
scaleX: 0.1f,
scaleY: 0.2f);
gl.BindBuffer(BufferTargetARB.ArrayBuffer, BufferId);
gl.BufferData<float>(BufferTargetARB.ArrayBuffer, triangles, BufferUsageARB.DynamicDraw);
gl.DrawArrays(GLEnum.Triangles, 0, (uint)triangles.Length / 2);
The very first naïve version of this file ended up generating a 3.8 MB
source file. Not a breaking deal but quite a bit larger than I was comfortable with. So I went with a low hanging fruit first. Using float arrays instead of Vector2 instantly dropped the file size to 2.3 MB
. Dropping all floats to 4 decimal places dropped it further to 2.0 MB
.
And no, I didn't think about reducing the whitespace. Code generated files don't need to be ugly and reducing space count to a minimum would do just that. Especially because removing spaces will result in the exactly same compiled code at the expense of readability. Not worth it.
However, merging consecutive pixels into one big rectangle was yet another optimization that's both cheap in implementation and reduces file size significantly. In my case, the end result was 1 MB
for 1,500 characters. And yes, this is still a big file but if you exclude all the beautiful Unicode non-ASCII characters, that can bring file size down to 61 KB
. Had I wanted to go the binary route, that would be even smaller but I was happy enough with this not to bother.
While the original Bedstead font is monospaced, I decided to throw a wrench into this and remove all extra spacing where I could do so easily. That means that font still feels monospaced but you won't have excessive spaces really visible. And yes, kerning certain letter pairs (e.g., rl
) was too out of scope.
On the OpenGL side, one could also argue that this style of bitmap drawing would be an excellent territory for the use of indices to reduce triangle count. One would be right on technicality but I opted not to complicate my life for dubious gains as modern GPUs (even the integrated ones) are quite capable of handling extra few hundred of triangles.
In any case, I solved my problem and, as always, the source code is available for download.
[2022-06-07: With a bit of optimization, ASCII-only file is at 50 KB
.]