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#.
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:
|
|
HANDLE
s are used a lot for pointers in FreeType so I declared a type alias to match:
1:
|
|
The function definitions are fairly straightforward. For example, initializing FreeType is done with the C function:
1:
|
|
The F# declaration for this would be:
1: 2: |
|
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 struct
s 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: |
|
The struct
s are nested, some with pointers to other struct
s. The best way I
found of managing this was to define them as HANDLE
s and then marshal the pointers
separately as was done with the top-level calls e.g. FT_New_Face
.
1: 2: 3: 4: |
|
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: |
|
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: |
|
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: |
|
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: |
|
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.
type HANDLE = nativeint
val nativeint : value:'T -> nativeint (requires member op_Explicit)
--------------------
[<Struct>]
type nativeint = System.IntPtr
--------------------
type nativeint<'Measure> =
nativeint
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
| Winapi = 1
| Cdecl = 2
| StdCall = 3
| ThisCall = 4
| FastCall = 5
val int : value:'T -> int (requires member op_Explicit)
--------------------
[<Struct>]
type int = int32
--------------------
type int<'Measure> =
int
type StructAttribute =
inherit Attribute
new : unit -> StructAttribute
--------------------
new : unit -> StructAttribute
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
| Sequential = 0
| Explicit = 2
| Auto = 3
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
...
val uint32 : value:'T -> uint32 (requires member op_Explicit)
--------------------
[<Struct>]
type uint32 = System.UInt32
--------------------
type uint32<'Measure> = uint<'Measure>
val int32 : value:'T -> int32 (requires member op_Explicit)
--------------------
[<Struct>]
type int32 = System.Int32
--------------------
type int32<'Measure> = int<'Measure>
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, structure: 'T) : unit
Marshal.PtrToStructure(ptr: nativeint, structureType: System.Type) : obj
Marshal.PtrToStructure(ptr: nativeint, structure: obj) : unit
from Microsoft.FSharp.Collections
val byte : value:'T -> byte (requires member op_Explicit)
--------------------
[<Struct>]
type byte = System.Byte
--------------------
type byte<'Measure> =
byte
(+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)
{ offset: float32
data: byte []
width: uint32
height: uint32
bearingX: int
bearingY: int
advance: int }
val float32 : value:'T -> float32 (requires member op_Explicit)
--------------------
[<Struct>]
type float32 = System.Single
--------------------
type float32<'Measure> =
float32