Tutorial de SDL – Parte 41 – Fontes Bitmap

Continuando nossa série de artigos traduzidos do site lazyfoo, veremos agora como renderizar fontes bitmap.
Algumas vezes fontes TTF são flexíveis o suficiente. Como renderizar texto é o mesmo que renderizar imagens de caracteres, podemos usar fontes bitmap para renderizar texto. Se você imaginar cada caractere em u a string como um pedaço de imagem, pode imaginar o trabalhao de renderização de fontes como o rearranjamento de vários desses pedaços:

Fontes bitmap funcionam pelo uso de uma planilha de “glifos” (imagens de caracteres) e renderizando eles de modo que formem uma string na tela.

//Texture wrapper class
class LTexture
{
    public:
        //Initializes variables
        LTexture();
        //Deallocates memory
        ~LTexture();
        //Loads image at specified path
        bool loadFromFile( std::string path );
        #ifdef _SDL_TTF_H
        //Creates image from font string
        bool loadFromRenderedText( std::string textureText, SDL_Color textColor );
        #endif
        //Deallocates texture
        void free();
        //Set color modulation
        void setColor( Uint8 red, Uint8 green, Uint8 blue );
        //Set blending
        void setBlendMode( SDL_BlendMode blending );
        //Set alpha modulation
        void setAlpha( Uint8 alpha );
        //Renders texture at given point
        void render( int x, int y, SDL_Rect* clip = NULL, double angle = 0.0, SDL_Point* center = NULL, SDL_RendererFlip flip = SDL_FLIP_NONE );
        //Gets image dimensions
        int getWidth();
        int getHeight();
        //Pixel manipulators
        bool lockTexture();
        bool unlockTexture();
        void* getPixels();
        int getPitch();
        Uint32 getPixel32( unsigned int x, unsigned int y );
    private:
        //The actual hardware texture
        SDL_Texture* mTexture;
        void* mPixels;
        int mPitch;
        //Image dimensions
        int mWidth;
        int mHeight;
};

Nos artigos anteriores, quando fizemos manipulação de pixels da textura, não nos importamos com quais pixels nós lemos pois queríamos ler todos os pixels. Agora precisamos ler os pixels das coordenadas x/y exatas, por isso iremos acrescentar uma função getPixel32. Essa função trabalha especificamente com pixels de 32 bits.

//Our bitmap font
class LBitmapFont
{
    public:
        //The default constructor
        LBitmapFont();
        //Generates the font
        bool buildFont( LTexture *bitmap );
        //Shows the text
        void renderText( int x, int y, std::string text );
    private:
        //The font texture
        LTexture* mBitmap;
        //The individual characters in the surface
        SDL_Rect mChars[ 256 ];
        //Spacing Variables
        int mNewLine, mSpace;
};

Aqui temos nossa fonte bitmap que atua com um encapsulador para a planilha de glifos. Possui um construtor que inicializa variáveis internas, uma função para montar a fonte, e uma função para renderizar o texto.

Quando a fonte bitmap é montada nós percorremos a textura para encontrar todos os pedaços dos 256 caracteres (que não armazenados em arrays mChar) e calculamos a distância para uma nova linha e um espaço.

