Programação com OpenGL/Modern OpenGL Tutorial Leitor de OBJ

Para ser mais rápido, vamos querer carregar um modelo existente que criamos no punho. Podemos importar alguns do Blender.

Aparentimente isto não é fácil carregarmos diretamente um arquivo .blender, mas nós podemos fazer assim:

  • File > Export em formato .obj, e escrevendo um leitor de OBJ.
  • File > Export em formato .3ds, e usando o lib3ds

Nós pensamos que escrever um leitor de OBJ é um bom exercício, então começaremos por ele.

Criando a Suzzane

editar
Ver também: Blender 3D
 
Wikipedia
A Wikipédia tem mais sobre este assunto:
Blender (software)#Suzanne
 
Captura de tela do Blender

Suzzane é um modelo de teste do Blender. ele possui 500 polígonos, o que será um bom teste para nós.

Para criá-lo, execute o Blender(Vamos usar a versão 2.58), depois:

  • Remova todos os elementos da cena (pressione A e depois X
  • No menu superior, clique em Add > Mesh > Monkey
  • Pressione n para mostrar o Transform panel e
    • Ajuste a localização para (0, 0, 0)
    • Ajuste a rotação para (90, 0, 0)
  • No menu superior, clique em File > Export > Wavefront (.obj)
    • Para manter a orientação do Blender, cuide que as seguintes estejam assim (para trocar para "Y-is-up" coordenadas padrão do OpenGL):
      • Forward: -Z Forward
      • Up: Y Up
    • Mexa em "Triangulate" para garantir que as faces sejam trianguladas evitando faces quadradas.

O Blender então criará dois arquivos, suzanne.obj e suzzane.mtl:

  • O arquivo .obj conterá as mesh(malha): de vértices e faces.
  • O arquivo .mtl conterá as informação dos materiais(Material Template Library)

Por enquanto vamos apenas carregar a malha.

Formação do arquivo

editar
 
Wikipedia
A Wikipédia tem mais sobre este assunto:
Wavefront .obj file

Examinando o arquivo .obj com um editor de texto, vamos ver que o formato do arquivo é muito simples:

  • Estruturado em linhas
  • Linhas iniciadas com # são comentários
  • o  : representa um novo objeto.
  • v  : representa um vertex
  • vn : representa uma normal
  • f  : representa uma face, usando o índice de vértice, iniciando em 1

Precisamos preencher algumas arrays em C:

  • De vértices
  • De elementos
  • De normals (usado em calcular a iluminação)

O formato também tem outros recursos, mas por enquanto vamos deixar eles de lado.

Aqui temos uma primeira implementação crua para trabalhamos nossos objetos.

Nosso interpretador será limitado (sem suporte para múltiplos objetos, alternado os formatos dos vértices, polígonos, etc.), mas isto será suficiente para o que precisamos.

void load_obj(const char* filename, vector<glm::vec4> &vertices, vector<glm::vec3> &normals, vector<GLushort> &elements) {
  ifstream in(filename, ios::in);
  if (!in) { cerr << "Não pode abrir o arquivo: " << filename << endl; exit(1); }

  string line;
  while (getline(in, line)) {
    if (line.substr(0,2) == "v ") {
      istringstream s(line.substr(2));
      glm::vec4 v; s >> v.x; s >> v.y; s >> v.z; v.w = 1.0f;
      vertices.push_back(v);
    }  else if (line.substr(0,2) == "f ") {
      istringstream s(line.substr(2));
      GLushort a,b,c;
      s >> a; s >> b; s >> c;
      a--; b--; c--;
      elements.push_back(a); elements.push_back(b); elements.push_back(c);
    }
    else if (line[0] == '#') { /* ignorando esta linha */ }
    else { /* ignoring this line */ }
  }

  normals.resize(mesh->vertices.size(), glm::vec3(0.0, 0.0, 0.0));
  for (int i = 0; i < elements.size(); i+=3) {
    GLushort ia = elements[i];
    GLushort ib = elements[i+1];
    GLushort ic = elements[i+2];
    glm::vec3 normal = glm::normalize(glm::cross(
      glm::vec3(vertices[ib]) - glm::vec3(vertices[ia]),
      glm::vec3(vertices[ic]) - glm::vec3(vertices[ia])));
    normals[ia] = normals[ib] = normals[ic] = normal;
  }
}

Nós usaremos vectors C++ para simplificarmos o gerenciamento de memória(sem malloc) Vamos passar argumentos por referencia, principalmente por que o acesso à ponteiro em vector fica horrível((*elements)[i])

Então podemos carregar o arquivo .obj assim:

  vector<glm::vec4> suzanne_vertices;
  vector<glm::vec3> suzanne_normals;
  vector<GLushort> suzanne_elements;
  [...]
  load_obj("suzanne.obj", suzanne_vertices, suzanne_normals, suzanne_elements);

E passaremos para OpenGL usando:

  glEnableVertexAttribArray(attribute_v_coord);
  // Descrevemos nossa array de vértices para o OpenGL (o formato não descoberto automaticamente)
  glBindBuffer(GL_ARRAY_BUFFER, vbo_mesh_vertices);
  glVertexAttribPointer(
    attribute_v_coord,  // atributo
    4,                  // numero de elementos por vértice, que é (x,y,z,w)
    GL_FLOAT,           // o tipo de cada elemento
    GL_FALSE,           // o valor que eles estão
    0,                  // sem dados extras em cada posição
    0                   // deslocamento do primeiro elemento
  );

  glBindBuffer(GL_ARRAY_BUFFER, vbo_mesh_normals);
  glVertexAttribPointer(
    attribute_v_normal, // attribute
    3,                  // numero de elementos por vértice, que é (x,y,z)
    GL_FLOAT,           // o tipo de cada elemento
    GL_FALSE,           // o valor que eles estão
    0,                  // sem dados extras em cada posição
    0                   // deslocamento de cada elemento
  );

  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_mesh_elements);
  int size;  glGetBufferParameteriv(GL_ELEMENT_ARRAY_BUFFER, GL_BUFFER_SIZE, &size);  
  glDrawElements(GL_TRIANGLES, size/sizeof(GLushort), GL_UNSIGNED_SHORT, 0);
 
