6.4 Lendo um modelo 3D

Nos capítulos anteriores vimos como usar o pipeline gráfico do OpenGL para renderizar primitivas formadas a partir de arranjos ordenados de vértices. Em particular, conseguimos representar diferentes formas no plano através de malhas de triângulos. Tomemos, como exemplo, o polígono da figura 6.24:

Um polígono de seis lados.

Figura 6.24: Um polígono de seis lados.

O polígono pode ser convertido em uma malha de triângulos através de uma triangulação. Uma possível triangulação é mostrada na figura 6.25:

Triangulação de um polígono de seis lados.

Figura 6.25: Triangulação de um polígono de seis lados.

Os triângulos \(T_1\) a \(T_4\) podem ser renderizados com glDrawArrays(GL_TRIANGLES, ...) usando um VBO formado por um arranjo de posições de vértices:

std::array<glm::ve2, 12> positions{// Triângulo T_1
                                   {-3, 3}, {-4,-2}, { 1,-4},
                                   // Triângulo T_2
                                   {-3, 3}, { 1,-4}, { 4,-1},
                                   // Triângulo T_3
                                   {-3, 3}, { 4,-1}, { 4, 2},
                                   // Triângulo T_4
                                   {-3, 3}, { 4, 2}, { 1, 4}};
...

glDrawArrays(GL_TRIANGLES, 0, 12);

Uma desvantagem dessa representação é que há repetição de coordenadas. Por exemplo, a coordenada \((-3,3)\) é repetida quatro vezes, uma para cada triângulo. Felizmente, como a triangulação forma um leque, podemos usar GL_TRIANGLE_FAN com um arranjo mais compacto:

std::array<glm::ve2, 6> positions{{-3, 3},
                                  {-4,-2},  
                                  { 1,-4},
                                  { 4,-1},
                                  { 4, 2},
                                  { 1, 4}};
...

glDrawArrays(GL_TRIANGLE_FAN, 0, 6);

Outra possibilidade é usar geometria indexada. A figura 6.26 mostra uma possível indexação dos vértices do polígono da figura 6.25. Os índices são numerados de 0 a 5:

Geometria indexada.

Figura 6.26: Geometria indexada.

Na geometria indexada, um arranjo ordenado de posições de vértices é armazenado no VBO, e um arranjo de números inteiros que representam os índices para esses vértices é armazenado no EBO:

std::array<glm::vec2, 6> positions{{-3, 3},  // Vértice 0
                                   {-4,-2},  // Vértice 1
                                   { 1,-4},  // Vértice 2
                                   { 4,-1},  // Vértice 3
                                   { 4, 2},  // Vértice 4
                                   { 1, 4}}; // Vértice 5

std::array indices{0, 1, 2,  // Triângulo T_1
                   0, 2, 3,  // Triângulo T_2
                   0, 3, 4,  // Triângulo T_3
                   0, 4, 5}; // Triângulo T_4
...

glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_INT, nullptr);

A geometria indexada é o formato mais utilizado para descrever malhas de triângulos. É, por exemplo, o formato utilizado nos modelos OBJ que utilizaremos nas atividades da disciplina.

Até agora, só utilizamos formas no plano. Entretanto, não há qualquer limitação no OpenGL que nos impeça de representar geometria no espaço. Basta especificarmos a posição dos vértices como coordenadas \((x,y,z)\) do espaço euclidiano usando tuplas de três elementos com glm::vec3.

Nesta seção descreveremos um passo a passo de construção de uma aplicação de leitura de modelos 3D no formato OBJ. Neste formato, os dados são gravados em formato texto e são fáceis de serem lidos. Veja a seguir o conteúdo de um arquivo OBJ contendo a definição de um cubo unitário centralizado na origem. Observe que há inicialmente a definição dos 8 vértices do cubo, e então a definição dos índices das 12 faces. Neste arquivo, cada face é um triângulo (o cubo tem seis 6 lados e cada lado é formado por 2 faces coplanares).

# object Box