bool LTexture::loadFromFile( std::string path )
{
    //Get rid of preexisting texture
    free();
    //The final texture
    SDL_Texture* newTexture = NULL;
    //Load image at specified path
    SDL_Surface* loadedSurface = IMG_Load( path.c_str() );
    if( loadedSurface == NULL )
    {
        printf( "Unable to load image %s! SDL_image Error: %s\n", path.c_str(), IMG_GetError() );
    }
    else
    {
        //Convert surface to display format
        SDL_Surface* formattedSurface = SDL_ConvertSurfaceFormat( loadedSurface, SDL_PIXELFORMAT_RGBA8888, NULL );
        if( formattedSurface == NULL )
        {
            printf( "Unable to convert loaded surface to display format! %s\n", SDL_GetError() );
        }
        else
        {
            //Create blank streamable texture
            newTexture = SDL_CreateTexture( gRenderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, formattedSurface->w, formattedSurface->h );
            if( newTexture == NULL )
            {
                printf( "Unable to create blank texture! SDL Error: %s\n", SDL_GetError() );
            }
            else
            {
                //Enable blending on texture
                SDL_SetTextureBlendMode( newTexture, SDL_BLENDMODE_BLEND );
                //Lock texture for manipulation
                SDL_LockTexture( newTexture, &formattedSurface->clip_rect, &mPixels, &mPitch );
                //Copy loaded/formatted surface pixels
                memcpy( mPixels, formattedSurface->pixels, formattedSurface->pitch * formattedSurface->h );
                //Get image dimensions
                mWidth = formattedSurface->w;
                mHeight = formattedSurface->h;
                //Get pixel data in editable format
                Uint32* pixels = (Uint32*)mPixels;
                int pixelCount = ( mPitch / 4 ) * mHeight;
                //Map colors
                Uint32 colorKey = SDL_MapRGB( formattedSurface->format, 0, 0xFF, 0xFF );
                Uint32 transparent = SDL_MapRGBA( formattedSurface->format, 0x00, 0xFF, 0xFF, 0x00 );
                //Color key pixels
                for( int i = 0; i < pixelCount; ++i )
                {
                    if( pixels[ i ] == colorKey )
                    {
                        pixels[ i ] = transparent;
                    }
                }
                //Unlock texture to update
                SDL_UnlockTexture( newTexture );
                mPixels = NULL;
            }
            //Get rid of old formatted surface
            SDL_FreeSurface( formattedSurface );
        }
        //Get rid of old loaded surface
        SDL_FreeSurface( loadedSurface );
    }
    //Return success
    mTexture = newTexture;
    return mTexture != NULL;
}

Aqui temos a função de carregamento de textura dos artigos anteriores com algumas alterações. Fizemos a codificação de cores externamente em uma artigo anterior, e aqui estamos fazendo internamente ma função de carregamento da textura. Além disso, especificamos o formato do pixel da textura como SDL_PIXELFORMAT_TGBA8888 de forma que possamos ter um pixel RG  de 32 bits.

Uint32 LTexture::getPixel32( unsigned int x, unsigned int y )
{
    //Convert the pixels to 32 bit
    Uint32 *pixels = (Uint32*)mPixels;
    //Get the pixel requested
    return pixels[ ( y * ( mPitch / 4 ) ) + x ];
}

Aqui temos nossa função para ler um pixel em uma área especifica. A parte importante a se saber é que mesmo que tenhamos uma textura de 2 dimensões como essa:

Os pixels são armazenados em uma dimensão, dessa forma:

Se você quiser ler o pixel azul da linha 1, coluna 1 (a primeira linha/coluna é a linha/coluna 0), teria que calcular a posição dessa forma:

 Y Offset * Pitch + X Offset

Que daria o seguinte resultado:

1 * 5 + 1 = 6

E, como você pode ver, o pixel na posição 6 do array de 1 dimensão é o mesmo que está na linha 1 coluna 1 do array de 2 dimensões. E  se você está se perguntando por quê dividimos o pitch por 4, lembre que pitch está em bytes. Como  precisamos do pitch em pixels e existem 4 bytes por pixel, dividimos ele por 4.

LBitmapFont::LBitmapFont()
{
    //Initialize variables
    mBitmap = NULL;
    mNewLine = 0;
    mSpace = 0;
}

Aqui temos o construtor onde inicializamos as variáveis internas.

