There's nothing particularly special in how font rendering is handled in OpenTK.
OpenTK - among other things - contains an OpenGL wrapper. And as such any techniques you'd use to render text in OpenGL apply here, namely:
- Using bitmap fonts - loading font atlas texture and rendering textured quads. BMFont
- Using geometric primitives (triangles/quads) to render glyphs - vector graphics style.
- Signed distance fields (SDFs) Improved Alpha-Tested Magnification for Vector Textures and Special Effects
Here's a complete self-contained sample that:
- generates mono-spaced bitmap font and saves it to disk
- runs an OpenTK example that displays it.
- N.B. will open the generated bitmap font using
Process.Start
;
- Has settings to either use fonts installed in the system or load a font from file (
FromFile
variable);
- If
BitmapFont
variable is true, the atlas texture will be rendered without anti aliasing, for pixelated fonts.
Add reference to OpenTK
and System.Drawing
to compile.
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.IO;
using OpenTK;
using OpenTK.Graphics.OpenGL;
using OpenTK.Input;
namespace SimpleText {
public static class Settings {
public static string FontBitmapFilename = "test.png";
public static int GlyphsPerLine = 16;
public static int GlyphLineCount = 16;
public static int GlyphWidth = 11;
public static int GlyphHeight = 22;
public static int CharXSpacing = 11;
public static string Text = "GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);";
// Used to offset rendering glyphs to bitmap
public static int AtlasOffsetX = -3, AtlassOffsetY = -1;
public static int FontSize = 14;
public static bool BitmapFont = false;
public static string FromFile; //= "joystix monospace.ttf";
public static string FontName = "Consolas";
}
class Program {
static void Main(string[] args) {
GenerateFontImage();
using (App app = new App()) {
app.Run();
}
}
static void GenerateFontImage()
{
int bitmapWidth = Settings.GlyphsPerLine * Settings.GlyphWidth;
int bitmapHeight = Settings.GlyphLineCount * Settings.GlyphHeight;
using (Bitmap bitmap = new Bitmap(bitmapWidth, bitmapHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
{
Font font;
if (!String.IsNullOrWhiteSpace(Settings.FromFile))
{
var collection = new PrivateFontCollection();
collection.AddFontFile(Settings.FromFile);
var fontFamily = new FontFamily(Path.GetFileNameWithoutExtension(Settings.FromFile), collection);
font = new Font(fontFamily, Settings.FontSize);
}
else {
font = new Font(new FontFamily(Settings.FontName), Settings.FontSize);
}
using (var g = Graphics.FromImage(bitmap))
{
if (Settings.BitmapFont)
{
g.SmoothingMode = SmoothingMode.None;
g.TextRenderingHint = TextRenderingHint.SingleBitPerPixel;
}
else {
g.SmoothingMode = SmoothingMode.HighQuality;
g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
//g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
}
for (int p = 0; p < Settings.GlyphLineCount; p++)
{
for (int n = 0; n < Settings.GlyphsPerLine; n++)
{
char c = (char)(n + p * Settings.GlyphsPerLine);
g.DrawString(c.ToString(), font, Brushes.White,
n * Settings.GlyphWidth + Settings.AtlasOffsetX, p * Settings.GlyphHeight + Settings.AtlassOffsetY);
}
}
}
bitmap.Save(Settings.FontBitmapFilename);
}
Process.Start(Settings.FontBitmapFilename);
}
}
class App : GameWindow {
public App() : base(800, 600){}
int FontTextureID;
int TextureWidth;
int TextureHeight;
protected override void OnLoad(EventArgs e) {
FontTextureID = LoadTexture(Settings.FontBitmapFilename);
GL.Enable(EnableCap.Texture2D);
GL.ClearColor(Color.ForestGreen);
GL.MatrixMode(MatrixMode.Projection);
GL.Ortho(0, Width, Height, 0, 0, 1);
GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);
}
int LoadTexture(string filename) {
using (var bitmap = new Bitmap(filename)) {
var texId = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2D, FontTextureID);
BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, bitmap.Width, bitmap.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0);
bitmap.UnlockBits(data);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
TextureWidth = bitmap.Width; TextureHeight = bitmap.Height;
return texId;
}
}
public void DrawText(int x, int y, string text) {
GL.Begin(BeginMode.Quads);
float u_step = (float)Settings.GlyphWidth / (float)TextureWidth;
float v_step = (float)Settings.GlyphHeight / (float)TextureHeight;
for (int n = 0; n < text.Length; n++) {
char idx = text[n];
float u = (float)(idx % Settings.GlyphsPerLine) * u_step;
float v = (float)(idx / Settings.GlyphsPerLine) * v_step;
GL.TexCoord2(u, v);
GL.Vertex2(x, y);
GL.TexCoord2(u + u_step, v);
GL.Vertex2(x + Settings.GlyphWidth, y);
GL.TexCoord2(u + u_step, v + v_step);
GL.Vertex2(x + Settings.GlyphWidth, y + Settings.GlyphHeight);
GL.TexCoord2(u, v + v_step);
GL.Vertex2(x, y + Settings.GlyphHeight);
x += Settings.CharXSpacing;
}
GL.End();
}
public void Blt(double x, double y, double width, double height) {
GL.Begin(BeginMode.Quads);
GL.TexCoord2(0, 0);
GL.Vertex2(x, y);
GL.TexCoord2(1, 0);
GL.Vertex2(x + width, y);
GL.TexCoord2(1, 1);
GL.Vertex2(x + width, y + height);
GL.TexCoord2(0, 1);
GL.Vertex2(x, y + height);
GL.End();
}
protected override void OnRenderFrame(FrameEventArgs e) {
GL.Clear(ClearBufferMask.ColorBufferBit);
GL.Disable(EnableCap.Blend);
Blt(10, 40, TextureWidth, TextureHeight);
GL.Enable(EnableCap.Blend);
DrawText(10, 10, Settings.Text);
SwapBuffers();
}
protected override void OnKeyDown(KeyboardKeyEventArgs e) {
if (e.Key == Key.Escape) Close();
}
}
}