No artigo anterior finalmente terminamos os conceitos básicos de luzes. Você deve ter ficado impressionado com os resultados e, certamente, já pode até ter feito algo para impressionar sua família e amigos. Entretanto, a menos que sua ambição seja fazer um jogo de sinuca, é bom que as formas que desenhamos na tela tenham texturas.
Nesse artigo, iremos ver como executar o primeiro passo na criação de imagens com texturas, que é ler uma imagem do disco. Como o OpenGL não fornece nenhuma função nativa para isso, iremos usar a SDL para carregar a imagem de maneira simples e multi-plataforma e analisar suas propriedades.
Texturas
Texturas permitem-nos pegar uma imagem plana, como a do mapa mundi e colocar sobre uma forma tridimensional, como uma esfera, de modo a preenche-la. No caso dos exemplos, teríamos como resultado o planeta terra (é necessário Java 6 ou superior para rodar o exemplo). Responda sim ao Java, pois infelizmente não tenho o certificado digital para te poupar dessas verificações de segurança.
Na verdade, esse exemplo é exatamente o que você poderá fazer após aprender a manipular texturas. Nele, você pode usar os números de 1 até 5 para alterar a qualidade do globo, a rodinha do mouse para mexer no zoom, o botão da direita para brincar com as luzes e o da esquerda para mover o planeta.
Iremos falar mais de texturas no próximo artigo. Por enquanto, fique só com o gostinho do que poderemos fazer, e com a certeza de que você finalmente poderá deixar sua família e amigos boquiabertos.
Carregando a imagem do disco
Se você se lembrar, quando criamos nossa janela SDL, eu comentei que qualquer coisa que possa ser desenhada na tela com a SDL era uma superfície e, portanto, uma SDL_Surface. Com imagens não podia ser diferente.
Carregamos uma imagem na SDL através da função IMG_Load. Essa função não está entre os pacotes básicos da SDL e pertence a extensão SDL_image. Se você não configurou ainda seu code::blocks para usar essa função, volte ao artigo sobre a criação do ambiente para jogos e o faça isso agora. A opção do pacote básico é a função SDL_LoadBMP mas. como o nome já diz, ela está restrita a imagens sem compactação no formato bitmap.
Carregar a imagem em si é uma tarefa muito simples. Basta usarmos a função passando como parâmetro o nome da imagem a ser carregada. Testamos então se o retorno não é nulo, o que indica uma falha no carregamento. Se não for, nossa imagem já foi corretamente carregada. Por exemplo:
SDL_Surface* imagem = SDL_Image("cake.png");
if (imagem == NULL)
{
std::cout << "Sorry, no cake for you." << std::endl;
return;
}
//Chama uma função para desenhar o bolo
drawCake(cake);
Existem outras formas de carregar a imagem, como nos casos onde ela está dentro de um zip, por exemplo, mas não nos aprofundaremos nesse tema aqui. Caso queira ler a respeito, confira esse artigo no Vertex Buffer.
Finalmente, após usarmos a imagem, usamos o comando SDL_FreeSurface para libera-la da memória.
Identificando as propriedades da imagem
Como o OpenGL não sabe como a imagem foi carregada, teremos que informar para ele detalhes sobre o formato da imagem. A forma mais fácil de fazer isso seria convencionar que todas as imagens carregadas tem o formato ARGB ou RGB, por exemplo, e criar funções para carregar desses dois formatos.
Agora, o ideal seria se pudessemos identificar as propriedades da imagem em tempo de execução, para dinamicamente fornecer esses dados para o OpenGL. Felizmente a SDL nos dá esse mecanismo e o estudaremos agora.
Toda SDL_Surface tem uma propriedade chamada format, do tipo SDL_PixelFormat. Ela nada mais é do que uma struct com diversos campos interessantes como:
- pallete: Dados da paleta de cor, caso a imagem use palletas, como o GIF, do tipo SDL_Pallete;
- BitsPerPixel: Quantidade de bits num único pixel de cor. Se só os formatos RGB e ARGB existirem em sua aplicação essa informação já será suficiente para diferenciar as duas imagens, já que um usa 24 bits por pixel e o outro 32. O uso dessa técnica pode ser vista aqui no blog do Shkaz.;
- BytesPerPixel: Equivalente ao de cima, mas em bytes;
- [RGBA]mask: Quatro propriedades que definem a máscara de como as informações de cor estão dispostas. Falaremos mais sobre esse campo abaixo;
- [RGBA]shift: Deslocamento para direita que os bits devem sofrer para isolar apenas o valor dessa propriedade em um byte.
- [RGBA]loss: Deslocamento para a esquerda que os bits devem sofrer devido a perda de precisão;
- ColorKey: Usado em formatos RGB que suportam transparência. Nesse caso, a cor indicada por esse valor é considerada transparente.
- Alfa: Quantidade geral do componente alfa na imagem;
Se quisermos identificar realmente a ordem dos bits em nossa imagem, teremos que analisar as propriedades [RGBA]mask. Cada propriedade dessas dá um número que, aplicado & sobre o pixel, isolará apenas os bits correspondentes ao componente de cor desejado. Os escovadores de bits de plantão já sacaram, mas se você ainda estiver confuso, vamos a um exemplo. Suponha que você leu a imagem, já usou a propriedade BytesPerPixel e descobriu que sua imagem tem 4 bytes (logo, existe um componente alfa). Um pixel qualquer em sua imagem, por exemplo, poderia ter esse valor:
10011101 10111101 11011101 11111111
Como descobrir só o valor do componente verde? O primeiro passo é olhar a propriedade Gmask. O valor dela poderia ser o número 16711680 (0xFF0000). Isso não é muito significativo em decimal, mas vejamos em binário:
00000000 11111111 00000000 00000000
Agora, veja o que ocorre se aplicarmos esse valor ao número original. Obtemos:
10011101 10111101 11011101 11111111 &
00000000 11111111 00000000 00000000 =
00000000 10111101 00000000 00000000
Notou? Todos os componentes de cor que não são verdes forem zerados após a operação lógica E em cada bit. E se quiséssemos isolar o valor somente daquele componente? Bem, teríamos que desloca-lo 16 bits para direita. Esse valor é exatamente o que está armazenado na propriedade Gshift. Ou seja, pixel >> Gshift nos daria:
00000000 00000000 00000000 10111101
Ok, mas e a propriedade Gloss? O que pode ocorrer é que nosso componente verde poderia ter sido compactado. Uma maneira comum de se fazer isso é descartando o bit menos significativo, deixando o tipo com 7 bits. Nesse caso, GShift conteria o valor 17, deslocando um bit a mais para direita e o componente Gloss teria o valor 1, indicando que devemos voltar esse bit a mais. Observe a operação bit a bit nesse caso:
10011101 10111101 11011101 11111111 & Gmask =
00000000 10111101 00000000 00000000 >> Gshift (17 bits) =
00000000 00000000 00000000 01011110 << Gloss (1 bit) =
00000000 00000000 00000000 10111100
Note que o último bit, menos significativo, foi descartado. Esquemas de compactação mais agressivos podem até mesmo reduzir 2 bits, o que é raro, mas possível.
Vejamos essa mesmas operações para obter o valor do componente vermelho do primeiro pixel de uma imagem qualquer:
//Lemos a imagem do disco
SDL_Surface* imagem = SDL_Image("cake.png");
if (imagem == NULL)
{
std::cout << "Sorry, no cake for you." << std::endl;
return;
}
SDL_PixelFormat *fmt = imagem->format;
//Obtém os pixels da imagem, veja o próximo tópico
SDL_LockSurface(surface);
Uint32* pixel=*((Uint32*)surface->pixels);
SDL_UnlockSurface(surface);
//Já estamos apontando para o primeiro pixel,
//vamos ler seu componente vermelho
Uint32 temp = pixel & fmt->Rmask; //Separamos só o vermelho
temp=temp >> fmt->Rshift;//Deslocamos os bits para direita
temp=temp << fmt->Rloss; //Voltamos os bits para esquerda
std::cout << "O valor no vermelho é:";
std::cout << static_cast<Uint8>(temp);
Aquele cast não era realmente necessário, mas deixei-o para demonstrar que o valor resultante é mesmo um único byte, com o valor do componente de cor.
Manipulação da imagem bit-a-bit
Um dos grandes poderes da SDL é que ela permite a manipulação bit-a-bit da superfície. Poderíamos usar isso para gerar imagens dinamicamente, copiar trechos de imagem uns sobre os outros, ou mesmo aplicar efeitos antes de gerar nossa textura.
Para isso toda SDL_Surface tem a propriedade pixels, que retorna um conjunto de bytes contendo os pixels no formato descrito anteriormente. Esse array pode ser alterado, causando a mudança imediata da imagem.
Para poder utiliza-lo, entretanto, algumas superfícies precisam estar trancadas (locked). Fazemos isso através da função SDL_LockSurface, que pode retornar 0 em caso de sucesso ou -1 em caso de falha. Esse comando prepara a superfície para manipulação direta de bits. O comando oposto, SDL_UnlockSurface, também deve ser usado, quando terminarmos de manipular a imagem.
Não exploraremos muito essa funcionalidade aqui, mas exemplos de seu uso podem ser encontrados no artigo SDL em processamento de imagens, do Diogo RBG.
Concluindo
Nesse artigo vimos como carregar imagens de maneira multiplataforma usando a SDL. Essa é uma funcionalidade importante pois o OpenGL não possui método nativo para carga de figuras. Também vimos como analisar o tipo da imagem, o que nos permitirá criar uma classe inteligente para carga de texturas no OpenGL.
Veja mais em:
Ótimo artigo, eu já procurei muito sobre isso, esse tipo de material sempre esta em falta.
Legal o seu artigo cara. É bom ver que cada vez mais surgem bons materiais em português sobre desenvolvimento de jogos.
Obrigado, pessoal!
Informando link quebrado:
http://www.skhaz.com/blog/carregando-imagens-com-ou-sem-canal-alpha/