Day 22: Fonts and Text – Part 2

Introduction

Last day we started creating an atlas-based font class to draw text. Today we are going to complete this work by using it to draw characters. In order to draw a piece of text on the screen, we need the following information.

  • The text (a series of characters)
  • Screen coordinates to draw the text
  • Font
  • Font size (and possibly other attributes)

Given font size, we need to scale the atlas texture accordingly before drawing characters. What we introduce later today are text sprites. Like other sprites in our framework, text sprites can be rotated, scaled, and translated. For ease of use, we associate the scaling factor of a text sprite to its font size. For instance if the scaling factor is 12, it represents a text with a font height of 12 pixels. This contrasts with a normal texture, where it means 12 times as big as the original size. In day 6 we discussed how transformations work in OpenGL ES. In order to figure out how we should apply transformations to text sprites, we have to keep the following in mind:

  • Rotation should be applied to the text as a whole, not individual characters.
  • Scaling can either be applied to the whole text or to individual characters.
  • Translation is applied to the whole text as well as individual characters. Note that we have to position the text first, and then every character should appear after the previous.

From day 6 we know that global translation should always be the last thing to be applied. Otherwise translation will be a function of other transformations, and will not be applied the way we expect it. Scaling is the next in line, which fulfills font sizing. Local (type-writer style) translation should be done after scaling for each character. Rotation comes last, just before global translation, since we want the whole text to rotate. And we should keep in mind that in OpenGL ES (and any other matrix-based transformation environment) transformations are applied in the reverse order of our calls (or multiplications in case of matrices). So, here is a sketch of the order of doing things:

  1. Call glTranslatef for global translation.
  2. Call glRotatef for rotation.
  3. For each character:
    1. Call glTranslatef for per-character translation
    2. Call glScalef for character scaling
    3. Revert after drawing the characters

Figure below shows what happens in each of these steps:

Text Transformations

Drawing text

As it stands by now, our Font class can read character information and graphics from a special-purpose SVG file, and create texture mappings (through a method called buildTextureMapping()). Today we use what we have done to implement a draw method. According to the previous section, this method should take the position, and size information of the text to be drawn along with the text itself. Since rotation is also handled differently in text than in normal sprites, it is the job of our Font class to handle it. So, we are looking at this prototype:

public void draw(char[] ca, float sx, float sy, float h, float rot) {
}

The first parameter is the string to be drawn. We pass strings as character arrays, as it is faster to work with arrays than Strings. Strings can easily be converted to character arrays using their toCharArray() method. sx and sy indicate the location of the first character, and h indicates the height of the font (in pixels). We also pass the rotational angle of the final text for reasons we just discussed. The steps we discussed in the previous section will look like this in code:

public void draw(char[] ca, float sx, float sy, float h, float rot) {
	// Calculate scaling factor
	final float sz = h / charH;

	// Scale horizontal spacing with sz
	final float hSpacing = hspacing * sz;

	// Same as normal textures
        GLES10.glEnable(GL10.GL_TEXTURE_2D);
        GLES10.glBindTexture(GL10.GL_TEXTURE_2D, textureId);
        GLES10.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

	GLES10.glPushMatrix();

	// Global translation
	GLES10.glTranslatef(sx, sy, 0);

	// Global rotation
	GLES10.glRotatef(rot, 0, 0, 1);

	float currentX = 0;

	for (char c : ca) {
		// Calculate the array index for the character
		c -= firstChar;

		// Calculate character width according to its height
		float w = (offsets[c * 2 + 1] - offsets[c * 2]) * sz;

		GLES10.glPushMatrix();
		// Local translation
		GLES10.glTranslatef(currentX + w / 2, 0, 0);

		// Set up the rectangle with character width and height (local scaling)
		GLES10.glScalef(w, h, 0);

		// Draw the desired character
		GLES10.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureCoords[c]);
		GLES10.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
		GLES10.glPopMatrix();

		// Calculate x coordinate for next character
		currentX += w + hSpacing;
	}

	GLES10.glPopMatrix();
}

The code above might look a little complicated at first sight, but it is simpler than it looks. It simply loops through all characters and does what we discussed. I have added descriptive comments to make it better understandable. An important thing is missing here. As you can see, we use textureCoords without ever creating it. This can be fixed by calling buildTextureMapping from previous day. So, add the following two lines to the beginning of the method:

	if(textureCoords == null)
		buildTextureMapping();

Handling the space character

Fonts in Artenus can specify cutouts for any character, and this includes space. But the truth is, it is a waste of memory to allocate coordinates for white space. We can assume a specific width for the space character, and just push forward that much each time we see it in the text. Here we assume each space is h / 3 wide, with h being the selected character height. We can then add the following code first thing in the loop:

if (c == ' ') {
	currentX += h / 3;
	continue;
}

If current character is not space, we will simply fall through the rest of the loop, and handle it normally.

Handling new line

New line is a little bit trickier. Up to now we only worked on one axis. New line introduces the second axis. So, this suggest we need a variable such as currentY beside currentX:

float currentX = 0, currentY = 0;

This goes for everything else as well, namely vertical spacing:

final float hSpacing = hspacing * sz, vSpacing = vspacing * sz;

And also for per-character translation:

// Previously: GLES10.glTranslatef(currentX + w / 2, 0, 0);
GLES10.glTranslatef(currentX + w / 2, currentY, 0);

Now that everything is migrated to two dimensions, the next step is to put it into use. What a new character (‘\n’) does is to move the cursor to the beginning of the next line. This means increasing currentY by h plus vertical spacing, and setting currentX to 0. Again, this should be checked and handled before we fall through our normal operation.

if (c == '\n') {
	currentX = 0;
	currentY += (h + vSpacing);
	continue;
}

Changes to TextureManager

Font is a subclass of textures and its instances can be added to TextureManager’s set of textures just like any other. But in order to get Font functionality from them, we have to instantiate the subclass, and this is not what TextureManager‘s add method does. There are elegant ways of solving this issue which we will discuss later in this tutorial, but a quick fix for now is to add another method for adding fonts:

public static void addFont(int resourceId) {
	state = STATE_LOADING;
	Font tex = new Font(resourceId);
	texMap.put(resourceId, tex);
	texList.add(tex);
	state = STATE_FRESH;
}

Text sprite

Like an ordinary texture, Font cannot be directly drawn in a Scene. The only items that can be added to a scene are sprites. In this section we implement TextSprite, a special kind of sprite that uses fonts to draw text. A text sprite takes the desired font, character height, and a string:

public class TextSprite extends Sprite {
	public TextSprite(Font font, int charHeight, String initialText) {
		super();
		myFont = font;
		setText(initialText);
		setScale(charHeight, charHeight);
	}

	public TextSprite(Font font, int charHeight) {
		this(font, charHeight, "");
	}

	private Font myFont;
	private char[] ca;
}

As you can notice, we have another helper constructor for when we want to defer setting a text for the sprite. You might also notice that we use a method called setText. Since we use character arrays internally, we encapsulate the text so the outside view is string-based.

public final void setText(String value) {
	if (value != null)
		ca = value.toCharArray();
	else ca = new char[0];
}
public final String getText() {
	return new String(ca);
}

One might argue that using character arrays internally and strings externally introduces a conversion overhead each time we get or set text. The justification behind this is that setting and getting the text usually does not happen too often, while rendering happens once per frame. This code is optimized for rendering. So, why do we get and set string? The reason is simple. We are developing a framework, and the framework’s interface with the outside world should be as intuitive as possible. Text is normally represented as String, and that’s how the developer expects to provide it to a text sprite.

It is in order to also encapsulate Font, so it can be changed later:

public final void setFont(Font font) {
	myFont = font;
}
public final Font getFont() {
	return myFont;
}

Although this is not necessary (we can just make myFont public), it provides an abstraction of interface and logic. For example, maybe in a later version we need to perform some processing on the font before assigning it. Should that be the case, encapsulation makes sure it will be compatible with current applications of the class.

The main part of every sprite is its implementation of the abstract method render. Having implemented the Font class, this method is already implemented!

@Override
public final void render(GL10 gl) {
	myFont.draw(ca, pos.x, pos.y, scale.x, rotation);
}

Text color

If you use TextSprite as is, you will have one problem. The color of the text will always be what it is in the SVG file. This means you always have to write with the same color, or include several fonts, one for each color you desire. However, we choose a third option! We use the basic concept of vertex colors to draw text. If you have glanced at any OpenGL or DirectX quick start guide, there is always a beautifully colored triangle like this:

Colored Triangle

In the above picture, each vertex is given a different color. Normally vertex colors are used when textures are not. But if we use them with texture, they have an interesting effect. Instead of painting the triangle (or rectangle in our case), they are multiplied by the colors of the texture (component by component). So, if the texel itself is white (1.0, 1.0, 1.0), the vertex color determines the display color.

Above is the idea behind our implementation of text color. First, we set a rule that all fonts must be in grayscale, and the main areas in each character should be completely white. Note that there can be borders, shadows, or other artifacts that are not white, but gray or black. Next, we define text color in our class:

public final void setColor(RGB rgb) {
	color.r = rgb.r;
	color.g = rgb.g;
	color.b = rgb.b;
}
public final RGB getColor() {
	return color;
}

private final RGB color = new RGB(1, 1, 1);

Now we need to set vertex color to the color field for all pixels, that is, before drawing the text. We should also remember to reset it once drawing is complete. So, the render method will change as follows:

@Override
public final void render(GL10 gl) {
	gl.glColor4f(alpha * color.r, alpha * color.g, alpha * color.b, alpha);
	myFont.draw(ca, pos.x, pos.y, scale.x, rotation);
	gl.glColor4f(1, 1, 1, 1);
}

Next steps

Today we finished our text drawing functionality. The graphical subsystem is almost complete. What we do next is to organize what we did so far, and create a more practical framework based on what we have learned. As mentioned before, part 4 of the tutorial focuses more on design concepts and less on OpenGL ES. You may download today’s code below. I have also added a bonus font file for you to play with. You can also open it with a text editor and see how font information is stored.