game f# opengl
Created: 2020-01-16

Text rendering with OpenGL and F#

Unlike WebGL, rendering text in OpenGL is a bit more complicated. Here's how I went about it with F#.

Example text rendering

While it's possible to load bitmap fonts fairly easily, TrueType fonts look nicer and give you a lot more flexibility in terms of sizes and styles.

LearnOpenGL.com has a very detailed article on text rendering. I recommend reading that article first and I'll cover some of the F# specifics here.

FreeType is used to load TrueType fonts. FreeType is a C based library so first I had to understand how to load a DLL in F# and access it's functions.

Marshalling calls to the FreeType C library

There are some libraries available to do this already but I couldn't really find one that simply worked and I wanted to understand the process anyway.

First we'll need the Interop library:

1: 
open System.Runtime.InteropServices

HANDLEs are used a lot for pointers in FreeType so I declared a type alias to match:

1: 
type HANDLE = nativeint

The function definitions are fairly straightforward. For example, initializing FreeType is done with the C function:

1: 
FT_EXPORT( FT_Error ) FT_Init_FreeType( FT_Library  *alibrary )

The F# declaration for this would be:

1: 
2: 
[<DllImport("lib/freetype.dll", CallingConvention = CallingConvention.Cdecl)>]
extern int FT_Init_FreeType(HANDLE&)

Once you've declared one function with the full path to the DLL, subsequent declarations will use this as well. FreeType is using standard C calling convention so we also specify that in the declaration. Notice that the parameter is HANDLE&. In FreeType, this is an output parameter and so we must pass in a pointer that will be pointed at the HANDLE for the FreeType library. Finally FT_Error is just an int.

Translating the structures/records used in FreeType was a bit laborious. So much so that I skipped a bunch of the private ones in the Face structure. As the structs in C are sequentially placed in memory this isn't a big deal.

Once I got the hang of it though, churning these out wasn't too bad.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
[<Struct; StructLayout(LayoutKind.Sequential)>]
type GlyphSlot =
  val library: HANDLE
  val face: HANDLE // Pointer to Face
  val next: HANDLE // Pointer to GlyphSlot
  val glyph_index: uint32
  val generic: Generic
  val metrics: GlyphMetrics

  val linearHoriAdvance: int32
  val linearVertAdvance: int32
  val advance: Vector

  val format: uint32

  val bitmap: Bitmap
  val bitmap_left: int32
  val bitmap_top: int32

The structs are nested, some with pointers to other structs. The best way I found of managing this was to define them as HANDLEs and then marshal the pointers separately as was done with the top-level calls e.g. FT_New_Face.

1: 
2: 
3: 
4: 
let fontpath = __SOURCE_DIRECTORY__ + "/../lib/font.ttf"
let mutable facePtr = HANDLE 0
FT_New_Face(library, fontpath, 0, &facePtr)
let face = Marshal.PtrToStructure<Face>(facePtr)

To load the entire font (well characters 32-128) it's necessary to allocate an array for each of the glyphs and copy in the data from the un-marshalled FreeType structure.

1: 
2: 
3: 
4: 
let bufferSize = int (bitmap.width * bitmap.rows)
let buffer = Array.zeroCreate<byte> bufferSize

Marshal.Copy(bitmap.buffer, buffer, 0, int bufferSize)

I defined my own Character record containing the bitmap data and metrics needed to process the glyphs and returned these along with the atlas size, width and height needed to contain them all, as this is needed to store them in a bitmap.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
type Character = {
  offset: float32
  data: byte[]
  width: uint32
  height: uint32
  bearingX: int
  bearingY: int
  advance: int
}

I did this for all the different font sizes I wanted. Then passed it over to OpenGL side to render it to a texture.

Creating the OpenGL font texture atlas

On the OpenGL side I set some GL.TexParameter options as per the LearnOpenGL tutorial before calling GL.TexImage2D to allocate the memory for the atlas/sprites.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
gl.TexImage2D(
  GLEnum.Texture2D,
  0,
  int GLEnum.Red,
  uint32 atlasWidth,
  uint32 atlasHeight,
  0,
  GLEnum.Red,
  GLEnum.UnsignedByte,
  IntPtr.Zero.ToPointer()
)

This texture just uses a single colour channel to store the data as we only need the alpha channel (how visible the pixel is) and are free to set the color ourselves.

The last parameter of the call above is normally for the data. If the data is to be filled in later, as in our case, you can pass in 0. Being an F# and .NET novice I wasn't sure how to pass 0 as an argument that required a void pointer. This time Google wasn't able to come to my aid but a quick post on Stack Overflow gave me the answer I was looking for.

I was then able to iterate over the Character records I created earlier to copy the data into the texture.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
let dummyFunc =
  use dataPtr = fixed character.data
  let dataVoidPtr = dataPtr |> NativePtr.toVoidPtr

  gl.TexSubImage2D(
    GLEnum.Texture2D,
    0,
    x,
    0,
    character.width,
    character.height,
    GLEnum.Red,
    GLEnum.UnsignedByte,
    dataVoidPtr
  )

Rendering to OpenGL

The final step, as in the tutorial, is to draw some quads for each character in a string of text with the correct texture coordinates set to display the right glyph from the texture atlas.

My render function is passed a context containing the Character record along with all the GL info needed to create the text (ie. program, shader, VAO, VBO).

I had some fun with inverted texture coordinates but overall it was a fairly painless experience.

I'll spare you the details as the rest is fairly similar to the original C tutorial.

namespace System
namespace System.Runtime
namespace System.Runtime.InteropServices
[<Struct>]
type HANDLE = nativeint
Multiple items
val nativeint : value:'T -> nativeint (requires member op_Explicit)

--------------------
[<Struct>]
type nativeint = System.IntPtr

--------------------
type nativeint<'Measure> =
  nativeint
Multiple items
type DllImportAttribute =
  inherit Attribute
  new : dllName: string -> unit
  val BestFitMapping : bool
  val CallingConvention : CallingConvention
  val CharSet : CharSet
  val EntryPoint : string
  val ExactSpelling : bool
  val PreserveSig : bool
  val SetLastError : bool
  val ThrowOnUnmappableChar : bool
  ...

--------------------
DllImportAttribute(dllName: string) : DllImportAttribute
type CallingConvention =
  | Winapi = 1
  | Cdecl = 2
  | StdCall = 3
  | ThisCall = 4
  | FastCall = 5
field CallingConvention.Cdecl: CallingConvention = 2
Multiple items
val int : value:'T -> int (requires member op_Explicit)

--------------------
[<Struct>]
type int = int32

--------------------
type int<'Measure> =
  int
val FT_Init_FreeType : byref<HANDLE> -> int
Multiple items
type StructAttribute =
  inherit Attribute
  new : unit -> StructAttribute

--------------------
new : unit -> StructAttribute
Multiple items
type StructLayoutAttribute =
  inherit Attribute
  new : layoutKind: int16 -> unit + 1 overload
  val CharSet : CharSet
  val Pack : int
  val Size : int
  member Value : LayoutKind

--------------------
StructLayoutAttribute(layoutKind: int16) : StructLayoutAttribute
StructLayoutAttribute(layoutKind: LayoutKind) : StructLayoutAttribute
type LayoutKind =
  | Sequential = 0
  | Explicit = 2
  | Auto = 3
field LayoutKind.Sequential: LayoutKind = 0
[<Struct>]
type GlyphSlot =
  val library: HANDLE
  val face: HANDLE
  val next: HANDLE
  val glyph_index: uint32
  val generic: obj
  val metrics: obj
  val linearHoriAdvance: int32
  val linearVertAdvance: int32
  val advance: obj
  val format: uint32
  ...
GlyphSlot.library: HANDLE
GlyphSlot.face: HANDLE
GlyphSlot.next: HANDLE
GlyphSlot.glyph_index: uint32
Multiple items
val uint32 : value:'T -> uint32 (requires member op_Explicit)