v  -0.5000 -0.5000  0.5000
v  -0.5000 -0.5000 -0.5000
v   0.5000 -0.5000 -0.5000
v   0.5000 -0.5000  0.5000
v  -0.5000  0.5000  0.5000
v   0.5000  0.5000  0.5000
v   0.5000  0.5000 -0.5000
v  -0.5000  0.5000 -0.5000
# 8 vertices

o Box
g Box
f 1 3 2 
f 3 1 4 
f 5 7 6 
f 7 5 8 
f 1 6 4 
f 6 1 5 
f 4 7 3 
f 7 4 6 
f 3 8 2 
f 8 3 7 
f 2 5 1 
f 5 2 8 
# 12 faces

Antes de iniciarmos o passo a passo de leitura do modelo 3D, veremos o conceito de orientação de triângulos e como isso pode afetar a renderização. Se os triângulos de um modelo 3D estiverem com orientação diferente do esperado, o modelo pode ser renderizado de forma incorreta.

Orientação e face culling

A direção do vetor normal pode ser utilizada para definir a orientação de uma superfície, isto é, qual é o “lado da frente” da superfície. Entretanto, reservaremos o uso dos vetores normais para o cálculo da iluminação de superfícies. Há uma forma mais simples de determinar a orientação de uma superfície quando triângulos são utilizados.

A orientação de um triângulo pode ser definida pela ordem em que seus vértices estão ordenados no arranjo de vértices quando o triângulo é visto de frente. Só há duas orientações possíveis (figura 6.27):

  • Sentido horário (clockwise ou CW): os vértices estão orientados no sentido horário quando o triângulo é visto de frente;

  • Sentido anti-horário (counterclockwise ou CCW): os vértices estão orientados no sentido anti-horário quando o triângulo é visto de frente.

Sentidos de orientação de um triângulo.

Figura 6.27: Sentidos de orientação de um triângulo.

Volte no exemplo anterior de geometria indexada (figura 6.26) e observe como os índices de cada triângulo \(T_1\) a \(T_4\) estão ordenados no sentido anti-horário (CCW).

Por padrão, o OpenGL considera que o lado da frente é o lado orientado no sentido anti-horário (CCW). Isso pode ser modificado com a função glFrontFace, usando o argumento GL_CW ou GL_CCW, como a seguir:

  • glFrontFace(GL_CW): para indicar que o lado da frente tem vértices no sentido horário;

  • glFrontFace(GL_CCW): para indicar que o lado da frente tem vértices no sentido anti-horário (padrão).

    CCW é a orientação padrão do OpenGL porque essa é também a orientação padrão utilizada na matemática. Por exemplo, os ângulos medidos no plano cartesiano são medidos no sentido anti-horário e seguem a regra da mão direita (figura 6.28):

    • \(0\) radianos (\(0^{\circ}\)) aponta para o eixo \(x\) positivo (para a direita);
    • \(\frac{\pi}{2}\) radianos (\(90^{\circ}\)) aponta para o eixo \(y\) positivo (para cima);
    • \(\pi\) radianos (\(180^{\circ}\)) aponta para o eixo \(x\) negativo (para a esquerda).
