Tutorial 20 : 2D Font [Part II] & Particle System

Tutorial Advanced Download Section

In previous font tutorials, we have seen how to use some predefined fonts. Here, we will use our own custom font stored in a texture.

The idea of this tutorial is to render font using a texture which contains a list of characters. A string will be rendered by rendering character by character. Each character is rendered by drawing a textured quad, using the texture region associated of the character.

The advantage of this technique is that you can design your own fonts and used them in a 3D scene. The font picture can be generated either manualy or with a generator using system font.

This next tutorial about OpenGl components will use our font printer to render component's text.
 

Suppose that you have a picture in which you have a collection of characters :


Font picture

Each characters is positionned at a particular location and a know dimension. Later, we will see how to determine the character region.
The picture can be divided in a bi-dimensiobal array, made of rows and lines, like shown in the next picture :


Picture subdivisions

Know the associated character location (row, line), we can easily associate its region.
 

We will see here how to determinate the character positions row and line.

The simple way is to use the ASCII code. The ascii code table associate an integer in the range [0, 256] to a character.
We will use the associated ASCII code of the character to determinate its location.

The ASCII code contains 256 characters. The 32 first characters are not used here (characters not representable). We will use only the 224 last characters. Those 224=16*14 characters can be represented by a bi-dimensional grid 16 x 14.
The corresponding font picture will contains 16 rows and 14 lines of characters. The character at (0,0) have the ASCII code 32, and on ...

In the next picture, I've represented the character grid. Each grid elements contains the associated characters ASCII code.


Character position

We can map easily a character c to a (row, line) position in the grid with :
    row  = (c - 32) % 16
    line = (c - 32) / 16
 

The interface for rendering text on the screen is simple. It is composed of an enumeration, used for the font alignement, and two overloaded methods which have the aim to render a String on the screen.

FontRenderer interface


public enum FontAlign
{
    TopLeft, TopCenter, TopRight,
       Left,    Center,    Right,
    BotLeft, BotCenter, BotRight;
}

public void drawText(String text);
public void drawText(String text, FontAlign align);

public void drawTextAt(String text, float x, float y, float z);
public void drawTextAt(String text, FontAlign align, float x, float y, float z);
 

 

The picture used here is a 256x256 picture (in pixels). This picture contains 256 characters shared in 16 rows and 14 lines. The character in the first rectangle have the code 32.

The region of a character can be represented by the coordinates of his top-left point. Effectivly, we know its size as we know the texture size and the number of character per rows and lines. So, the characters region size is (16, 256/14) = (charWidth, charHeight).

First, we convert the ASCII code of a character to the location (row, line) like seen previously. Then we calculate the upper left point of the character region :

ASCII code to location of top-left point

    //Convert the ascii code into the position in the 16*14 array
    int line = (character-32)/16;
    int row  = (character-32)%16;
   
    //convert the position of the fictitious array into the pixel position in the picture ( (0,0) is top left pixel)
    Point topLeft = new Point(row*charWidth, 256-line*charHeight);

Now, we can render the character as a quad with the following code :

Render a character

    //Render the character with a texture quad
    gl.glBegin(GL.GL_QUADS);
        gl.glTexCoord2f( topLeft.x       /textureWidth,  topLeft.y        /textureHeight);
        gl.glVertex2i(0,         charHeight);
       
        gl.glTexCoord2f( topLeft.x       /textureWidth, (topLeft.y-height)/textureHeight);
        gl.glVertex2i(0,         0);
       
        gl.glTexCoord2f((topLeft.x+width)/textureWidth, (topLeft.y-height)/textureHeight);
        gl.glVertex2i(charWidth, 0);
       
        gl.glTexCoord2f((topLeft.x+width)/textureWidth,  topLeft.y        /textureHeight);
        gl.glVertex2i(charWidth, charHeight);
    gl.glEnd();

Note: Don't forgot the lower left corner of the texture is (0, 0).
 

We have seen the basis to render a character, know we will render the full String.

