OpenGL ES SDK for Android ARM Developer Center
High Quality Text Rendering

Improving quality for textured text.

# Introduction

The source for this sample can be found in the folder of the SDK.

Preserving the highest possible quality text in real-time 3D graphics is challenging. Objects may change their position, rotation, scale and viewing angle dynamically. All these have a negative impact on quality because text is usually generated once, not in every frame. Generating a texture for the whole text takes a long time depending on the font engine and its performance. Usually this amount of time is enough to affect performance.

This document presents an approach of how to achieve the best possible quality of text when the object is semi-dynamic. A semi-dynamic object is an object which is being changed neither too often (not every frame) nor during animation time.

This sample describes how to calculate font size, which should give a close matching of texels onto screen pixels.

We are going to use the font engine which is part of Android. The font engine produces an RGBA image which contains the shape of the whole text. Then the image is uploaded into a texture and next the texture is mapped on a rectangle. The rectangle geometry must have an appropriate aspect ratio defined according to the texture size.

# Evaluating A Font Size

To evaluate the font size for a current transformation of the object we need to have four corners of a rectangle transformed from 3D world space into 2D pixel screen space. Having corners expressed in pixels we can calculate the distance between two left corners and the distance between two right corners. Then an average value is being calculated from those distances. The average value is what we are looking for, as this is the size of the font we are going to use to generate the image.

// 1. Calculate bounding box in screen coordinates with current matrices
Vector4f cLT = new Vector4f(-0.5f,-0.5f, 0.0f, 1.0f);
Vector4f cLB = new Vector4f(-0.5f, 0.5f, 0.0f, 1.0f);
Vector4f cRT = new Vector4f( 0.5f,-0.5f, 0.0f, 1.0f);
Vector4f cRB = new Vector4f( 0.5f, 0.5f, 0.0f, 1.0f);
// Instead of calculating matrices again lets reuse ones which were already calculated
// for rendering purpose. One important thing is the update() method must be called
// after render() method
cLT.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);
cLB.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);
cRT.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);
cRB.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);
// 2. Evaluate font size based on the height of bounding box corners
Vector4f vl = Vector4f.sub(cLB, cLT);
Vector4f vr = Vector4f.sub(cRB, cRT);
textSize = (vl.length3() + vr.length3()) / 2.0f;

Below is a definition of the makePixelCoords method from the Vector4f class. The method transforms a 3D vertex position onto a 2D pixel position.

public void makePixelCoords(float[] aMatrix,
int aViewportWidth,
int aViewportHeight) {
// Transform the vector into screen coordinates we assumes aMatrix is ModelViewProjection matrix
// transform method multiplies this vector by the matrix
transform(aMatrix);
// Make coordinates as homogenous
x /= w;
y /= w;
z /= w;
w = 1.0f;
// Now the vector is normalized to the range [-1.0, 1.0]
// Normalize values into NDC.
x = 0.5f + x * 0.5f;
y = 0.5f + y * 0.5f;
z = 0.5f + z * 0.5f;
w = 1.0f;
// Currently the valuse are clipped to the [0.0, 1.0] range
// Move coordinates into window space (in pixels)
x *= (float) aViewportWidth;
y *= (float) aViewportHeight;
}

# Texture Generation

As we already know the font size we can estimate a size for our destination image. The image has to be large enough to store the whole text without any cuts. On the other hand it cannot be too big because the following geometry calculations are based on the image size. We want to have a size fitted precisely to the content the font engine is going to produce.

The height calculation is simple because this is the size of the font, but such a width is very complex. To calculate the width properly we need to use the font engine to help us estimate it. Android Java SDK comes with the measureText method from the Paint object. Before the measurement happens we need to deliver all necessary data to the object like: name of a font, size of a font (which we already calculated), anti-aliasing, color ARGB (in our case it is always white because coloring may be done in the fragment shader later on), and other less important data.

Before we paint the text into a Bitmap object we need to clear its content using white color with an alpha entirely transparent ARGB = (0, 255, 255, 255). Having the background cleared with this color and Paint color set to white as well, prevents dark texels which might appear by alpha blending. If we are talking about blending it is important to mention about GL blending function which must be set properly before the text is rendered. The blend function must be set as: `glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)`

The function below does all the steps mentioned above:

private void drawCanvasToTexture(
String aText,
float aFontSize) {
if (aFontSize < 8.0f)
aFontSize = 8.0f;
if (aFontSize > 500.0f)
aFontSize = 500.0f;
Paint textPaint = new Paint();
textPaint.setTextSize(aFontSize);
textPaint.setFakeBoldText(false);
textPaint.setAntiAlias(true);
textPaint.setARGB(255, 255, 255, 255);
// If a hinting is available on the platform you are developing, you should enable it (uncomment the line below).
//textPaint.setHinting(Paint.HINTING_ON);
textPaint.setSubpixelText(true);
textPaint.setXfermode(new PorterDuffXfermode(Mode.SCREEN));
float realTextWidth = textPaint.measureText(aText);
// Creates a new mutable bitmap, with 128px of width and height
bitmapWidth = (int)(realTextWidth + 2.0f);
bitmapHeight = (int)aFontSize + 2;
Bitmap textBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
textBitmap.eraseColor(Color.argb(0, 255, 255, 255));
// Creates a new canvas that will draw into a bitmap instead of rendering into the screen
Canvas bitmapCanvas = new Canvas(textBitmap);
// Set start drawing position to [1, base_line_position]
// The base_line_position may vary from one font to another but it usually is equal to 75% of font size (height).
bitmapCanvas.drawText(aText, 1, 1.0f + aFontSize * 0.75f, textPaint);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0]);
HighQualityTextRenderer.checkGLError("glBindTexture");
// Assigns the OpenGL texture with the Bitmap
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, textBitmap, 0);
// Free memory resources associated with this texture
textBitmap.recycle();
// After the image has been subloaded to texture, regenerate mipmaps
GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
HighQualityTextRenderer.checkGLError("glGenerateMipmap");
}

# Further Improvements

• If the text is being changed frequently in your program this concept might fit that as well. We can suggest creating a separate thread which is going to update the texture continuously with some interval. In most cases it would be mostly desired to keep the thread on lowest possible priority since generating text is always considered as heavyweight operation which is more likely cause a disruption in performance. Needless to say that updating a texture should be done on the thread which serves the GL context.
• If you are rendering the text along a Bezier curve or you are doing some displacements, you need to have size of font estimated more precisely. For this purpose increase rectangle resolution horizontally. At this point the rectangle will be split into vertical slices. Then evaluate average height of all these slices. The average height value is used to increase precision of the font size.