2
\$\begingroup\$

I've been looking everywhere for a solution on how to render/draw text for my 2D OpenTK-based game.

Been trying quite a few QuickFont ones without success (either a AccessViolation exception or text just not rendering), which is why I finally decided to ask here.

So my question is; How do I render/draw text in OpenTK for my 2D game?

Thanks!

EDIT

I have tried the following example from QuickFont.Desktop GitHub

QFont _myFont;
QFont _myFont2;
QFontDrawing _drawing;

_myFont  = new QFont("Fonts/HappySans.ttf", 72, new QFontBuilderConfiguration(true));
_myFont2 = new QFont("basics.qfont", new QFontBuilderConfiguration(true));
_drawing = new QFontDrawing();  //This line throws exception "Exception thrown: 'System.AccessViolationException' in QuickFont.dll"

As I get an error here, It's hard to tell if the rest is wrong or not. It's all in the link above for anyone interested.

What I am looking for is basically a complete "working" guide for the latest OpenTK build with .NET Framework 4.5.2

\$\endgroup\$
5
  • \$\begingroup\$ You'll have to show some code in order to have any chance of anyone being able to help you out with QuickFont. \$\endgroup\$
    – JBeurer
    Jun 16, 2016 at 14:48
  • \$\begingroup\$ @JBeurer updated my post above. \$\endgroup\$ Jun 16, 2016 at 18:09
  • \$\begingroup\$ Can you step through the debugger to see where inside the QFontDrawing it fails? \$\endgroup\$
    – JBeurer
    Jun 16, 2016 at 19:42
  • \$\begingroup\$ QuickFont aside, there really are multiple ways how to draw text with OpenGL (as OpenTK is OpenGL wrapper, among other things ). If you're looking for a simple solution, I would suggest loading a bitmap of a mono-spaced font as a texture, and then drawing letters quad by quad from it. Would be easy to implement as long if you're fine being limited with ASCII 256chars. \$\endgroup\$
    – JBeurer
    Jun 16, 2016 at 19:52
  • \$\begingroup\$ @JBeurer tried to step through it but I couldn't. It's been installed via NuGet so I guess I can't access the source-code. I managed to trace it via Stack Trace and found out that It's stopping at; QuickFont.QFontDrawing.LoadShaderFromResource(String path) \$\endgroup\$ Jun 16, 2016 at 20:04

1 Answer 1

6
\$\begingroup\$

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:

  1. Using bitmap fonts - loading font atlas texture and rendering textured quads. BMFont
  2. Using geometric primitives (triangles/quads) to render glyphs - vector graphics style.
  3. 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(); 
    }        
}    
}
\$\endgroup\$
4
  • \$\begingroup\$ I tried this and It actually works. Thanks! However, as I started to modify it, I noticed there's really no "way" of clearing the text. Any advice on how to do so? \$\endgroup\$ Jun 17, 2016 at 10:36
  • \$\begingroup\$ Decided to just stop rendering it instead, it kinda works I guess. \$\endgroup\$ Jun 17, 2016 at 10:52
  • \$\begingroup\$ OnRenderFrame gets called all the time at whatever FPS your computer can handle. (unless vsync is on, then it's capped) GL.Clear(ClearBufferMask.ColorBufferBit); - clears the screen and then you decide what you want to draw at the given frame. \$\endgroup\$
    – JBeurer
    Jun 17, 2016 at 11:41
  • 1
    \$\begingroup\$ @LudvigÅslund Consider marking the answer as "accepted" if it answers the question. \$\endgroup\$ Jul 27, 2016 at 0:35

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .