Search code examples
c#openglopentkfreetypedrawtext

C# OpenTK - Draw string on window


I've a big problem: I've an OpenTK window open where I draw textures, images, etc. I've to do a little videogame in this manner for a test and I'ld like to show text on it that shows game infos.

Actually I've been only able to open a Window form with text and it's not what I need.

Is there a manner to show text in a OpenTK window?

I can't use OpenTK 3.0, so QuickFont has to be excluded.

I can use GL Class.

Thank you very much!


Solution

  • One possibility would be to use FreeType library to load a TrueType Font to texture objects.
    SharpFont provides Cross-platform FreeType bindings for C#.
    The source can be found at GitHub - Robmaister/SharpFont.
    (x64 SharpFont.dll and freetype6.dll from MonoGame.Dependencies)

    A full example can be found at GitHub - Rabbid76/c_sharp_opengl.
    The example eis based on LearnOpenGL - Text Rendering.

    Load the font and glyph information for the characters and create a texture object for each character:

    public struct Character
    {
        public int TextureID { get; set; }
        public Vector2 Size { get; set; }
        public Vector2 Bearing { get; set; }
        public int Advance { get; set; }
    }
    
    // initialize library
    Library lib = new Library();
    Face face = new Face(lib, "FreeSans.ttf");
    face.SetPixelSizes(0, 32);
    
    // set 1 byte pixel alignment 
    GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
    
    // Load first 128 characters of ASCII set
    for (uint c = 0; c < 128; c++)
    {
        try
        {
            // load glyph
            //face.LoadGlyph(c, LoadFlags.Render, LoadTarget.Normal);
            face.LoadChar(c, LoadFlags.Render, LoadTarget.Normal);
            GlyphSlot glyph = face.Glyph;
            FTBitmap bitmap = glyph.Bitmap;
    
            // create glyph texture
            int texObj = GL.GenTexture();
            GL.BindTexture(TextureTarget.Texture2D, texObj);
            GL.TexImage2D(TextureTarget.Texture2D, 0,
                          PixelInternalFormat.R8, bitmap.Width, bitmap.Rows, 0,
                          PixelFormat.Red, PixelType.UnsignedByte, bitmap.Buffer);
    
            // set texture parameters
            GL.TextureParameter(texObj, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
            GL.TextureParameter(texObj, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
            GL.TextureParameter(texObj, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
            GL.TextureParameter(texObj, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
    
            // add character
            Character ch = new Character();
            ch.TextureID = texObj;
            ch.Size = new Vector2(bitmap.Width, bitmap.Rows);
            ch.Bearing = new Vector2(glyph.BitmapLeft, glyph.BitmapTop);
            ch.Advance = (int)glyph.Advance.X.Value;
            _characters.Add(c, ch);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
    

    Create a Vertex Array Object which draws a quad by 2 trinagles:

    // bind default texture
    GL.BindTexture(TextureTarget.Texture2D, 0);
    
    // set default (4 byte) pixel alignment 
    GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4);
    
    float[] vquad =
    {
    // x      y      u     v    
        0.0f, -1.0f,   0.0f, 0.0f,
        0.0f,  0.0f,   0.0f, 1.0f,
        1.0f,  0.0f,   1.0f, 1.0f,
        0.0f, -1.0f,   0.0f, 0.0f,
        1.0f,  0.0f,   1.0f, 1.0f,
        1.0f, -1.0f,   1.0f, 0.0f
    };
    
    // Create [Vertex Buffer Object](https://www.khronos.org/opengl/wiki/Vertex_Specification#Vertex_Buffer_Object)
    _vbo = GL.GenBuffer();
    GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
    GL.BufferData(BufferTarget.ArrayBuffer, 4 * 6 * 4, vquad, BufferUsageHint.StaticDraw);
    
    // [Vertex Array Object](https://www.khronos.org/opengl/wiki/Vertex_Specification#Vertex_Array_Object)
    _vao = GL.GenVertexArray();
    GL.BindVertexArray(_vao);
    GL.EnableVertexAttribArray(0);
    GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 4 * 4, 0);
    GL.EnableVertexAttribArray(1);
    GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 4 * 4, 2 * 4);
    

    Furthermore create a method which draws a string at specified position which a given direction:

    public void RenderText(string text, float x, float y, float scale, Vector2 dir)
    {
        GL.ActiveTexture(TextureUnit.Texture0);
        GL.BindVertexArray(_vao);
    
        float angle_rad = (float)Math.Atan2(dir.Y, dir.X);
        Matrix4 rotateM = Matrix4.CreateRotationZ(angle_rad);
        Matrix4 transOriginM = Matrix4.CreateTranslation(new Vector3(x, y, 0f));
    
        // Iterate through all characters
        float char_x = 0.0f;
        foreach (var c in text) 
        {
            if (_characters.ContainsKey(c) == false)
                continue;
            Character ch = _characters[c];
    
            float w = ch.Size.X * scale;
            float h = ch.Size.Y * scale;
            float xrel = char_x + ch.Bearing.X * scale;
            float yrel = (ch.Size.Y - ch.Bearing.Y) * scale;
    
            // Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
            char_x += (ch.Advance >> 6) * scale; // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
    
            Matrix4 scaleM = Matrix4.CreateScale(new Vector3(w, h, 1.0f));
            Matrix4 transRelM = Matrix4.CreateTranslation(new Vector3(xrel, yrel, 0.0f));
    
            Matrix4 modelM = scaleM * transRelM * rotateM * transOriginM; // OpenTK `*`-operator is reversed
            GL.UniformMatrix4(0, false, ref modelM);
    
            // Render glyph texture over quad
            GL.BindTexture(TextureTarget.Texture2D, ch.TextureID);
    
            // Render quad
            GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
        }
    
        GL.BindVertexArray(0);
        GL.BindTexture(TextureTarget.Texture2D, 0);
    }
    

    Vertex shader:

    #version 460
    
    layout (location = 0) in vec2 in_pos;
    layout (location = 1) in vec2 in_uv;
    
    out vec2 vUV;
    
    layout (location = 0) uniform mat4 model;
    layout (location = 1) uniform mat4 projection;
    
    void main()
    {
        vUV         = in_uv.xy;
        gl_Position = projection * model * vec4(in_pos.xy, 0.0, 1.0);
    }
    

    Fragment shader:

    #version 460
    
    in vec2 vUV;
    
    layout (binding=0) uniform sampler2D u_texture;
    
      layout (location = 2) uniform vec3 textColor;
    
    out vec4 fragColor;
    
    void main()
    {
        vec2 uv = vUV.xy;
        float text = texture(u_texture, uv).r;
        fragColor = vec4(textColor.rgb*text, text);
    }
    

    See the example:

    enter image description here

    Matrix4 projectionM = Matrix4.CreateScale(new Vector3(1f/this.Width, 1f/this.Height, 1.0f));
    projectionM = Matrix4.CreateOrthographicOffCenter(0.0f, this.Width, this.Height, 0.0f, -1.0f, 1.0f);
    
    GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    GL.Clear(ClearBufferMask.ColorBufferBit);
    
    GL.Enable(EnableCap.Blend);
    GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
    
    text_prog.Use();
    GL.UniformMatrix4(1, false, ref projectionM);
    
    GL.Uniform3(2, new Vector3(0.5f, 0.8f, 0.2f));
    font.RenderText("This is sample text", 25.0f, 50.0f, 1.2f, new Vector2(1f, 0f));
    
    GL.Uniform3(2, new Vector3(0.3f, 0.7f, 0.9f));
    font.RenderText("(C) LearnOpenGL.com", 50.0f, 200.0f, 0.9f, new Vector2(1.0f, -0.25f));