Suzzane, em nosso aplicativo

E por ultimo, vamos ajustar nossa visualizaçao de acordo com o sistem de coordenada Y-is-top, e a camera voltada para Suzzane:

  glm::mat4 view = glm::lookAt(
    glm::vec3(0.0,  2.0, 4.0),   // olho
    glm::vec3(0.0,  0.0, 0.0),   // direção
    glm::vec3(0.0,  1.0, 0.0));  // up
  glm::mat4 projection = glm::perspective(45.0f, 1.0f*screen_width/screen_height, 0.1f, 100.0f);

Eu enganei um pouco e implementei um modelo de iluminação de Gouraud, vamos melhorar aos poucos.

Flat-shading - duplicando vértices e normals

editar
 
Suzanne com flat-shading

Como discutimos em Tutorial de texturização, as algumas vértices obtém um valor diferente dependendo de qual face é usado.

Isto acontece se nós não compartilharmos as normals (e escolheu um rosto arbitrário quando calcularmos uma vertex' normal, como fizemos acima). Neste caso nós precisamos duplicar um vértice toda vez que for usado com uma normal diferente e depois recriando o array de elementos. Isto fará que demore mais tempo para carregar mas será mais rápido para o OpenGL processo a longo prazo. quanto menos vértices for enviado para OpenGL melhor. ou, como dizemos anteriormente, neste exemplo apenas duplicaremos as vértices uma única vez quando eles aparecem e nós podemos fazer sem a array de elementos.

  for (int i = 0; i < elements.size(); i++) {
    vertices.push_back(shared_vertices[elements[i]]);
    if ((i % 3) == 2) {
      GLushort ia = elements[i-2];
      GLushort ib = elements[i-1];
      GLushort ic = elements[i];
      glm::vec3 normal = glm::normalize(glm::cross(
        shared_vertices[ic] - shared_vertices[ia],
	shared_vertices[ib] - shared_vertices[ia]));
      for (int n = 0; n < 3; n++)
	normals.push_back(normal);
    }
  }
  glDrawArrays(GL_TRIANGLES, 0, suzanne_vertices.size());

Com este ajuste, nós podemos ter uma flat-shading: a variável varying atualmente varia entre os vértices no fragment shader, por que as normal serão os mesmos para as 3 vértices do triângulo.

Calculando as normals

editar

Nosso algoritmos funciona, mas se duas faces forem referenciadas pelo mesmo vetor, em seguida a ultima face sobrescrever a normal para este vértice, isto significa que o objeto pode ficar bem diferente dependo da ordenação das faces.

Para consertar isto, nós calcularemos as normal entre as duas faces. Para calcular dois vetores, você pega metade do primeiro vetor mais metade do segundo vetor. Usaremos o nb_seen para guardar o coeficiente do vetor, assim podemos calcular com novo vetor em qualquer numero de vezes, sem gravar toda lista de vetores:


 
Normals calculadas
  mesh->normals.resize(mesh->vertices.size(), glm::vec3(0.0, 0.0, 0.0));
  nb_seen.resize(mesh->vertices.size(), 0);
  for (int i = 0; i < mesh->elements.size(); i+=3) {
    GLushort ia = mesh->elements[i];
    GLushort ib = mesh->elements[i+1];
    GLushort ic = mesh->elements[i+2];
    glm::vec3 normal = glm::normalize(glm::cross(
      glm::vec3(mesh->vertices[ib]) - glm::vec3(mesh->vertices[ia]),
      glm::vec3(mesh->vertices[ic]) - glm::vec3(mesh->vertices[ia])));

    int v[3];  v[0] = ia;  v[1] = ib;  v[2] = ic;
    for (int j = 0; j < 3; j++) {
      GLushort cur_v = v[j];
      nb_seen[cur_v]++;
      if (nb_seen[cur_v] == 1) {
	mesh->normals[cur_v] = normal;
      } else {
	// average
	mesh->normals[cur_v].x = mesh->normals[cur_v].x * (1.0 - 1.0/nb_seen[cur_v]) + normal.x * 1.0/nb_seen[cur_v];
	mesh->normals[cur_v].y = mesh->normals[cur_v].y * (1.0 - 1.0/nb_seen[cur_v]) + normal.y * 1.0/nb_seen[cur_v];
	mesh->normals[cur_v].z = mesh->normals[cur_v].z * (1.0 - 1.0/nb_seen[cur_v]) + normal.z * 1.0/nb_seen[cur_v];
	mesh->normals[cur_v] = glm::normalize(mesh->normals[cur_v]);
      }
    }
  }

Normals pré calculadas

editar

TODO: melhorar interpretador para suportar .obj com normals.

O formato OBJ suporta normal pré calculadas. Isto é interessante se ele for especificado nas faces, quando estiver presente em váris faces, podemos ter normals diferentes, que significa que temos que usar a duplicação de vertices usadas na técnica especificada acima.

Por exemplo, uma exportação basica da Suzanne referencia as verticés #1 com duas normals diferente #1 e #7:

v 0.437500 0.164063 0.765625
...
vn 0.664993 -0.200752 0.719363
...
f 47//1 1//1 3//1
...
f 1//7 11//7 9//7
f 1//7 9//7 3//7

Para comparar, o formato MD2/MD3 (usado pelo Quake e outros) também inclui normal pré calculadas, mas elas são anexas no vértices e não nas faces.


Navegue e baixe os códigos completos