Mapeamento de textura no OpenGL

Uma das ferramentas mais poderosas em gráficos de computador é o mapeamento de texturas. O mapeamento de texturas aplica uma imagem a uma superfície. Modelar uma superfície complexa frequentemente não é uma tarefa prática por causa do nível de detalhes necessário e por ser difícil renderizar esses detalhes de forma acurada. Ao invés disso, o mapeamento de texturas permite que um polígono simples aparente ter uma superfície complexa.

Nesse artigo, estaremos trabalhando com o código disponível nesse exemplo: tutorial4.zip.

Note que a classe CTexture que será usada para ler arquivo PPM ou BMP. O critério para uma textura OpenGL é:

  • Ao menos ter 4×4.
  • Altura e largura podem ser diferentes, mas devem ser potências de 2.

Nesse tutorial, iremos:

  1. Adicionar novas texturas ao cubo.
  2. Mapear a imagem de um globo a uma esfera.
  3. Mapear textura entre dois arcos ligados.

Executando o programa

Quando você executa o programa, verá um cubo. Teste as opções do menu Lab Stuff.  Note que o código inclui um cubo, uma esfera e dois toros conectados. O cubo possui uma textura na face frontal. Você pode abrir o arquivo plank01.bmp para visualizar a imagem que é posta sobre a face do cubo. Vamos dar uma olhada no que está acontecendo aqui.

Gerenciamento básico de imagem

Primeiro, dê uma olhada no arquivo Texture.cpp.  Essa classe foi criada para armazenar as imagens usadas como textura. Ela possui funções membro para carregar uma imagem de um arquivo. Você também pode criar suas próprias imagens manualmente se quiser. Uma imagem é armazenada dessa forma:

BYTE ** m_image;

O array para a imagem é alocado dessa forma:

BYTE *image = new BYTE[usewidth * m_height];
m_image = new BYTE *[m_height];
for(int i=0; i<m_height; i++, image += usewidth)
{
m_image[i] = image;
}

Note que isso PARECE com um array 2D.  A imagem de fato está em um array 1D, de forma que os dados da imagem são sequenciais.  Isso é importante porque o OpenGL espera apenas um ponteiro único para toda a memória de sua imagem. Como queremos ser capazes de acessar a memória como um array 2D, criamos um array simples de ponteiros para cada linha de dados.

No exemplo, estaremos usando o ordenamento de byte Blue, Green, Red (BGR).  Assim, uma coluna possui 3 vezes a largura da imagem por existirem três bytes por pixel. Então os dados serão simplesmente uma sequência BGRBGRBGR.

Informe o OpenGL sobre a textura

Temos que fazer diversas coisas para permitir que usemos uma imagem como uma textura no OpenGL. O primeiro passo é obter um inteiro que servirá como identificação da textura, que o OpenGL chama de nome da textura. Para isso, precisamos entender o conceito de contexto de renderização do OpenGL.

Durante a execução da função OnGLDraw, você pode usar livremente funções OpenGL. Porém, não pode usar funções OpenGL em outras partes do programa. Como exemplo, você não pode fazer nenhuma chamada OpenGL no construtor de CChildView. Quando esse construtor é executado, o OpenGL ainda não foi inicializado. Quando OnGLDraw é executado, existe um contexto de renderização OpenGL, o que significa que o OpenGL está ativo e pronto para receber comandos. Quando a função é finalizada, o contexto é novamente desligado.

Nessa aplicação, isso significa que as chamadas para inicialização da textura precisam ser feitas em OnGLDraw. (Existem outras opções, discutidas mais tarde). Como queremos executar LoadFile para CTexture no construtor, precisamos de uma forma fácil de inicializar o OpenGL quando estamos prontos para fazer isso.

Existe uma função membro CTexture::TexName(). Essa função retorna o nome da textura. Porém, na primeira vez que é chamada, ela não terá um nome de textura. Ao invés disso, criará uma e ajustará a textura para uso com o OpenGL. Abaixo segue o códio para fazer isso (de Texture.cpp):

if(m_initialized)
return m_texname;

glGenTextures(1, &m_texname);
glBindTexture(GL_TEXTURE_2D, m_texname);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_width, m_height, 0,
GL_BGR_EXT, GL_UNSIGNED_BYTE, m_image[0]);

m_initialized = true;

return m_texname;

A função glGenTextures() cria um nome de textura. Tudo que isso está realmente fazendo é garantindo que você tenha um inteiro único que não esteja sendo usado como ID de textura. Em seguida, associamos esse nome como textura 2D atual. As chamadas seguintes todas operam sobre a textura atual. Como associamos a textura ao nosso novo ID, essas chamadas atuarão sobre ela.