Direção convencional dos ângulos em um eixo de rotação ([fonte](https://en.wikipedia.org/wiki/File:Right-hand_grip_rule.svg)).

Figura 6.28: Direção convencional dos ângulos em um eixo de rotação (fonte).

Há algumas vantagens em saber qual é o lado da frente de um triângulo:

  • Podemos desenhar cada lado com uma cor ou efeito diferente. No fragment shader, a variável embutida gl_FrontFacing é uma variável booleana que é true sempre que o fragmento pertencer a um triângulo visto de frente, e false caso contrário.
  • Podemos fazer uso da técnica de face culling, também chamada de back-face culling.

Face culling

Face culling é uma técnica que consiste em descartar todas as faces (triângulos no OpenGL) que não estão de frente em relação ao plano de projeção. O uso de face culling pode aumentar de forma considerável a eficiência da renderização, pois os triângulos podem ser removidos antes da rasterização, poupando tempo de processamento.

Se a malha de triângulos formar um sólido opaco e fechado, então o face culling pode remover cerca de metade dos triângulos. Esse é o caso da malha que aproxima uma esfera. Na figura 6.29, parte das faces da frente da malha foram deslocadas para revelar as faces voltadas para trás, em vermelho. Essas faces em vermelho podem ser removidas completamente se a esfera estiver fechada, pois elas estarão totalmente encobertas pelas faces da frente.

Faces voltadas para trás (em vermelho).

Figura 6.29: Faces voltadas para trás (em vermelho).

O descarte de primitivas usando face culling pode ser feito automaticamente pelo pipeline gráfico do OpenGL, após a etapa de recorte de primitivas, e imediatamente antes da rasterização. Podemos ativar o face culling através de glEnable(GL_CULL_FACE) e desativá-lo através de glDisable(GL_CULL_FACE).

Por padrão, o face culling está desativado.

A função glCullFace pode ser utilizada para especificar qual lado deve ser descartado quando o face culling estiver habilitado. Por exemplo:

  • glCullFace(GL_FRONT): para descartar os triângulos que estão de frente quando projetados no viewport;
  • glCullFace(GL_BACK): para descartar os triângulos que estão voltados para trás quando projetados no viewport (padrão).
  • glCullFace(GL_FRONT_AND_BACK): para descartar todos os triângulos, mas ainda renderizar pontos e segmentos.

Configuração inicial

A configuração inicial do nosso leitor de arquivos OBJ é semelhante a dos projetos anteriores.

  • No arquivo abcg/examples/CMakeLists.txt, inclua a linha:

    add_subdirectory(loadmodel)
  • Crie o subdiretório abcg/examples/loadmodel e o arquivo abcg/examples/loadmodel/CMakeLists.txt, que ficará assim:

    project(loadmodel)
    add_executable(${PROJECT_NAME} main.cpp window.cpp)
    enable_abcg(${PROJECT_NAME})
  • Crie os arquivos main.cpp, window.cpp e window.hpp.

  • Crie o subdiretório abcg/examples/loadmodel/assets. Dentro dele, crie os arquivos loadmodel.frag e loadmodel.vert.

    Baixe o arquivo bunny.zip e descompacte-o em assets. O conteúdo é o arquivo bunny.obj que contém o modelo 3D de um coelho de cerâmica escaneado: o Stanford Bunny (figura 6.30). O modelo utilizado aqui não é o original, mas uma versão processada e simplificada no MeshLab.

Modelo real do "Stanford Bunny" ([fonte](http://graphics.stanford.edu/data/3Dscanrep/)).

Figura 6.30: Modelo real do “Stanford Bunny” (fonte).

O conteúdo de abcg/examples/loadmodel ficará com a seguinte estrutura:

loadmodel/
│   CMakeLists.txt
│   main.cpp
│   window.hpp
│   window.cpp
│
└───assets/
    │   bunny.obj
    │   loadmodel.frag    
    └   loadmodel.vert

main.cpp

O conteúdo de main.cpp é bem similar ao dos projetos anteriores. Não há nada de realmente novo aqui:

#include "window.hpp"

int main(int argc, char **argv) {
  try {
    abcg::Application app(argc, argv);

    Window window;
    window.setOpenGLSettings({.samples = 4});
    window.setWindowSettings({
        .width = 600,
        .height = 600,
        .title = "Load Model",
    });

    app.run(window);
  } catch (std::exception const &exception) {
    fmt::print(stderr, "{}\n", exception.what());
    return -1;
  }
  return 0;
}

loadmodel.vert

O vertex shader ficará como a seguir:

#version 300 es

layout(location = 0) in vec3 inPosition;

uniform float angle;

void main() {
  float sinAngle = sin(angle);
  float cosAngle = cos(angle);

  gl_Position =
      vec4(inPosition.x * cosAngle + inPosition.z * sinAngle, inPosition.y,
           inPosition.z * cosAngle - inPosition.x * sinAngle, 1.0);
}

Só há um atributo de entrada, inPosition, que é a posição \((x,y,z)\) do vértice.

Observe que não há atributo de saída. A cor dos fragmentos será determinada unicamente no fragment shader.

A variável uniforme angle (linha 5) é um ângulo em radianos que será incrementado continuamente para produzir uma animação de rotação.

No código de main, gl_Position recebe inPosition transformado através de uma operação de rotação em torno do eixo \(y\) pelo ângulo angle. A transformação de inPosition (posição \(x,y,z\)) para gl_Position (posição \(x',y',z',1\)) é como segue:

\[ \begin{align} x' &= x \cos(\theta) + z \sin(\theta),\\ y' &= y,\\ z' &= z \cos(\theta) - x \sin(\theta). \end{align} \]

Os fundamentos de transformação geométrica, incluindo rotação, serão abordados no próximo capítulo. Veremos também que será possível simplificar esse código através do uso de operações matriciais.

loadmodel.frag

O conteúdo do fragment shader ficará assim:

#version 300 es

precision mediump float;

out vec4 outColor;

void main() {
  float i = 1.0 - gl_FragCoord.z;

  if (gl_FrontFacing) {
    outColor = vec4(i, i, i, 1);
  } else {
    outColor = vec4(i, 0, 0, 1);
  }
}

A variável i é um valor de intensidade de cor, calculado a partir da componente z da variável embutida gl_FragCoord.

gl_FragCoord é um vec4 que contém a posição do fragmento no espaço da janela. As componentes \(x\) e \(y\) são a posição do fragmento na janela, em pixels. A componente \(z\) é a “profundidade” do fragmento, que varia de 0 (mais perto) a 1 (mais distante). Lembre-se que esse é o valor \(z\) que estava no intervalo \([-1, 1]\) em coordenadas normalizadas do dispositivo (NDC) e que, após a rasterização, foi mapeado para \([0, 1]\) no espaço da janela (o mapeamento pode ser controlado com glDepthRange).

A cor de saída depende do valor da variável embutida gl_FrontFacing, que indica se o fragmento pertence a uma face de frente ou de trás. Se é true (frente), a cor de saída é um tom de cinza dado pelo valor de i. Caso contrário (trás), a cor de saída é um tom de vermelho.

O resultado será um modelo renderizado em tons de cinza (frente) ou vermelho (trás). Quanto maior for a profundidade do fragmento, menor será sua intensidade. Com isso conseguiremos distinguir melhor a forma e o volume do objeto.

window.hpp

A definição da classe Window ficará assim:

#ifndef WINDOW_HPP_
#define WINDOW_HPP_

#include "abcgOpenGL.hpp"

struct Vertex {
  glm::vec3 position{};

  friend bool operator==(Vertex const &, Vertex const &) = default;
};

class Window : public abcg::OpenGLWindow {
protected:
  void onCreate() override;
  void onPaint() override;
  void onPaintUI() override;
  void onResize(glm::ivec2 const &size) override;
  void onDestroy() override;

private:
  glm::ivec2 m_viewportSize{};

  GLuint m_VAO{};
  GLuint m_VBO{};
  GLuint m_EBO{};
  GLuint m_program{};

  float m_angle{};
  int m_verticesToDraw{};

  std::vector<Vertex> m_vertices;
  std::vector<GLuint> m_indices;

  void loadModelFromFile(std::string_view path);
  void standardize();
};

#endif

Primeiro, observe a estrutura Vertex definida nas linhas 6 a 10:

struct Vertex {
  glm::vec3 position{};

  friend bool operator==(Vertex const &, Vertex const &) = default;
};

Essa estrutura define os atributos que compõem um vértice. Há apenas uma posição \((x,y,z)\) e a definição de um operador de igualdade (==) que verifica se dois vértices são iguais.

Os objetos to tipo Vertex serão os elementos de uma tabela hash implementada com std::unordered_map. Durante a leitura do modelo OBJ, a tabela hash será utilizada para verificar se há algum vértice com posição repetida (por isso o operador de igualdade). Através disso conseguiremos criar uma geometria indexada o mais compacta possível. Veremos mais sobre isso na implementação da função de leitura do modelo OBJ.

A variável m_angle (linha 28) é o ângulo de rotação que será enviado à variável uniforme do vertex shader.

A variável m_verticesToDraw (linha 29) é a quantidade de vértices do VBO que será processada pela função de renderização, glDrawElements. O valor de m_verticesToDraw será controlado por um slider da ImGui. Assim conseguiremos controlar quantos triângulos queremos renderizar.

Nas linhas 31 e 32, m_vertices e m_indices são os arranjos de vértices e índices lidos do arquivo OBJ. Esses são os dados que serão enviados ao VBO (m_VBO) e EBO (m_EBO).

O carregamento do arquivo OBJ será feito pela função Window::loadModelFromFile (linha 34).

A função Window::standardize (linha 35) será chamada após Window::loadModelFromFile e servirá para centralizar o modelo na origem e normalizar as coordenadas de todos os vértices no intervalo \([-1, 1]\).

window.cpp

O início de window.cpp começa como a seguir:

#include "window.hpp"

#include <glm/gtx/fast_trigonometry.hpp>
#include <unordered_map>

// Explicit specialization of std::hash for Vertex
template <> struct std::hash<Vertex> {
  size_t operator()(Vertex const &vertex) const noexcept {
    auto const h1{std::hash<glm::vec3>()(vertex.position)};
    return h1;
  }
};

Nas linhas 7 a 12 há uma especialização explícita de std::hash para a nossa estrutura Vertex. Isso é necessário para que possamos usar Vertex como chave de uma tabela hash (como std::unordered_map).

Por padrão, std::hash gera valores de hashing (do tipo std::size_t) a partir de tipos de dados mais simples, como char, int e float. Como queremos usar um Vertex, precisamos definir como o valor de hashing será gerado. Isso é feito através da sobrecarga do operador de chamada de função () na linha 8. O valor de hashing é o h1 da linha 11, gerado a partir da posição do vértice. A biblioteca GLM implementa sua própria especialização de std::hash para glm::vec3 (definido no cabeçalho glm/gtx/hash.cpp). Na verdade, para este projeto poderíamos ter usado glm::vec3 diretamente no lugar de Vertex. Entretanto, em projetos futuros ampliaremos o número de atributos de Vertex e nossas chaves serão um pouco mais complexas. Esse código será reutilizado e ficará maior nos próximos projetos.

Vamos agora à definição de Window::onCreate:

void Window::onCreate() {
  auto const &assetsPath{abcg::Application::getAssetsPath()};

  abcg::glClearColor(0, 0, 0, 1);

  // Enable depth buffering
  abcg::glEnable(GL_DEPTH_TEST);

  // Create program
  m_program =
      abcg::createOpenGLProgram({{.source = assetsPath + "loadmodel.vert",
                                  .stage = abcg::ShaderStage::Vertex},
                                 {.source = assetsPath + "loadmodel.frag",
                                  .stage = abcg::ShaderStage::Fragment}});

  // Load model
  loadModelFromFile(assetsPath + "bunny.obj");
  standardize();

  m_verticesToDraw = m_indices.size();

  // Generate VBO
  abcg::glGenBuffers(1, &m_VBO);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
  abcg::glBufferData(GL_ARRAY_BUFFER,
                     sizeof(m_vertices.at(0)) * m_vertices.size(),
                     m_vertices.data(), GL_STATIC_DRAW);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);

  // Generate EBO
  abcg::glGenBuffers(1, &m_EBO);
  abcg::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO);
  abcg::glBufferData(GL_ELEMENT_ARRAY_BUFFER,
                     sizeof(m_indices.at(0)) * m_indices.size(),
                     m_indices.data(), GL_STATIC_DRAW);
  abcg::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

  // Create VAO
  abcg::glGenVertexArrays(1, &m_VAO);

  // Bind vertex attributes to current VAO
  abcg::glBindVertexArray(m_VAO);

  abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
  auto const positionAttribute{
      abcg::glGetAttribLocation(m_program, "inPosition")};
  abcg::glEnableVertexAttribArray(positionAttribute);
  abcg::glVertexAttribPointer(positionAttribute, 3, GL_FLOAT, GL_FALSE,
                              sizeof(Vertex), nullptr);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);

  abcg::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO);

  // End of binding to current VAO
  abcg::glBindVertexArray(0);
}

Na linha 20, o teste de profundidade é habilitado. Isso faz com que os fragmentos sejam descartados durante a renderização com base na comparação de sua profundidade com o valor atual do buffer de profundidade. Precisamos usar o teste de profundidade pois, em cenas 3D, geralmente é inviável ordenar e renderizar os triângulos do mais distante para o mais próximo, como fizemos com os objetos do projeto asteroids (seção 5.2) usando o “algoritmo do pintor”.

Na linha 30 carregamos o arquivo bunny.obj. Internamente, Window::loadModelFromFile armazena os vértices e índices em m_vertices e m_indices. Na linha 31, Window::standardize modifica esses dados para fazer com que a geometria caiba no volume de visão do pipeline gráfico, que é o cubo de tamanho \(2 \times 2 \times 2\) centralizado em \((0,0,0)\) no espaço normalizado do dispositivo (figura 6.31).

Volume de visão em coordenadas normalizadas do dispositivo.

Figura 6.31: Volume de visão em coordenadas normalizadas do dispositivo.

O restante do código de Window::onCreate contém a criação do VAO, VBO e EBO usando os dados de m_vertices e m_indices.

A definição de Window::loadModelFromFile será como a seguir:

void Window::loadModelFromFile(std::string_view path) {
  tinyobj::ObjReader reader;

  if (!reader.ParseFromFile(path.data())) {
    if (!reader.Error().empty()) {
      throw abcg::RuntimeError(
          fmt::format("Failed to load model {} ({})", path, reader.Error()));
    }
    throw abcg::RuntimeError(fmt::format("Failed to load model {}", path));
  }

  if (!reader.Warning().empty()) {
    fmt::print("Warning: {}\n", reader.Warning());
  }

  auto const &attributes{reader.GetAttrib()};
  auto const &shapes{reader.GetShapes()};

  m_vertices.clear();
  m_indices.clear();

  // A key:value map with key=Vertex and value=index
  std::unordered_map<Vertex, GLuint> hash{};

  // Loop over shapes
  for (auto const &shape : shapes) {
    // Loop over indices
    for (auto const offset : iter::range(shape.mesh.indices.size())) {
      // Access to vertex
      auto const index{shape.mesh.indices.at(offset)};

      // Vertex position
      auto const startIndex{3 * index.vertex_index};
      auto const vx{attributes.vertices.at(startIndex + 0)};
      auto const vy{attributes.vertices.at(startIndex + 1)};
      auto const vz{attributes.vertices.at(startIndex + 2)};

      Vertex const vertex{.position = {vx, vy, vz}};

      // If map doesn't contain this vertex
      if (!hash.contains(vertex)) {
        // Add this index (size of m_vertices)
        hash[vertex] = m_vertices.size();
        // Add this vertex
        m_vertices.push_back(vertex);
      }

      m_indices.push_back(hash[vertex]);
    }
  }
}

A variável reader (linha 72) é um objeto da classe tinyobj::ObjReader, da biblioteca TinyObjLoader, responsável pela leitura e parsing do arquivo. O resultado é um conjunto de malhas (shapes) e um conjunto de atributos de vértices (attributes).

Cada malha pode ser um objeto, ou apenas parte de um objeto. No nosso caso, trataremos todas as malhas como um único objeto. Para mais detalhes sobre a estrutura utilizada pelo TinyObjLoader, consulte a documentação.

Na linha 92 definimos a tabela hash que será utilizada para fazer a consulta de vértices não repetidos.

Embora o formato OBJ utilize geometria indexada, durante a leitura do modelo é possível que tenhamos vértices em uma mesma posição, embora com índices diferentes. Isso acontece porque os vértices podem diferir em relação a outros atributos além da posição. Por exemplo, dois vértices podem ter a mesma posição no espaço, mas cada um pode ter uma cor diferente. Neste projeto, cada vértice só contém o atributo de posição. Podemos então simplificar o modelo mantendo apenas um índice para cada posição de vértice.

Cada vértice lido do modelo OBJ será inserido na tabela hash usando a posição \((x,y,z)\) como chave, e a ordem de leitura do vértice (isto é, seu índice) como valor associado à chave. Se o vértice está sendo inserido na tabela hash pela primeira vez, ele é inserido também na lista de vértices m_vertices, que contém os dados do VBO. Por sua vez, a lista de índices m_indices (que contém os dados do EBO) será formada pela inserção do valor associado à chave do vértice que está sendo lido. No fim, teremos uma lista de vértices não repetidos (m_vertices), e uma lista de índices (m_indices) a esses vértices.

No laço da linha 96, o conjunto de malhas (shapes) é iterado para ler todos os triângulos e vértices.

A posição de cada vértice é lida nas linhas 104 a 106, nas variáveis vx, vy e vz, e utilizada para criar o vértice vertex na linha 108.

Na linha 111 verificamos se o vértice atual existe na tabela hash. Se não existir, ele é incluído na tabela e em m_vertices.

Na linha 118, o índice do vértice atual é inserido em m_indices. O índice é o valor da tabela hash para a chave de vertex.

A definição da função Window::standardize é dada a seguir:

void Window::standardize() {
  // Center to origin and normalize bounds to [-1, 1]

  // Get bounds
  glm::vec3 max(std::numeric_limits<float>::lowest());
  glm::vec3 min(std::numeric_limits<float>::max());
  for (auto const &vertex : m_vertices) {
    max = glm::max(max, vertex.position);
    min = glm::min(min, vertex.position);
  }

  // Center and scale
  auto const center{(min + max) / 2.0f};
  auto const scaling{2.0f / glm::length(max - min)};
  for (auto &vertex : m_vertices) {
    vertex.position = (vertex.position - center) * scaling;
  }
}

As maiores e menores coordenadas \(x\), \(y\) e \(z\) dos vértices são calculadas no laço da linha 129. Esses valores determinam os limites de uma caixa delimitante do modelo geométrico. O centro dessa caixa e um fator de escala de normalização são calculados nas linhas 135 e 136. No laço da linha 141, essas variáveis são utilizadas para centralizar o modelo na origem e mudar sua escala de modo que o modelo fique contido no volume de visão em NDC, isto é, todos os vértices terão coordenadas no intervalo \([-1, 1]\).

Definiremos Window::onPaint como a seguir:

void Window::onPaint() {
  // Animate angle by 15 degrees per second
  auto const deltaTime{gsl::narrow_cast<float>(getDeltaTime())};
  m_angle = glm::wrapAngle(m_angle + glm::radians(15.0f) * deltaTime);

  // Clear color buffer and depth buffer
  abcg::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  abcg::glViewport(0, 0, m_viewportSize.x, m_viewportSize.y);

  abcg::glUseProgram(m_program);
  abcg::glBindVertexArray(m_VAO);

  // Update uniform variable
  auto const angleLocation{abcg::glGetUniformLocation(m_program, "angle")};
  abcg::glUniform1f(angleLocation, m_angle);

  // Draw triangles
  abcg::glDrawElements(GL_TRIANGLES, m_verticesToDraw, GL_UNSIGNED_INT,
                       nullptr);

  abcg::glBindVertexArray(0);
  abcg::glUseProgram(0);
}

Na linha 145, o valor de m_angle é incrementado a uma taxa de 15 graus por segundo.

Observe, na linha 148, que glClear agora usa GL_DEPTH_BUFFER_BIT além de GL_COLOR_BUFFER_BIT. Isso é necessário para limpar o buffer de profundidade antes de renderizar o quadro atual.

O restante do código é similar ao que já usamos em projetos anteriores.

Vamos agora à definição de Window::onPaintUI:

void Window::onPaintUI() {
  abcg::OpenGLWindow::onPaintUI();

  // Create window for slider
  {
    ImGui::SetNextWindowPos(ImVec2(5, m_viewportSize.y - 94));
    ImGui::SetNextWindowSize(ImVec2(m_viewportSize.x - 10, -1));
    ImGui::Begin("Slider window", nullptr, ImGuiWindowFlags_NoDecoration);

    // Create a slider to control the number of rendered triangles
    {
      // Slider will fill the space of the window
      ImGui::PushItemWidth(m_viewportSize.x - 25);

      static auto n{m_verticesToDraw / 3};
      ImGui::SliderInt(" ", &n, 0, m_indices.size() / 3, "%d triangles");
      m_verticesToDraw = n * 3;

      ImGui::PopItemWidth();
    }

    ImGui::End();
  }

  // Create a window for the other widgets
  {
    auto const widgetSize{ImVec2(172, 62)};
    ImGui::SetNextWindowPos(ImVec2(m_viewportSize.x - widgetSize.x - 5, 5));
    ImGui::SetNextWindowSize(widgetSize);
    ImGui::Begin("Widget window", nullptr, ImGuiWindowFlags_NoDecoration);

    static bool faceCulling{};
    ImGui::Checkbox("Back-face culling", &faceCulling);

    if (faceCulling) {
      abcg::glEnable(GL_CULL_FACE);
    } else {
      abcg::glDisable(GL_CULL_FACE);
    }

    // CW/CCW combo box
    {
      static std::size_t currentIndex{};
      std::vector<std::string> const comboItems{"CW", "CCW"};

      ImGui::PushItemWidth(70);
      if (ImGui::BeginCombo("Front face",
                            comboItems.at(currentIndex).c_str())) {
        for (auto const index : iter::range(comboItems.size())) {
          auto const isSelected{currentIndex == index};
          if (ImGui::Selectable(comboItems.at(index).c_str(), isSelected))
            currentIndex = index;
          if (isSelected)
            ImGui::SetItemDefaultFocus();
        }
        ImGui::EndCombo();
      }
      ImGui::PopItemWidth();

      if (currentIndex == 0) {
        abcg::glFrontFace(GL_CW);
      } else {
        abcg::glFrontFace(GL_CCW);
      }
    }

    ImGui::End();
  }
}

No escopo da linha 177 é definido o slider que controla o número de triângulos que será renderizado.

Na linha 199 é criada uma caixa de seleção (checkbox) de ativação do back-face culling (estamos usando o padrão do glCullFace, que é GL_BACK). O resultado da variável booleana faceCulling é utilizado para ativar ou desativar o face culling nas linhas 202 e 204.

Uma caixa de combinação (combo box) com as opções CW e CCW é definida no escopo a partir da linha 208. Nas linhas 227 e 229, a função glFrontFace é chamada com GL_CW ou GL_CCW de acordo com o que foi selecionado pelo usuário. Como a opção CW é a primeira opção da caixa, a aplicação iniciará com GL_CW.

O conteúdo restante de window.cpp é similar ao utilizado nos projetos anteriores:

void Window::onResize(glm::ivec2 const &size) { m_viewportSize = size; }

void Window::onDestroy() {
  abcg::glDeleteProgram(m_program);
  abcg::glDeleteBuffers(1, &m_EBO);
  abcg::glDeleteBuffers(1, &m_VBO);
  abcg::glDeleteVertexArrays(1, &m_VAO);
}

Baixe o código completo do projeto a partir deste link.

Outros modelos OBJ estão disponíveis neste link.