Skip to content
Heiko Brumme edited this page Feb 4, 2015 · 3 revisions

This tutorial shows you how to draw fonts in OpenGL, for this we will use the AWT Font class for loading a TrueType font and finally render it dynamically. For this tutorial it is good if you have already read the texture tutorial.

Text rendering in OpenGL

You may think that text rendering is some basic functionality, but it is a high-level function that does not belong to a low-level library like OpenGL.
For rendering text there are many methods how to do it. These two approaches are the most common:

  • Draw text on a BufferedImage and create a texture out of it
    • It's really simple to do, but you would need to create a texture for every text you want to write
  • Create a texture atlas with every glyph of a font
    • This approach is more difficult but you can dynamically write text with creating only one texture

For beginners the first approach is pretty straight-forward, but with that method you can only create static text that is difficult to edit, so most of the time it is a texture that is used just once.
Because of that we will take a look at the second approach, since this is the more efficient way to go.

Creating a glyph texture atlas

Creating a texture atlas with every glyph contains a few steps, first you have to load the font, after that you determine width and height of the texture and finally you draw the glyphs on an image before generating a texture out of it.

Loading a TrueType font

In Java loading a font is pretty straight forward with java.awt.Font you can do it either by loading a font file with an InputStream or by simply calling the constructor with the name of a font family.

java.awt.Font font = new java.awt.Font(MONOSPACED, PLAIN, 16);

With that code we load a default monospaced font with a point size of 16. In case to wonder, MONOSPACED and PLAIN are static imports from java.awt.Font. Of course yout could load any other font if you want to.
If you want to load from a file you should use java.awt.Font.createFont(fontFormat, fontStream) if you load with an InputStream or java.awt.Font.createFont(fontFormat, fontFile) if you want to load with the File class. The fontFormat should be TRUETYPE_FONT. After loading a font like this the point size is 1 and the style is PLAIN. For varying size and style you should then use deriveFont(style, size) on that font.

Determine total width and height

Now that the font is loaded we can start with determining the width and height for the font, for that we need to iterate through the ASCII chars we want in our texture. For the standard characters we iterate through ASCII char #32 to #256, we can omit ASCII #0 to #31 because they are just control codes, we can also skip ASCII #127 because it is the DEL control code. It would also be a good idea to save the font height because you need it a few times.

int imageWidth = 0;
int imageHeight = 0;

for (int i = 32; i < 256; i++) {
    if (i == 127) {
        continue;
    }
    char c = (char) i;
    BufferedImage ch = createCharImage(font, c, antiAlias);

    imageWidth += ch.getWidth();
    imageHeight = Math.max(imageHeight, ch.getHeight());
}

fontHeight = imageHeight;

That loop should be clear enough, the important part about this is in the createCharImage(font, c antiAlias) method.
The first thing there is to get the font metrics, we get them by creating a temporary image where we set the font to the one we want to put on the texture.
After that we just create a Graphics2D object from the image and pull the font metrics from it.

BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
if (antiAlias) {
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
g.setFont(font);
FontMetrics metrics = g.getFontMetrics();
g.dispose();

If we want to have an antialiased font we set the rendering hints according to it. But that depends on the font you want to use, some fonts are good enough without antialiasing.
With the font metric we can just extract the char width and height.

int charWidth = metrics.charWidth(c);
int charHeight = metrics.getHeight();

After doing so we can create a new image with that width and height and return it for now, but since that method should also create a image with the char we draw the char on the image before we return it. For drawing we use the color white, so we can change the text color during rendering.

image = new BufferedImage(charWidth, charHeight, BufferedImage.TYPE_INT_ARGB);
g = image.createGraphics();
if (antiAlias) {
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
g.setFont(font);
g.setPaint(Color.WHITE);
g.drawString(String.valueOf(c), 0, metrics.getAscent());
g.dispose();
return image;

Well back to our method for creating the texture, now that we have the font height and the sum of the width of each char we can create a image with that width and height.

BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();

Generate the texture

Before we start generating the texture we should create a simple class for the glyphs so that we can save width and height of each glyph and also were you can find it on the texture.

public class Glyph {
    public final int width;
    public final int height;
    public final int x;
    public final int y;

    public Glyph(int width, int height, int x, int y) {
        this.width = width;
        this.height = height;
        this.x = x;
        this.y = y;
    }
}

In our Font class we need to map the chars to its specific glyph, so we make a Map for it, you may also want to use a simple array.

private Map<Character, Glyph> glyphs = new HashMap<>();

Great, now we have everything to create our texture atlas, like before we iterate through the standard ASCII characters to get the char image. We also need an offset x for creating the Glyph object.

if (i == 127) {
    continue;
}
char c = (char) i;
BufferedImage charImage = createCharImage(font, c, antiAlias);

int charWidth = charImage.getWidth();
int charHeight = charImage.getHeight();

With the x and the char image we create the glyph, it should be noted, that for the y value we take the image height minus the char height because at the end we will flip the image vertically to get the origin to the bottom left. Then we draw the char image on the texture image and increase the x offset by the char width and put the glyph in our Map.

Glyph ch = new Glyph(charWidth, charHeight, x, image.getHeight() - charHeight);
g.drawImage(charImage, x, 0, null);
x += ch.width;
glyphs.put(c, ch);

After the loop is finished we use the AffineTransform operation from the texture tutorial to flip the image vertically and put the pixel data inside a ByteBuffer before generating a texture handle and uploading the pixel data to the GPU.

Rendering the text

For drawing the text we will use the filled Map to get the right texture coordinates, but before doing that we need to know the height of the text, in case it has multiple lines and since we have saved the font height it can be easily calculated by iteration through the chars.

int lines = 1;
for(int i = 0; i < text.length(); i++) {
    char ch = text.charAt(i);
    if(char == '\n') {
        lines++;
    }
}
int textHeight = lines * fontHeight;

That text height is important, because traditionally the origin of an OpenGL window is at the bottom left, so to draw the lines in the right order we have to start at the highest position.
So for drawing at position (x,y) we check if the text height is greater than the font height, in that case we know we have more than one line.

float drawX = x;
float drawY = y;
if(textHeight > fontHeight) {
    drawY += textHeight - fontHeight;
}

Now to the tricky part, after determining the coordinates we start the rendering process, then we simply go over each char and draw it with the help of the stored Glyph.

texture.bind();
renderer.begin();
for (int i = 0; i < text.length(); i++) {
    char ch = text.charAt(i);
    if (ch == '\n') {
        /* Line feed, set x and y to draw at the next line */
        drawY -= fontHeight;
        drawX = x;
        continue;
    }
    if (ch == '\r') {
        /* Carriage return, just skip it */
        continue;
    }
    Glyph g = glyphs.get(ch);
    renderer.drawTextureRegion(texture, drawX, drawY, g.x, g.y, g.width, g.height, c);
    drawX += g.width;
}
renderer.end();

For now you should just know that this is actually done with batch rendering, but we will look at this in the next tutorial.


Source

References