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.