bool LBitmapFont::buildFont( LTexture* bitmap )
{
    bool success = true;
    //Lock pixels for access
    if( !bitmap->lockTexture() )
    {
        printf( "Unable to lock bitmap font texture!\n" );
        success = false;
    }

Agora estamos entrando na função que irá percorrer a fonte bitmpa e definir os retângulos de corte para cada letra. Para fazer isso iremos bloquear a textura para acessar seus pixels.

    else
    {
        //Set the background color
        Uint32 bgColor = bitmap->getPixel32( 0, 0 );
        //Set the cell dimensions
        int cellW = bitmap->getWidth() / 16;
        int cellH = bitmap->getHeight() / 16;
        //New line variables
        int top = cellH;
        int baseA = cellH;
        //The current character we're setting
        int currentChar = 0;

Para que o carregamento da fonte bitmap possa ser executado, os glifos de caracteres precisam estar arrumados em células:

Todas as células precisam ter a mesma largura e altura, e estarem dispostas em 16 colunas e 16 linhas, na ordem da tabela ASCII. A função de carregamento da fonte bitmap irá percorrer cada uma dessas células, encontrar os lados dos glifos e configurar o retângulo de corte para o pedaço.

Primeiro nós lemos a cor de fundo para que possamos encontrar os cantos dos glifos. Em seguida calculamos a largura e altura da célula. Temos uma variável chamada top que irá guardar o topo do glifo mais alto na planilha. A variável baseA irá guardar a posição do glifo A maiúsculo, que será usado como base para renderizar os caracteres.

Por último, temos uma variável currentChar que guarda o glifo de caractere atual.

        //Go through the cell rows
        for( int rows = 0; rows < 16; ++rows )
        {
            //Go through the cell columns
            for( int cols = 0; cols < 16; ++cols )
            {
                //Set the character offset
                mChars[ currentChar ].x = cellW * cols;
                mChars[ currentChar ].y = cellH * rows;
                //Set the dimensions of the character
                mChars[ currentChar ].w = cellW;
                mChars[ currentChar ].h = cellH;

Esses dois loops aninhados percorrem as linhas e colunas da célula.

No topo do loop da célula, inicializamos a posição do glifo no topo da célula e as dimensões de cada pedaço. Isso significa que o glifo padrão é a célula cheia.

                //Find Left Side
                //Go through pixel columns
                for( int pCol = 0; pCol < cellW; ++pCol )
                {
                    //Go through pixel rows
                    for( int pRow = 0; pRow < cellH; ++pRow ) { //Get the pixel offsets int pX = ( cellW * cols ) + pCol; int pY = ( cellH * rows ) + pRow; //If a non colorkey pixel is found if( bitmap->getPixel32( pX, pY ) != bgColor )
                        {
                            //Set the x offset
                            mChars[ currentChar ].x = pX;
                            //Break the loops
                            pCol = cellW;
                            pRow = cellH;
                        }
                    }
                }

Para cada célula precisamos percorrer todos os pixels da célula para encontrar as bordas do glifo. Nesse loop nós percorremos cada coluna de cima para baixo e procuramos pelo primeiro pixel cuja cor seja diferente da cor de fundo. Uma vez que tenhamos encontrado esse pixel, significa que encontramos a borda do glifo:

Quando encontramos o lado esquerdo do glifo, configuramos ele com a posição x do pedaço, e então saímos do loop.

                //Find Right Side
                //Go through pixel columns
                for( int pColW = cellW - 1; pColW >= 0; --pColW )
                {
                    //Go through pixel rows
                    for( int pRowW = 0; pRowW < cellH; ++pRowW ) { //Get the pixel offsets int pX = ( cellW * cols ) + pColW; int pY = ( cellH * rows ) + pRowW; //If a non colorkey pixel is found if( bitmap->getPixel32( pX, pY ) != bgColor )
                        {
                            //Set the width
                            mChars[ currentChar ].w = ( pX - mChars[ currentChar ].x ) + 1;
                            //Break the loops
                            pColW = -1;
                            pRowW = cellH;
                        }
                    }
                }

Aqui procuramos pelo pixel no lado direito. Funciona de forma análoga ao modo como encontramos o pixel no lado esquerdo, só que agora nos movemos da direita para a esquerda ao invés de da esquerda para a direita.

Quando encontramos o pixel à direita, usamos ele para configurar a largura. Como o array começa na posição 0, precisamos adicionar 1 à largura.

                //Find Top
                //Go through pixel rows
                for( int pRow = 0; pRow < cellH; ++pRow )
                {
                    //Go through pixel columns
                    for( int pCol = 0; pCol < cellW; ++pCol ) { //Get the pixel offsets int pX = ( cellW * cols ) + pCol; int pY = ( cellH * rows ) + pRow; //If a non colorkey pixel is found if( bitmap->getPixel32( pX, pY ) != bgColor )
                        {
                            //If new top is found
                            if( pRow < top )
                            {
                                top = pRow;
                            }
                            //Break the loops
                            pCol = cellW;
                            pRow = cellH;
                        }
                    }
                }

Aqui temo o código para encontrar o topo do glifo. Quando encontramos um topo que seja mais alto que o atual, configuramos ele como o novo topo.

Observe que como o eixo y está invertido, o topo mais alto é na verdade a menor posição em y.

                //Find Bottom of A
                if( currentChar == 'A' )
                {
                    //Go through pixel rows
                    for( int pRow = cellH - 1; pRow >= 0; --pRow )
                    {
                        //Go through pixel columns
                        for( int pCol = 0; pCol < cellW; ++pCol ) { //Get the pixel offsets int pX = ( cellW * cols ) + pCol; int pY = ( cellH * rows ) + pRow; //If a non colorkey pixel is found if( bitmap->getPixel32( pX, pY ) != bgColor )
                            {
                                //Bottom of a is found
                                baseA = pRow;
                                //Break the loops
                                pCol = cellW;
                                pRow = -1;
                            }
                        }
                    }
                }
                //Go to the next character
                ++currentChar;
            }
        }

Em se tratando de procurar pela parte de baixo do glifo, o único que nos importamos é o A maiúsculo. Para essa fonte bitmap, iremos usar a parte de baixo do glifo A como base para que caracteres como “g”,  “j”, “y”, etc que se estendem para baixo não definam a parte de baixo. Você não precisa fazer isso dessa forma, mas dessa forma pude ter bons resultados antes.

        //Calculate space
        mSpace = cellW / 2;
        //Calculate new line
        mNewLine = baseA - top;
        //Lop off excess top pixels
        for( int i = 0; i < 256; ++i ) { mChars[ i ].y += top; mChars[ i ].h -= top; } bitmap->unlockTexture();
        mBitmap = bitmap;
    }
    return success;
}

