Continuando nossa série de artigos traduzidos do site lazyfoo, veremos agora como aplicar alguns efeitos nas texturas de uma imagem, manipulando duas características.
Para criar efeitos gráficos frequentemente é necessário acesso aos pixels. Nesse artigo, iremos alterar os pixels de uma imagem para clarear o fundo.
//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(); private: //The actual hardware texture SDL_Texture* mTexture; void* mPixels; int mPitch; //Image dimensions int mWidth; int mHeight; };
Aqui iremos adicionar nova funcionalidade à classe da textura. Temos funções para bloquear/desbloquear a textura pois para podermos acessar os pixels da textura temos que bloqueá-la e uma vez que tivermos terminados de lidar com os pixels temos que desbloqueá-la.
Temos uma função para obter os pixels brutos e uma função para obter o pitch. O pitch é basicamente a largura da texteura na memória. Em alguns equipamentos mais antigos e portáteis, existem limitações do tamanho da textura que você pode ter. Se você criar uma textura com uma lardura de 100 pixels, pode ser que ela seja alterada para 128 pixels (a potência de 2 mais próxima). Usando o pitch, saberemos como a imagem é armazenada na memória.
Em termos de atributos, temos um ponteiro para os pixels após o bloqueio da textura e o pitch.
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_ConvertSurface( loadedSurface, SDL_GetWindowSurface( gWindow )->format, NULL ); if( formattedSurface == NULL ) { printf( "Unable to convert loaded surface to display format! SDL Error: %s\n", SDL_GetError() ); } else { //Create blank streamable texture newTexture = SDL_CreateTexture( gRenderer, SDL_GetWindowPixelFormat( gWindow ), SDL_TEXTUREACCESS_STREAMING, formattedSurface->w, formattedSurface->h ); if( newTexture == NULL ) { printf( "Unable to create blank texture! SDL Error: %s\n", SDL_GetError() ); }
Para que sejamos capazes de editar a textura, temos que carregar nossa textura de forma diferente. Quando criamos texturas a partir de superfícies em artigo anteriores, elas tinham o valor de SDL_TextureAccess padrão SDL_TEXTUREACCESS_STATIC, o que significava que não podíamos altera-la após ter sido criada. Para podermos ser capazes de editar os pixels da textura temos que criar a textura como SDL_TEXTUREACCESS_STREAMING. Primeiro temos que carregar a imagem como uma superfície como fizemos anteriormente. Então temos que converter a superfície para o mesmo formato de pixel da janela. Por fim, criamos uma textura em branco com SDL_Createtexture.
else { //Lock texture for manipulation SDL_LockTexture( newTexture, NULL, &mPixels, &mPitch ); //Copy loaded/formatted surface pixels memcpy( mPixels, formattedSurface->pixels, formattedSurface->pitch * formattedSurface->h ); //Unlock texture to update SDL_UnlockTexture( newTexture ); mPixels = NULL; //Get image dimensions mWidth = formattedSurface->w; mHeight = formattedSurface->h; } //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; }
Depois que a textura é criada, temos que manualmente copias os pixels da superfície para a textura. Para capturas os pixels da textura usamos SDL_LockTexture. O primeiro argumento é a textura que queremos capturar seus pixels. O segundo argumento é a região de onde queremos capturas os pixels, e como iremos capturas os pixels de toda a textura configuramos esses argumento como NULL. O terceira argumento é o ponteiro que será usado como endereço dos pixels, e o último argumento será configurada com o pitch da textura.
Depois que tivermos os pixels da textura, copiamos os pixels da superfície para a textura usando memcpy. O primeiro argumento é o destino, o segundo é a origem, e o terceiro argumento é o número de bytes que iremos copiar. Felizmente, o pitch do SDL nos fornece o número de bytes por linha de pixels, de forma que precisamos apenas multiplicar ele pela altura da superfície para copiar todos os pixels da image. Quando tivermos copiados os pixels da superfície para a textura, desbloqueamos a textura para atualiza-la com os novos pixels usando SDL_UnlickTexture. Depois que a textura é desbloqueada o ponteiro de pixels é invalidado configurando ele como NULL.
Com os pixels da superfície copiados para a textura, nos livramos das superfícies antigas e retornamos true se a textura tiver sido carregada com sucesso.
bool LTexture::lockTexture() { bool success = true; //Texture is already locked if( mPixels != NULL ) { printf( "Texture is already locked!\n" ); success = false; } //Lock texture else { if( SDL_LockTexture( mTexture, NULL, &mPixels, &mPitch ) != 0 ) { printf( "Unable to lock texture! %s\n", SDL_GetError() ); success = false; } } return success; } bool LTexture::unlockTexture() { bool success = true; //Texture is not locked if( mPixels == NULL ) { printf( "Texture is not locked!\n" ); success = false; } //Unlock texture else { SDL_UnlockTexture( mTexture ); mPixels = NULL; mPitch = 0; } return success; }
Aqui temos nossas função de bloqueio e desbloqueio da textura após o carregamento da imagem.
void* LTexture::getPixels() { return mPixels; } int LTexture::getPitch() { return mPitch; }
Finalmente, temos os métodos de acesso para os pixels e para o pitch enquanto a textura estiver bloqueada. Agora que criamos uma textura que pode ser editável e bloqueada/desbloqueada, chegou a hora de executar algum processamento nos pixels da textura.
bool loadMedia() { //Loading success flag bool success = true; //Load foo' texture if( !gFooTexture.loadFromFile( "40_texture_manipulation/foo.png" ) ) { printf( "Failed to load corner texture!\n" ); success = false; } else { //Lock texture if( !gFooTexture.lockTexture() ) { printf( "Unable to lock Foo' texture!\n" ); }
Na nossa função de carregamento de mídia, depois que carregamos a textura, bloqueamos ela de forma que possamos alterar os pixels.
//Manual color key else { //Get pixel data Uint32* pixels = (Uint32*)gFooTexture.getPixels(); int pixelCount = ( gFooTexture.getPitch() / 4 ) * gFooTexture.getHeight();
Depois que a textura é bloqueada, iremos percorrer os pixels e tornar todos os pixels do fundo transparentes. O que estamos fazendo é essencialmente manualmente codificando as cores da imagem.
Primeiro precisamos capturas todos os pixels. Nosso método de acesso aos pixels retorna um ponteiro void e queremos pixels de 32 bits, assim sobrecarregamos o tipo para um inteiro sem sinal de 32 bits (unsigned integer).
Em seguida, queremos obter o número de pixels. Temos o pitch que é a largura em bytes. Precisamos da largura em pixels e como temos 4 bytes por pixel. tudo que precisamos fazer é dividir por 4 para obtermos o pitch em pixels. Então multiplicamos o pitch pela altura para o obter o número total de pixels.
//Map colors Uint32 colorKey = SDL_MapRGB( SDL_GetWindowSurface( gWindow )->format, 0, 0xFF, 0xFF ); Uint32 transparent = SDL_MapRGBA( SDL_GetWindowSurface( gWindow )->format, 0xFF, 0xFF, 0xFF, 0x00 ); //Color key pixels for( int i = 0; i < pixelCount; ++i ) { if( pixels[ i ] == colorKey ) { pixels[ i ] = transparent; } } //Unlock texture gFooTexture.unlockTexture(); } } return success; }
O que iremos fazer é encontrar todos os pixels da codificação de cores e substitui-los por pixels transparentes. Primeiro mapeamos o código de cores e a cor transparente. Em seguida percorremos todos os pixels e verificamos se o pixel bate com a codificação das cores. Se bater, damos a ele o valor de um pixel transparente.
Após tivermos percorrido os pixels, desbloqueamos a textura para atualizar-la com os novos pixels.
Baixe os arquivos de mídia e de código fonte do exemplo desse artigo aqui.