As duas primeiras chamadas para glTexParameter informam ao OpenGL que queremos ser capazes de “ladrilhar” nossa textura. O ladrilhamento significa simplesmente que a textura será repetida se formos além de suas bordas. Note que algumas texturas funcionam bem com ladrilhamento enquanto outras nem tanto. Você pode ter que executar algum trabalho com a textura para que ela possa ser ladrilhada. As dimensões S e T são das dimensões X e Y da textura. O OpenGL usa outras variáveis para que você não confunda S e T com X e Y.

As próximas duas chamadas para glTexParameter informam o OpenGL que iremos usar interpolação bilinear para determinar a cor entre dois pixels da textura. A outra opção é  GL_NEAREST, que frequentemente é mais rápida, mas não tem uma boa qualidade.

Finalmente, a chamada para glTexImage2D informa ao OpenGL onde encontrar os dados para a textura. Note que isso é uma referência, assim não apague os dados até que esteja certo de que não irá usa-los novamente..

Carregando a imagem

Normalmente é mais fácil adicionar objetos CTexure como membros de CChildView e carregar a imagem no construtor de CChildView dessa forma:

m_wood.LoadFile(“plank01.bmp”);

Isso simplesmente carrega o arquivo de imagem  plank01.bmp no objeto.

Usando a imagem

Vá para a função CChildView::Box().  Essa função desenha os 6 quadriláteros que formam as faces do cubo. O primeiro deles é mapeado para usar uma textura. Para mapear uma primitiva para usar uma textura, siga os seguintes passos:

Ative a textura que quer usar dessa forma:

glEnable(GL_TEXTURE_2D);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glBindTexture(GL_TEXTURE_2D, m_wood.TexName());

A primeira chamada ativa o mapeamento da textura. Certifique-se de desativar isso quando terminar, ou tudo será texturizado. Você verá um glDisable(GL_TEXTURE_2D) depois que o quadrilátero é desenhado.  A segunda linha configura o modo do ambiente da textura para GL_MODULATE.  Existem diversas maneiras para texturas poderem ser combinadas com as cores da superfície.  GL_REPLACE pode ser especificada para desenhar a superfície apenas com as cores da textura. Mas, como estamos usando iluminação nesse exemplo e queremos que a intensidade da superfície varie dependendo da iluminação sobre ela.  GL_MODULATE informa que a cor computada para a superfície é multiplicada pela cor da textura.

AVISO:  Quando você mapeia uma textura em uma superfície, a cor da superfície que você escolhe será multiplicada pela cor da textura. Se a cor da superfície não for branca, você pode acabar com um tingimento de sua textura. Além disso, você perde o bom realce especular quando aplica texturas no OpenGl 1.1.  (a versão 1.2 corrige esse problema em algumas implementações).

A última linha simplesmente associal a textura como textura 2D atual. Podemos agora usar a textura.

Considere a texture como um papel de parede de borracha. Você pode escolher pontos da textura e anexa-los aos vértices. As coordenadas da textura variam de 0 a 1.  Se tivermos ativado GL_REPEAT (como de fato fizemos no exemplo), podemos acessar a textura além dessas coordenadas e ela será repetida. Como exemplo, nossa textura possui quatro cantos:  (0,0), (1, 0), (1,1) e (0,1).  Mas a imagem é repetida em (1,0), (2, 0), (2,1), (1, 1).

Associaremos os cantos da textura aos cantos da imagem. O código para fazer isso é:

glBegin(GL_QUADS);
glNormal3d(0, 0, 1);
glTexCoord2f(0, 0);
glVertex3dv(a);
glTexCoord2f(1, 0);
glVertex3dv(b);
glTexCoord2f(1, 1);
glVertex3dv(c);
glTexCoord2f(0, 1);
glVertex3dv(d);
glEnd();

Note a chamada para glTexCoord2f para especificar as coordenadas da textura.  Essas coordenadas precisam ser especificadas ANTES da chamada à glVertex.  Note a chamada para glDisable depois que tudo isso é feito.

Iss é tudo o que precisa ser feito para por a imagem na face frontal do cubo. Execute o programa mais uma vez para se certificar que entenda o que é feito.

Vamos mapear o Mundo em um lado do cubo

Agora, iremos mapear uma textura em outro lado do cubo. Em primeiro lugar, adiciona uma nova variável membro a CChildView do tipo CTexture chamada m_worldmap. Em seguida, adicionaremos o seguinte código ao construtor de CChildView:

m_worldmap.LoadFile(“worldmap.bmp”);

Esse arquivo esta no mesmo diretório onde estão todos os outros arquivos do projeto.

Agora, usaremos ele. Mova a chamada para glDisable localizada a antes do segundo quadrilátero do cubo para depois dele. No lugar onde ele estava, ponha a linha:

glBindTexture(GL_TEXTURE_2D, m_worldmap.TexName());

