Como no case dos VBOs e VAOs, texturas são objetos que precisam ser gerados primeiro pela chamada á uma função. Não deve ser uma surpresa nesse ponto como essa função é chamada.
GLuint tex;
glGenTextures(1, &tex);
Texturas são tipicamente usadas para imagens que decoram modelos 3D, mas na realidade elas podem ser usadas para armazenar uma grande variedade de tipos de dados. É possível ter texturas de 1D, 2D e 3D, que pode ser usada para armazenas dados volumosos na GPU. Um exemplo de um outro uso para texturas é o armazenamento de informações sobre o terreno. Esse artigo irá dar atenção para o uso de texturas para imagens, mas os princípios geralmente se aplicam para todos os tipos de textura.
glBindTexture(GL_TEXTURE_2D, tex);
Como outros objetos, texturas tem que ser ativadas para que seja possível efetuar operações com ela. Como imagens 2D são arrays de pixels, elas serão ativadas com o alvo GL_TEXTURE_2D.
Os pixels da textura serão endereçados usado coordenadas de textura durante as operações de desenho. Essas coordenadas variam de 0.0 a 1.0 onde (0, 0) é convencionalmente o canto inferior esquerdo e (1, 1) o canto superior direito da imagem usada como textura; A operação que usa essas coordenadas para recuperar a informação de cor dos pixels é chamada de sampling. Existem diversas maneiras para lidar com esse problema, cada uma sendo apropriada para cenários diferentes. O OpenGL lhe oferece muitas opções para controlar como o sampling é feito, sendo que as mais comuns serão discutidas nesse artigo.
Encapsulamento
A primeira coisa que você terá que considerar é como a textura deve serlida quando uma coordenada fora da faixa de 0 a 1 é dada. O OpenGL oferece 4 maneiras de lidar com isso:
GL_REPEAT
: A parte inteira da coordenada será ignorada e um padrão repetido é formado.GL_MIRRORED_REPEAT
: A textura também será repetida, mas será espelhada quando a parte inteira é impar.GL_CLAMP_TO_EDGE
: A coordenada será simplesmente espremida (clamping) entre 0 e 1.GL_CLAMP_TO_BORDER
: As coordenadas que caírem fora da faixa serão dadas um valor de uma cor específica.
Essas explicações podem parecer um pouco difíceis de entender e como o OpenGL é sobre gráficos, vamos ver como esses casos devem parecer:
O clamping pode ser configurado por coordenada, onde o equivalente de (x, y, z) em coordenadas é chamado (s, t, r). Parâmetros de textura são alterados com as funções glTextParameter* como demonstrado a seguir:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
Como dito em outro artigo, o i indica o tipo do valor que você quer especificar. Se usar GL_CLAMP_TO_BORDER e quiser alterar a cor da borda, precisa alterar o valor de GL_TEXTURE_BORDER_COLOR passando um array de floats RGBA:
float color[] = { 1.0f, 0.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, color);
Essa operação irá configurar a cor da borda para vermelho.
Filtro
Como coordenadas de textura são independentes de resolução, elas nem sempre correspondem a um pixel exatamente. Isso acontece quando uma imagem de textura é esticada além de seu tamanho original ou quando é encolhida. O OpenGL oferece vários métodos para decidir a cor do pixel quando isso acontece. Esse processo é chamado de filtro e os seguintes métodos estão disponíveis:
GL_NEAREST
: Retorna o pixel mais próximo da coordenada.GL_LINEAR
: Retorna a média ponderada dos 4 pixels em torno das coordenada dadas.GL_NEAREST_MIPMAP_NEAREST
,GL_LINEAR_MIPMAP_NEAREST
,GL_NEAREST_MIPMAP_LINEAR
,GL_LINEAR_MIPMAP_LINEAR
: Amostragem a partir de mipmaps.
Antes de discutirmos mipmaps, vamos ver primeiro a diferença entre as interpolações por proximidade ou linear. A original imagem é 16 vezes menor do que o retângulo onde ela foi desenhada.
Apesar da interpolação linear resultar em resultado mais suave, não é sempre a opção mais ideal. Interpolação por proximidade é mais adequada para jogos que querem simular gráficos de 8 bits, por causa da aparência pixelizada.
Você pode especificar o tipo de interpolação que deve ser usada para dois casos separados: escalonar a imagem para um tamanho menor ou para um tamanho maior. Esses dois casos são identificados pelas palavra-chaves GL_TEXTURE_MIN_FILTER
e GL_TEXTURE_MAG_FILTER
.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Como vimos, existe uma outra maneira de filtrar as texturas: mipmaps. Mipmaps são pequenas cópias de sua textura que foram encolhidas e filtrada de antemão. É recomendado que você use-as porque resultam tanto em uma qualidade mais alta quanto em uma performance melhor.
glGenerateMipmap(GL_TEXTURE_2D);
A geração de um mipmap é tão simples quanto chamar a função acima, então não há desculpa para não usa-la! Note que você tem que carregar a imagem da textura você mesmo antes que os mipmaps possam ser gerados a partir dela.
To use mipmaps, select one of the four mipmap filtering methods.
Para usar mipmaps, selecione um dos quatro métodos de filtragem de mipmaps.
GL_NEAREST_MIPMAP_NEAREST
: usa o mipmap que mais se aproxima do tamanho do pixel sendo texturizado e usa a interpolação por proximidade.GL_LINEAR_MIPMAP_NEAREST
: usa o mipmap mais próximo com a interpolação linear.GL_NEAREST_MIPMAP_LINEAR
: usa os dois mipmaps mais próximos do tamanho do pixel sendo texturizado com a interpolação por proximidade.GL_LINEAR_MIPMAP_LINEAR
: usa os dois mipmaps mais próximos com a interpolação linear.
Existem alguns outros parâmetros de texturização disponíveis, mas eles são mais adequados para operações especializadas. Você pode ler sobre eles na especificação.
Carregando imagens de texturas
Agora que o objeto da textura foi configurado é hora de carregar a imagem da textura. Isso é feito simplesmente carregando um array de pixels no objeto:
// Black/white checkerboard
float pixels[] = {
0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f
};
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 2, 2, 0, GL_RGB, GL_FLOAT, pixels);
O primeiro parâmetro depois do alvo da textura é o nível de detalhe, onde 0 é a imagem base. Esse parâmetro pode ser usado para carregar suas próprias imagens mipmap. O segundo parâmetro especifica o formato interno do pixel, que é o formato no qual os pixels serão armazenados na placa de vídeo. Muitos formatos diferentes estão disponíveis, incluindo formatos comprimidos, então certamente é bom dar uma olhada em todas as opções. O terceiro e quarto parâmetros especificam a largura e altura da imagem. O quinto parâmetro deve sempre ter o valor 0 de acordo com especificação. Os próximos dois parâmetros descrevem o formato dos pixels no array que será carregado e o parâmetro final especifica o próprio array. A função começa a carregar a imagem pela coordenada (0, 0), então preste atenção a isso.
Mas como o array de pixels é estabelecido? Texturas em aplicações gráficas normalmente serão mais sofisticadas do que padrões simples e serão carregadas de arquivos. A melhor prática seria ter arquivos em um arquivo suportado nativamente pelo hardware, mas algumas vezes pode ser mais conveniente carregar texturas de formatos de imagem comuns como JPG e PNG. Infelizmente, o OpenGL não oferece nenhum função auxiliar para carregar pixels desses arquivos de imagem, mas aqui é onde bibliotecas de terceiro podem ajudar. A biblioteca SOIL será discutida aqui junto com algumas outras alternativas.
SOIL
A biblioteca SOIL (Simple OpenGL Image Library) é uma biblioteca pequena e fácil de usar para carregar arquivos de imagens diretamente em objetos de textura ou cria-los para você. Você pode começar a usa-las em seu projeto pela lincagem dele com SOIL e adicionando o diretório src em seu caminho de arquivos de cabeçalho. Ela inclui arquivos de projeto do Visual Studio para que você possa compila-la junto com seu projeto.
Apesar da SOIL incluir funções para criar automaticamente uma textura a partir de uma imagem, estas funções usam funcionalidades que não estão disponíveis com o OpenGL moderno. Por causa disso, simplesmente usaremos SOIL para carregar a imagem e criaremos a textura nós mesmos.
int width, height;
unsigned char* image =
SOIL_load_image("img.png", &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, image);
Você pode começar a configurar os parâmetros da textura e geração dos mipmaps depois disso.
SOIL_free_image_data(image);
Você pode limpar os dados da imagem logo depois de tê-la carregado na textura.
Como mencionado anteriormente, o OpenGL espera que o primeiro pixel esteja localizado no canto inferior esquerdo, o que significa que as texturas serão invertidas quando carregadas com o SOIL. Para contra-atacar isso, o código do tutorial usará coordenas Y invertidas de agora em diante. Isso significa que (0, 0) será assumida como o canto superior esquerdo ao invés do inferior esquerdo. Essa prática pode tornas as coordenadas das texturas mais intuitivas como efeito colateral.
Opções alternativas
Outras bibliotecas que suportam uma ampla variedade de tipos de arquivos como a SOIL são a DevIL e FreeImage. Se você estiver interessado em apenas um tipo de arquivo, é pssível usar bibliotecas como libpng e libjpeg diretamente. Se você for corajoso, pode dar uma olhada na especificação dos formatos BMP e TGA. Não é difícil implementar um carregador próprio para esses formatos.
Usando uma textura
Como vimos, texturas são aplicadas usando coordenadas de texturas e você precisará adicionar esses atributos em seus vértices. Vamos modificar o exemplo deste artigo para incluir essas coordenadas. O novo array de vértices irá agora incluir as coordenadas s e t para cada vértice:
float vertices[] = {
// Position Color Texcoords
-0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // Top-left
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // Top-right
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, // Bottom-right
-0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f // Bottom-left
};
O vertex shader precisa ser modificado para que essas coordenadas de textura sejam interpoladas sobre os fragmentos:
...
in vec2 texcoord;
out vec3 Color;
out vec2 Texcoord;
...
void main()
{
Texcoord = texcoord;
Assim como fizemos quando o atributo de cor foi adicionado, os ponteiros de atributos precisam ser adaptados para o novo formato:
glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE,
7*sizeof(float), 0);
glVertexAttribPointer(colAttrib, 3, GL_FLOAT, GL_FALSE,
7*sizeof(float), (void*)(2*sizeof(float)));
GLint texAttrib = glGetAttribLocation(shaderProgram, "texcoord");
glEnableVertexAttribArray(texAttrib);
glVertexAttribPointer(texAttrib, 2, GL_FLOAT, GL_FALSE,
7*sizeof(float), (void*)(5*sizeof(float)));
Como dois floats foram adicionados para as coordenadas, um vértice tem agora 7 floats de tamanho e a coordenada da textura consiste de 2 desses floats.
Agora só resta uma coisa: fornecer acesso à textura no fragment shader para seleção dos pixels a partir dela. Isso é feito pelo acréscimo de um uniform do tipo sample2D, que irá ter o valor padrão de 0. Isso só precisa ser alterado quando se precisa dar acesso à múltiplas texturas, o que será considerado em artigos futuros.
Para esse exemplo, a imagem do gato usada acima será carregada usado a biblioteca SOIL. Certifique-se de que a imagem esteja no mesmo diretório da aplicação.
int width, height;
unsigned char* image =
SOIL_load_image("sample.png", &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, image);
SOIL_free_image_data(image);
Para selecionar um pixel a partir da textura 2D usando o sampler, a função texture pode ser chamada com o sampler relevante e as coordenadas da textura como parâmetros. Também multiplicamos a cor selecionada com o atributo de cor para obter um efeito interessante. Seu fragment shader deve se parecer com isso:
#version 150
in vec3 Color;
in vec2 Texcoord;
out vec4 outColor;
uniform sampler2D tex;
void main()
{
outColor = texture(tex, Texcoord) * vec4(Color, 1.0);
}
Ao executar esta aplicação, você deve obter o seguinte resultado:
Se você obter uma tela vazia, certifique-se de seus shader foram compilados com sucesso e que a imagem foi carregada. Se não conseguir encontrar o problema, tente comparar o seu código com este código fonte.
Unidades de texturas
O sampler em seu fragment shader está ligado a unidade de textura 0. Unidades de textura são referências a objetos de textura que podem ser selecionados em um shader. Texturas são ligadas a unidades de textura usando a função glBindTexture que foi usada anteriormente. Como você não especifica explicitamente qual unidade de textura usar, a textura foi ligada à GL_TEXTURE0. Por isso o valor padrão 0 para o sampler em seu shader funcionou.
A função glActiveTexture especifica com qual unidade de textura um objeto de textura será ligado quando glBindTexture é chamado.
glActiveTexture(GL_TEXTURE0);
A quantidade de unidades de textura suportado difere para cada placa de vídeo, mas será ao menos 48. É seguro dizer que você nunca atingirá esse limite mesmo em aplicações gráficas extremamente complexas.
Para praticar a seleção de múltiplas texturas, vamos tentar misturar as imagem do gato e de um filhote para obter o melhor de dois mundos! Vamos primeiro modificar o fragment shader para selecionar as duas texturas e misturar os pixels:
...
uniform sampler2D texKitten;
uniform sampler2D texPuppy;
void main()
{
vec4 colKitten = texture(texKitten, Texcoord);
vec4 colPuppy = texture(texPuppy, Texcoord);
outColor = mix(colKitten, colPuppy, 0.5);
}
A função mix usada acima é uma função especial do GLSL que interpola linearmente duas variáveis baseando-se no terceiro parâmetro. Um valor 0.0 irá resultar no primeiro valor, um valor de 1.0 resultará no segundo e um valor no meio irá resultar numa mistura entre os dois valores.
Agora que os dois samplers estão prontos, você terá que associar as duas primeiras unidades de texturas a eles e ligar as duas texturas a essas unidades. Isso é feito pela adição da chamada apropriada à glActiveTexture ao código de carregamento da textura.
GLuint textures[2];
glGenTextures(2, textures);
int width, height;
unsigned char* image;
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textures[0]);
image = SOIL_load_image("sample.png", &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, image);
SOIL_free_image_data(image);
glUniform1i(glGetUniformLocation(shaderProgram, "texKitten"), 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, textures[1]);
image = SOIL_load_image("sample2.png", &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, image);
SOIL_free_image_data(image);
glUniform1i(glGetUniformLocation(shaderProgram, "texPuppy"), 1);
As unidades de texturas dos samplers são configuradas usando a função glUniform que vimos nesse artigo. Ela simplesmente aceita um inteiro que especifica a unidade de textura. Esse código deve resultar na seguinte imagem:
Você pode dar uma olhada neste código fonte se tiver alguma problema em fazer o programa funcionar.
Agora que o processo de seleção de texturas foi discutido, você está pronto para mergulhar no tópico de transformações e por fim 3D. O conhecimento que você tem nesse ponto deve ser suficiente para produzir diversos tipos de jogos 2D, exceto quando algum tipo de transformação for necessária.
Fonte: open.gl/textures