--------------------
[<Struct>]
type uint32 = System.UInt32

--------------------
type uint32<'Measure> = uint<'Measure>
GlyphSlot.generic: obj
GlyphSlot.metrics: obj
GlyphSlot.linearHoriAdvance: int32
Multiple items
val int32 : value:'T -> int32 (requires member op_Explicit)

--------------------
[<Struct>]
type int32 = System.Int32

--------------------
type int32<'Measure> = int<'Measure>
GlyphSlot.linearVertAdvance: int32
GlyphSlot.advance: obj
GlyphSlot.format: uint32
GlyphSlot.bitmap: obj
GlyphSlot.bitmap_left: int32
GlyphSlot.bitmap_top: int32
val fontpath : string
val mutable facePtr : HANDLE
val face : obj
type Marshal =
  static member AddRef : pUnk: nativeint -> int
  static member AllocCoTaskMem : cb: int -> nativeint
  static member AllocHGlobal : cb: int -> nativeint + 1 overload
  static member AreComObjectsAvailableForCleanup : unit -> bool
  static member BindToMoniker : monikerName: string -> obj
  static member ChangeWrapperHandleStrength : otp: obj * fIsWeak: bool -> unit
  static member CleanupUnusedObjectsInCurrentContext : unit -> unit
  static member Copy : source: byte [] * startIndex: int * destination: nativeint * length: int -> unit + 15 overloads
  static member CreateAggregatedObject : pOuter: nativeint * o: obj -> nativeint + 1 overload
  static member CreateWrapperOfType : o: obj * t: Type -> obj + 1 overload
  ...
Marshal.PtrToStructure<'T>(ptr: nativeint) : 'T
Marshal.PtrToStructure<'T>(ptr: nativeint, structure: 'T) : unit
Marshal.PtrToStructure(ptr: nativeint, structureType: System.Type) : obj
Marshal.PtrToStructure(ptr: nativeint, structure: obj) : unit
val bufferSize : int
val buffer : byte []
module Array

from Microsoft.FSharp.Collections
val zeroCreate : count:int -> 'T []
Multiple items
val byte : value:'T -> byte (requires member op_Explicit)

--------------------
[<Struct>]
type byte = System.Byte

--------------------
type byte<'Measure> =
  byte
Marshal.Copy(source: float32 [], startIndex: int, destination: nativeint, length: int) : unit
   (+0 other overloads)
Marshal.Copy(source: nativeint [], startIndex: int, destination: nativeint, length: int) : unit
   (+0 other overloads)
Marshal.Copy(source: nativeint, destination: float32 [], startIndex: int, length: int) : unit
   (+0 other overloads)
Marshal.Copy(source: nativeint, destination: nativeint [], startIndex: int, length: int) : unit
   (+0 other overloads)
Marshal.Copy(source: nativeint, destination: int64 [], startIndex: int, length: int) : unit
   (+0 other overloads)
Marshal.Copy(source: nativeint, destination: int [], startIndex: int, length: int) : unit
   (+0 other overloads)
Marshal.Copy(source: nativeint, destination: int16 [], startIndex: int, length: int) : unit
   (+0 other overloads)
Marshal.Copy(source: nativeint, destination: float [], startIndex: int, length: int) : unit
   (+0 other overloads)
Marshal.Copy(source: nativeint, destination: char [], startIndex: int, length: int) : unit
   (+0 other overloads)
Marshal.Copy(source: nativeint, destination: byte [], startIndex: int, length: int) : unit
   (+0 other overloads)
type Character =
  { offset: float32
    data: byte []
    width: uint32
    height: uint32
    bearingX: int
    bearingY: int
    advance: int }
Character.offset: float32
Multiple items
val float32 : value:'T -> float32 (requires member op_Explicit)

--------------------
[<Struct>]
type float32 = System.Single

--------------------
type float32<'Measure> =
  float32
Character.data: byte []
Character.width: uint32
Character.height: uint32
Character.bearingX: int
Character.bearingY: int
Character.advance: int
val dummyFunc : obj