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\$
  • \$\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 '16 at 14:48
  • \$\begingroup\$ @JBeurer updated my post above. \$\endgroup\$ – Ludvig Åslund Jun 16 '16 at 18:09
  • \$\begingroup\$ Can you step through the debugger to see where inside the QFontDrawing it fails? \$\endgroup\$ – JBeurer Jun 16 '16 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 '16 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\$ – Ludvig Åslund Jun 16 '16 at 20:04
5
\$\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(); 
    }        
}    
}
| improve this answer | |
\$\endgroup\$
  • \$\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\$ – Ludvig Åslund Jun 17 '16 at 10:36
  • \$\begingroup\$ Decided to just stop rendering it instead, it kinda works I guess. \$\endgroup\$ – Ludvig Åslund Jun 17 '16 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 '16 at 11:41
  • 1
    \$\begingroup\$ @LudvigÅslund Consider marking the answer as "accepted" if it answers the question. \$\endgroup\$ – S. Tarık Çetin Jul 27 '16 at 0:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.