Após termos definido todos os pedaços, temos algum pós-processamento para ser feito. Primeiro calculamos o tamanho do espaço. Aqui definimos como metade da largura da célula. Em seguida calculamos a altura de uma nova linha usando a base e o topo do glifo mais alto.

Nós então cortamos o espaço extra no topo de cada glifo para prevenir que tenhamos muito espaço entre as linhas. Finalmente, desbloqueamos a textura e configuramos o bitmap para a fonte.

Vale notar que a forma como construímos a fonte bitmap não é a única forma de fazer isso. Você pode definir espaços, novas linhas e bases de outra forma. Você pode usar um arquivo XML para definir as posições dos glifos ao invés de células. Eu decide seguir esse método pois é o mais comum e sempre funcionou para mim.

void LBitmapFont::renderText( int x, int y, std::string text )
{
    //If the font has been built
    if( mBitmap != NULL )
    {
        //Temp offsets
        int curX = x, curY = y;

Agora que temos todos os glifos definidos, chegou a hora de renderiza-los na tela. Primeiro, verificamos se existe um bitmap para renderiza-lo, em seguida declaramos as posições x/y que serão usadas para renderizar o glifo atual.

        //Go through the text
        for( int i = 0; i < text.length(); ++i )
        {
            //If the current character is a space
            if( text[ i ] == ' ' )
            {
                //Move over
                curX += mSpace;
            }
            //If the current character is a newline
            else if( text[ i ] == '\n' )
            {
                //Move down
                curY += mNewLine;
                //Move back
                curX = x;
            }

Aqui temos o loop que percorre a string para renderizar cada glifo. Porém, existem dois valores ASCII que não iremos renderizar nada para ele. Quando temos um espaço, tudo que temos que fazer é nos mover na largura do espaço. Quando temos uma nova linha, nos movemos para baixo e retornamos para a posição x base.

            else
            {
                //Get the ASCII value of the character
                int ascii = (unsigned char)text[ i ];
                //Show the character
                mBitmap->render( curX, curY, &mChars[ ascii ] );
                //Move over the width of the character with one pixel of padding
                curX += mChars[ ascii ].w + 1;
            }
        }
    }
}

Para caracteres não especiais, renderizamos o glifo. Como você pode ver, é tão simples quanto ler o valor ASCII, renderizando o glifo associado com o valor ASCII e então nos movendo a largura do glifo. O loop for irá percorrer todos os caracteres e renderizar o glifo de cada um dele um após o outro.

Baixe os arquivos de mídia e de código fonte desse artigo aqui.