Programação com OpenGL/Modern OpenGL Tutorial 06
Carregando uma textura
[editar | editar código-fonte]Para carregar uma textura, nós precisamos de um decoficador para carregar uma imagens, em particular as do tipo JPEG ou PNG. Normalmente, seu programa final usara uma biblioteca genérica como o SDL_Image, SFML ou Irrlich, que suportam vários formatos de imagem, assim você não precisa escrever o código da imagem para carrega-lo. Bibliotecas especializadas como o SOIL(veja abaixo) pode ser interessante.
Para o primeiro passo, nós precisamos manipular uma imagem em nível-baixo para entender o básico, assim faremos um truque: O GIMP pode exportar uma imagem para um código fonte C. que poderá estar em nosso programa! Eu usarei a opção de salvamento igual a captura de tela.
Se tiver demanda, nós podemos providenciar um tutorial especial para leitura de formato simples como o PNM, ou do tipo como BMP ou TGA (este dois também são simples, mas suportam compressão e vários formatos por isto é um pouco difícil suportar todas as suas opções).
Nota: Agregar imagem em código C é muito eficiente em questão de memória, por isto não vamos usar sempre. Tecnicamente: ela é armazenada em um segmento BBS do programa, em vez do heap, por isto não pode ser liberado.
Nota 2: você pode encontrar a fonte do GIMP em res_texture.xcf no repositório de códigos.
Ta compilar automaticamente o aplicativo quando você modificar o res_texture.c, coloque ele no Makefile:
cube.o: res_texture.c
Criando uma textura para o OpenGL buffer
[editar | editar código-fonte]Um buffer é basicamente um espaço na memória da placa de vídeo, assim o OpenGL pode acessá-lo rapidamente.
/* Globais */
GLuint texture_id;
GLint uniform_mytexture;
/* init_resources */
glGenTextures(1, &texture_id);
glBindTexture(GL_TEXTURE_2D, texture_id);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, // target
0, // level, 0 = base, sem minimap,
GL_RGB, // internalformat (formato interno)
res_texture.width, // largura
res_texture.height, // altura
0, // borda, sempre em 0 no OpenGL ES
GL_RGB, // formato
GL_UNSIGNED_BYTE, // tipo
res_texture.pixel_data);
/* render */
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture_id);
glUniform1i(uniform_mytexture, /*GL_TEXTURE*/0);
/* free_resources */
glDeleteTextures(1, &texture_id);
Coordenadas da textura
[editar | editar código-fonte]Agora nós precisamos dizer onde cada vertex será localizada em nossa textura.
Para isto, vamos substituir o atributo v_color
pelo vertex shader com um texcoord
:
GLint attribute_coord3d, attribute_v_color, attribute_texcoord;
/* init_resources */
attribute_name = "texcoord";
attribute_texcoord = glGetAttribLocation(program, attribute_name);
if (attribute_texcoord == -1) {
fprintf(stderr, "Could not bind attribute %s\n", attribute_name);
return 0;
}
Agora, que parte da textura nós mapearemos, para dizer, no canto superior esquerdo da face da frente? Bem isto depende:
- Para a face frontal: o canto superior esquerdo da nossa textura
- Para o topo da face: o canto inferior-esquerdo da nossa textura
Nós vemos que múltiplos pontos da textura será anexada em algumas vértices, O vertex shader não será capaz de decidir qual deve ser escolhido.
Assim nós precisamos reescrever o cubo usando 4 vértices por face, sem reutilizar as vértices.
Para começar enfim, vamos apenas trabalhar com a face frontal. Facil! nós apenas teremos na tela os 2 primeiros triângulos (as 6 primeiras vértices):
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
Assim, as coordenadas da nossa textura estarão entre [0, 1], com o eixo x da esquerda para direita, e o eixo y de baixo para cima:
/* init_resources */
GLfloat cube_texcoords[] = {
// front
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
};
glGenBuffers(1, &vbo_cube_texcoords);
glBindBuffer(GL_ARRAY_BUFFER, vbo_cube_texcoords);
glBufferData(GL_ARRAY_BUFFER, sizeof(cube_texcoords), cube_texcoords, GL_STATIC_DRAW);
/* onDisplay */
glEnableVertexAttribArray(attribute_texcoord);
glBindBuffer(GL_ARRAY_BUFFER, vbo_cube_texcoords);
glVertexAttribPointer(
attribute_texcoord, // atributos
2, // numero de elementos por vértices, que é (x,y)
GL_FLOAT, // o tipo de cada elemento
GL_FALSE, // como o valor está
0, // sem dados extras em cada posição
0 // deslocamento do primeiro elemento
);
Vertex shader:
attribute vec3 coord3d;
attribute vec2 texcoord;
varying vec2 f_texcoord;
uniform mat4 mvp;
void main(void) {
gl_Position = mvp * vec4(coord3d, 1.0);
f_texcoord = texcoord;
}
Fragment shader:
varying vec2 f_texcoord;
uniform sampler2D mytexture;
void main(void) {
gl_FragColor = texture2D(mytexture, f_texcoord);
}
Mas o que houve? Nossa textura está de cabeça para baixo!
Na convenção do OpenGL (começando no canto inferior esquerdo) é diferente das aplicações em 2D (começo no canto superior esquerdo). Para corrigir isto podemos:
- Ler as linhas do pixel de baixo para cima
- Trocar as linhas do pixel
- Trocar as coordenadas Y da textura
A maior parte das bibliotecas gráficas retornam os array dos pixel como na convenção 2D. Porem o DevIL é uma opção que mantém a origem como conhecemos. Como alternativa, temos alguns formatos como BMP e TGA que guardar as linhas do pixel de baixo para cima nativamente (o que pode explicar a certa popularidade que pesa para o TGA entre os desenvolvedores 3D), muito util quando se escrever um carregador próprio para ele.
Trocando as linhas do pixel podemos terminar o código C que rodaremos, se o seu programa está em linguagem de nível alto como o Python isto pode ser feito apenas em uma linha. A desvantagem é que o carregamento da textura será mais lento por causa dos passos extras.
Revertendo a coordenada da textura é um meio mais fácil para nós, podemos fazer isto no fragment shader:
void main(void) {
vec2 flipped_texcoord = vec2(f_texcoord.x, 1.0 - f_texcoord.y);
gl_FragColor = texture2D(mytexture, flipped_texcoord);
}
Certo, tecnicamente nós poderíamos escrever as coordenadas da textura em outra direçõe logo de cara - mas outros aplicativos 3D costumam trabalhar como nós descrevemos.
Um cubo completo
[editar | editar código-fonte]Assim como nós dizemos, vamos especificar vértices independentes para cada face:
GLfloat cube_vertices[] = {
// frente
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
// Em cima
-1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, -1.0,
-1.0, 1.0, -1.0,
// Atrás
1.0, -1.0, -1.0,
-1.0, -1.0, -1.0,
-1.0, 1.0, -1.0,
1.0, 1.0, -1.0,
// Em baixo
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0, 1.0,
-1.0, -1.0, 1.0,
// Esquerda
-1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, 1.0, -1.0,
// Direita
1.0, -1.0, 1.0,
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
1.0, 1.0, 1.0,
};
Para cada face, as vértices serão colocadas em sentido anti-horário( Como o espectador esta encarando as faces). Como consequência, o mapeamento da textura será a mesma em todas as faces:
GLfloat cube_texcoords[2*4*6] = {
// front
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
};
for (int i = 1; i < 6; i++)
memcpy(&cube_texcoords[i*4*2], &cube_texcoords[0], 2*4*sizeof(GLfloat));
Aqui nós especificamos o mapeamento para a face da frente, e copiaremos isto nas 5 faces restantes.
Se as faces que estivessem em sentido horário, a textura seria mostrada espelhada depois disto. Não existe nenhuma convenção sobre a orientação, se você fizer corretamente as coordenadas da textura ficarão devidamente mapeadas.
Os elementos do cubo são escritos similarmente, com 2 triângulo como índices(x, x+1, x+2), (x+2,x+3,x):
GLushort cube_elements[] = {
// frente
0, 1, 2,
2, 3, 0,
// topo
4, 5, 6,
6, 7, 4,
// fundo
8, 9, 10,
10, 11, 8,
// em baixo
12, 13, 14,
14, 15, 12,
// esquerda
16, 17, 18,
18, 19, 16,
// direita
20, 21, 22,
22, 23, 20,
};
...
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_cube_elements);
int size; glGetBufferParameteriv(GL_ELEMENT_ARRAY_BUFFER, GL_BUFFER_SIZE, &size);
glDrawElements(GL_TRIANGLES, size/sizeof(GLushort), GL_UNSIGNED_SHORT, 0);
Para ficar mais divertido, e para ver a face inferior, vamos implementar 3 movimentos de rotação mostradas no tutorial flying cube do NeHe´s, no onIdle
:
float angle = glutGet(GLUT_ELAPSED_TIME) / 1000.0 * 15; // base 15° per second
glm::mat4 anim = \
glm::rotate(glm::mat4(1.0f), angle*3.0f, glm::vec3(1, 0, 0)) * // X axis
glm::rotate(glm::mat4(1.0f), angle*2.0f, glm::vec3(0, 1, 0)) * // Y axis
glm::rotate(glm::mat4(1.0f), angle*4.0f, glm::vec3(0, 0, 1)); // Z axis
Está pronto!
Usando o SOIL
[editar | editar código-fonte]Em desenvolvimento
o SOIL provém uma maneira de carregar um arquivo de imagem PNG, JPG e alguns outros formatos, feitos para integrar com o OpenGL. É uma biblioteca bastante simples com nenhuma dependência. É usada com cobertura do SFML (Embora o SFML também use o libjpeg e libpng diretamente).
Para instala-lo:
aptitude install libsoil-dev
Referencie ele no seu Makefile:
LDLIBS=-lglut -lSOIL -lGLEW -lGL -lm
Uma função de nivel-alto permite que você carregue diretamente para um contexto OpenGL:
glActiveTexture(GL_TEXTURE0);
GLuint texture_id = SOIL_load_OGL_texture
(
"res_texture.png",
SOIL_LOAD_AUTO,
SOIL_CREATE_NEW_ID,
SOIL_FLAG_INVERT_Y
);
if(texture_id == 0)
cerr << "SOIL loading error: '" << SOIL_last_result() << "' (" << "res_texture.png" << ")" << endl;
- O
SOIL_FLAG_INVERT_Y
intermediá a reversão da coordenada Y como dizemos acima. - O SOIL também adapta as texturas NPOT(as não potencia de 2), quando a placa gráfica não manipula diretamente
Note que com este método, você não tem acesso as dimensões da imagem. Para isto, você precisa de uma API de nivel-baixo:
int width, height;
unsigned char* img = SOIL_load_image("res_texture.png", &width, &height, NULL, 0);
glGenTextures(...
...