Agora, a cada chamada a glVertex(), queremos adicionar uma coordenada de textura.  Se olhar como essa face é definida, verá que ela é desenhada especificando os vértices em um sentido anti-horário começando com o canto superior esquerdo. Queremos que o canto superior esquerdo do mapeamento corresponda a esse vértice para em seguida especificar as demais coordenadas em sentido anti-horário em torno do mapeamento. Note que você pode inverter a ordem e virar o mapa ou fazer coisas estranhas com ele. Você pode tentar outras ordens para ver o que acontece. Quando tiver terminado, o código deve ficar dessa forma:

glBindTexture(GL_TEXTURE_2D, m_worldmap.TexName());

// Right
glBegin(GL_QUADS);
glNormal3d(1, 0, 0);
glTexCoord2f(0, 1);
glVertex3dv(c);
glTexCoord2f(0, 0);
glVertex3dv(b);
glTexCoord2f(1, 0);
glVertex3dv(f);
glTexCoord2f(1, 1);
glVertex3dv(g);
glEnd();

glDisable(GL_TEXTURE_2D);

Execute isso e veja se funciona de forma correta.

Mapeando um Globo em uma Esfera

Nós temos uma esfera nesse código. Não seria legal mapear o mundo nessa esfera? Mas o que seria preciso para fazer isso?

Em primeiro lugar, vamos ativar a textura. Adicione o seguinte código na função Sphere antes da chamada a SphereFace.

glEnable(GL_TEXTURE_2D);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glBindTexture(GL_TEXTURE_2D, m_worldmap.TexName());

Uma questão que você deve ter: por quê usar glTexEnvf quando essa função não foi usada para mapear a textura no cubo? Sabemos que não havia nenhum outro código entre as faces do cubo que poderiam alterar GL_TEXTURE_ENV_MODE.  Mas não sabemos se não alterado em algum outro local. Além do mais. se chamarmos sphere sem chamar cube, o parâmetro pode nunca ter sido configurado.

O problema que interessa  para a esfera é determinar as coordenadas de cada vértice. Uma maneira de encarar esse problema é usar a normal da superfície como uma maneira de informar onde estamos na face da esfera. Se considerarmos ele como um vetor ao lado da esfera, podemos facilmente calcular a latitude e longitude na superfície do globo. Esses valores correspondem aos pontos do mapeamento.

Logo depois da chamada a glBegin(GL_TRIANGLES), adicione o seguinte código:

// What’s the texture coordinate for this normal?
tx1 = atan2(a[0], a[2]) / (2. * GR_PI) + 0.5;
ty1 = asin(a[1]) / GR_PI + .5;

glTexCoord2f(tx1, ty1);

Então, o que isso faz?  a[0] e a[2] são os valores X e Z da normal.  Se você olhar o globo de cima para baixo, o vetor formado pelos valores de X e Z informarão a longitude do globo! Usamos atan2 para conveter isso para um ângulo em radianos.  Esse ângulo está entre -PI e PI.  Dividimos ele por 2PI, de forma que agora está entre -0.5 e 0.5.  Adicionando 0.5 faz que varie de  0 a 1.  Esse será o valor X no mapeamento da textura.

Em seguida, calculamos o valor Y do mapeamento da textura.  a[1] é o valor Y da normal.  Se considerarmos um triângulo a direita com uma hipotenusa de tamanho 1 (nosso vetor normalizado) e um acréscimo de Y, podemos calcular o ângulo usando asin. Esse ângulo fica entre o vetor Y e o plano X/Z. Isso nos dá valores entre -PI / 2 e PI / 2.  ty1 então varia de 0 a 1.

Adiciona linhas como essa para os outros dois vértices. Eu sugiro não reutilizar tx1 e ty1, já que precisaremos alterar alguma coisa em algum momento.

Execute isso é gire o globo. Você perceberá que haverão partes do globo que estão bagunçadas. Tente por um momento entender o que está havendo. Em seguida, leia a resposta a seguir.

O problema é que alguns triângulos mapeiam as duas pontas do mapa. Afinal de contar, a borda direita do mapa se encontra com a borda esquerda. Imagine um triângulo pendurado em uma borda. O problema é que as funções trigonométricas simplesmente envolvem os valores. Assim, você acaba com um triângulo que possui dois vértices em uma das bordas do mapa e um na outra. Isso faz com que todo o mapeamento entre esses pontos seja esmagada na imagem mapeado no polígono.

Então, como corrigimos isso?  A solução mais fácil é checar por esse problema. Tente isso para o segundo vértice:

// The second vertex
tx = atan2(b[0], b[2]) / (2. * GR_PI) + 0.5;
ty = asin(b[1]) / GR_PI + .5;
if(tx < 0.75 && tx1 > 0.75)
tx += 1.0;
else if(tx > 0.75 && tx1 < 0.75)
tx -= 1.0;

glTexCoord2f(tx, ty);

Faça o mesmo para terceiro vértice (baseado no vetor c, naturalmente) e então você terá uma mapa perfeitamente mapeado.

 Traduzido de