To quickly explain the code, it is composed of 3 parts :
 - We enable and set-up the OpenGL states to render the text (texture, blending ...).
 - Then, we calculate the alignement offset.
 - Before all the initialization steps, we loop over characters and render them.
 - To finish, the OpenGL states are restored.
 

Draw a complete String

/**
 * Draw a String at the position (x, y) in an OpenGL scene
 * @param glDrawable
 * @param x x position of the text
 * @param y y position of the text
 * @param charSize dimension of a character
 */

private void drawTextAt(String text, FontAlign align, float x, float y, float z, Dimension charSize)
{
    final GL gl = GLContext.getCurrent().getGL();

    gl.glPushMatrix();
    gl.glPushAttrib(GL.GL_COLOR_BUFFER_BIT);
    //activate texture
    selectFontTexture(gl);
    boolean textureActive = gl.glIsEnabled(GL.GL_TEXTURE_2D);
    if(!textureActive)
        gl.glEnable(GL.GL_TEXTURE_2D);

    //activate the blending
    if(fontHaveAlphaLayer)
        gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
    else
        gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE);
    if(!gl.glIsEnabled(GL.GL_BLEND))
        gl.glEnable(GL.GL_BLEND);

    //Text alignement (just an x, y offset)
    float posX = x, posY = y, posZ = z;
    switch(align)
    {
        case TopRight:
            posX -= charSize.width*text.length();
            posY -= charSize.height;
            break;
        case Right:
            posX -= charSize.width*text.length();
            posY -= charSize.height/2;
            break;
        case BotRight:
            posX -= charSize.width*text.length();
            break;

        case TopCenter:
            posX -= charSize.width*text.length()/2;
            posY -= charSize.height;
            break;
        case Center:
            posX -= charSize.width*text.length()/2;
            posY -= charSize.height/2;
            break;
        case BotCenter:
            posX -= charSize.width*text.length()/2;
            break;


        case TopLeft:
            posY -= charSize.height;
            break;
        case Left:
            posY -= charSize.height/2;
            break;
//        case BotLeft:
//            break;

    }
    gl.glTranslatef(posX, posY, posZ);

    int width = CHAR_SIZE.width;
    int height = CHAR_SIZE.height;

    for(int i = 0; i < text.length(); i++)
    {
        //Position of the character on the grid
        int line = (text.charAt(i)-CHAR_OFFSET)/16;
        int row = (text.charAt(i)-CHAR_OFFSET)%16;
        Point topLeft = new Point(row*width, TEXTURE_HEIGHT-line*height);

        //Draw the character and translate
        gl.glBegin(GL.GL_QUADS);
          gl.glTexCoord2f( topLeft.x       /((float)TEXTURE_WIDTH),  topLeft.y        /((float)TEXTURE_HEIGHT));
          gl.glVertex2i(0,              charSize.height);
         
          gl.glTexCoord2f( topLeft.x       /((float)TEXTURE_WIDTH), (topLeft.y-height)/((float)TEXTURE_HEIGHT));
          gl.glVertex2i(0,              0);
         
          gl.glTexCoord2f((topLeft.x+width)/((float)TEXTURE_WIDTH), (topLeft.y-height)/((float)TEXTURE_HEIGHT));
          gl.glVertex2i(charSize.width, 0);
         
          gl.glTexCoord2f((topLeft.x+width)/((float)TEXTURE_WIDTH),  topLeft.y        /((float)TEXTURE_HEIGHT));
          gl.glVertex2i(charSize.width, charSize.height);
        gl.glEnd();
       
        gl.glTranslatef(charSize.width, 0, 0);
    }

    if(!textureActive)
        gl.glDisable(GL.GL_TEXTURE_2D);
    gl.glPopAttrib();
    gl.glPopMatrix();
}

Note: If the texture font have an alpha layer (fontHaveAlphaLayer), the appropriate blending function is used. Look at the next paragraph to have more informations on how alpha layer is created.
 

