Programação com OpenGL/Modern OpenGL Tutorial 06

Carregando uma textura

editar
 
Nossa textura, em 2D

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.

 
Exportando uma imagem como C pelo GIMP

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

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

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);
}
 
Alguma coisa está errada...

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

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);


 
Voe, cubo, voe!

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

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(...
  ...


Navegue e baixe os códigos completos