The font picture used is a 2 bit picture (black & white only). Character is composed of white pixels, black pixels should be considered as transparent pixels.

To remove these pixels, we can use blending. For this, we affect an alpha of 0 for black pixels and an alpha of 1 for white pixels.
This can be done using the method createsAlphaLayer of the class PictureEffects. This method returns a Picture in which an alpha layer is added. Alpha is zero where pixels are full black, else alpha is set to one. The alpha layer can be used to mask black pixels.
To have more informations on PictureEffects, look at the Tutorial 21.

The default font renderer is created like this :

Adding an alpha layer

/**
 * Creates a default Font2D
 * @param glDrawable
 * @return the default Font2D object created
 */

public static Font2D createDefault()
{
    /**
     * Creates an alpha layer on the picture.
     * In all black pixels, the alpha is setted to 0.
     */

    Picture picture = PictureEffects.createsAlphaLayer(textureLoader, "/textures/Font1.png");
    TextureLoader textureLoader = new TextureLoader();
    textureLoader.setPicture(picture);
    int fontTexture = textureLoader.loadTexture(true);

    Font2D printer = new Font2D(fontTexture);
    printer.setFontHaveAlphaLayer(true);       //The font have an alpha layer, so use it !
    return printer;
}

 

We have created an alpha layer on the texture, but how to use it ?
It is very simple, we enable blending to have access to the alpha value, and we use the appropriate blending function :

Blending

    if(fontHaveAlphaLayer)
        gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
    else
        gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE);

If the alpha layer of the texture is not used, I mix equally source (font texture) and destination colors (framebuffer color). Else, source and destination colors are mixed accordingly to the alpha value.
 

In Tutorial 16, we have seen display lists for optimizing rendering performances. We will use it here to render a font.

I've told you in the Advanced part of the tutorial 16 a way to call multiple display lists with one call. Here is a concrete application of this.

First, we generate our display lists. A display list render a character (...) and position to the next character (glTranslate).

Display list version

    //Generate 256 adjacent display lists, one display list per character
    FONT = gl.glGenLists(256);
   
    //Create the display list
    for(char i = 32; i < 256; i++)
    {
        gl.glNewList(FONT+i, GL.GL_COMPILE);
          ...    //Code from 'Render a character'
          gl.glTranslatef(charSize.width, 0.0f, 0.0f);
        gl.glEndList();
    }

Now our display lists are created, we will see how to call them for rendering a string.
For this, we will see the power of glListBase & glCallLists to achieve such effect in one call.

Here are the description of the two methods used :
    glListBase(listOffset)
    glCallLists(numList, format, lists)
lists is an array of display list, the size of the array is at least numList. Le glCallLists method will render the list of display list.

For our application, we have a list of character which correspond to the index of the display list. The first display list (index 0) is FONT (see the code above). To use directly the byte[] representation of the string (from getBytes()), we can use glListBase with FONT as parameter. The result of this is display list rendered will be offseted by listOffset.

Suppose we want to render the string "a". The following code will be equivalent to glCallList(FONT+'a').
 

Render a String

    //Render a string
    gl.glListBase(FONT);
    gl.glCallLists(text.length(), GL.GL_UNSIGNED_BYTE, ByteBuffer.wrap(text.getBytes()));
//  gl.glCallLists(text.length(), GL.GL_UNSIGNED_BYTE, text.getBytes(), 0);    //Alternative way
 

is equivalent to :

Render a String

    //Render a string
    for(int i = 0; i < text.length; i++)
    {
       gl.glCallList(FONT+text[i]);
    }

 

F : switch between Font2D/Font2DL
 

Remember to download the GraphicEngine-1.1.2 to run this tutorial !


If you've got any remarks on this tutorial, please let me know to improve it.
Thanks for your feedback.
 

Previous Tutorial

Back

Next turorial

Copyright © 2004-2012 Jérôme Jouvie - All rights reserved. http://jerome.jouvie.free.fr/