8.4 Visualizador 3D

Nesta seção, seguiremos o passo a passo de implementação de um visualizador de modelos geométricos 3D que permite a interação através do trackball virtual.

Esta será apenas a primeira de uma série de versões do visualizador 3D. Nos próximos capítulos, faremos aprimoramentos em relação aos shaders e modelos suportados.

Por enquanto, nossa primeira versão do visualizador usa um único objeto pré-carregado que, para variar, é o Stanford Bunny. Além disso, só é utilizado um par de shaders (vertex/fragment shader), que é o mesmo já utilizado no projeto lookat (seção 7.7).

O resultado ficará como a seguir.

Use o mouse para interagir com o objeto. Clique e arraste para rodá-lo. Use o botão de rolagem para aproximar ou distanciar a câmera do objeto. Se a câmera estiver muito próxima, o objeto será recortado pelo plano de recorte próximo (near clipping plane) e será possível ver o interior do objeto.

Configuração inicial

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

    add_subdirectory(viewer1)
  • Crie o subdiretório abcg/examples/viewer1 e o arquivo abcg/examples/viewer1/CMakeLists.txt com o seguinte conteúdo:

    project(viewer1)
    add_executable(${PROJECT_NAME} main.cpp model.cpp openglwindow.cpp 
                                   trackball.cpp)
    enable_abcg(${PROJECT_NAME})
  • Crie os seguintes arquivos vazios:

    • main.cpp;
    • model.cpp e model.hpp;
    • openglwindow.cpp e openglwindow.hpp;
    • trackball.cpp e trackball.hpp.
  • Crie o subdiretório abcg/examples/viewer1/assets. Dentro dele, crie os arquivos vazios depth.frag e depth.vert. Além disso, baixe o arquivo bunny.zip e descompacte-o em assets.

A estrutura de abcg/examples/viewer1 ficará assim:

viewer1/
│   CMakeLists.txt
│   main.cpp
│   model.hpp
│   model.cpp
│   openglwindow.hpp
│   openglwindow.cpp
│   trackball.hpp
│   trackball.cpp
│
└───assets/
    │   bunny.obj
    │   depth.frag    
    └   depth.vert

main.cpp

O conteúdo é o mesmo do projeto anterior. Só vamos mudar o título da janela:

#include <fmt/core.h>

#include "abcg.hpp"
#include "openglwindow.hpp"

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

    auto window{std::make_unique<OpenGLWindow>()};
    window->setOpenGLSettings({.samples = 4});
    window->setWindowSettings(
        {.width = 600, .height = 600, .title = "Model Viewer (version 1)"});

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

depth.vert

Este também é praticamente o mesmo vertex shader utilizado no projeto lookat:

#version 410

layout(location = 0) in vec3 inPosition;

uniform vec4 color;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projMatrix;

out vec4 fragColor;

void main() {
  vec4 posEyeSpace = viewMatrix * modelMatrix * vec4(inPosition, 1);

  float i = 1.0 - (-posEyeSpace.z / 3.0);
  fragColor = vec4(i, i, i, 1) * color;

  gl_Position = projMatrix * posEyeSpace;
}

A única diferença está na linha 15. No cálculo da intensidade i, dividimos posEyeSpace.z por 3.0 e não por 5.0.

Lembre-se que o shader faz com que a cor em cada vértice (fragColor) tenha uma intensidade inversamente proporcional à distância do vértice ao longo de \(z\) negativo no espaço da câmera. Quanto mais longe o vértice, menor será sua intensidade. Neste caso, a intensidade será zero quando \(z\leq-3\).

depth.frag

O conteúdo do fragment shader ficará assim:

#version 410

in vec4 fragColor;
out vec4 outColor;

void main() {
  if (gl_FrontFacing) {
    outColor = fragColor;
  } else {
    outColor = vec4(fragColor.r * 0.5, 0, 0, fragColor.a);
  }
}

Se o triângulo estiver orientado de frente para a câmera, a cor final do fragmento será a cor de entrada (fragColor). Caso contrário, a cor será vermelha.

model.hpp

Neste arquivo definiremos a classe Model, responsável por gerenciar o VBO, EBO e VAO do modelo geométrico lido do arquivo OBJ:

#ifndef MODEL_HPP_
#define MODEL_HPP_

#include <vector>

#include "abcg.hpp"

struct Vertex {
  glm::vec3 position{};

  bool operator==(const Vertex& other) const noexcept {
    return position == other.position;
  }
};

class Model {
 public:
  void loadObj(std::string_view path, bool standardize = true);
  void render(int numTriangles = -1) const;
  void setupVAO(GLuint program);
  void terminateGL();

  [[nodiscard]] int getNumTriangles() const {
    return static_cast<int>(m_indices.size()) / 3;
  }

 private:
  GLuint m_VAO{};
  GLuint m_VBO{};
  GLuint m_EBO{};

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

  void createBuffers();
  void standardize();
};

#endif

Nas linhas 8 a 14 está definida a estrutura Vertex que temos utilizado para descrever os atributos de um vértice. Como nos projetos anteriores, cada vértice só possui um atributo, que é a posição 3D.

A classe contém as seguintes funções membro:

  • void loadObj(std::string_view path, bool standardize = true).

    O conteúdo desta função é o mesmo da função loadModelFromFile utilizada nos projetos anteriores para carregar um arquivo OBJ. Dessa vez, incluímos um parâmetro booleano standardize que indica se o modelo deve ter o tamanho normalizado e centralizado na origem após o carregamento. O comportamento padrão é normalizar o objeto.

  • void render(int numTriangles = -1) const.

    Esta é a função que deve ser chamada em OpenGLWindow::paintGL para renderizar o objeto. A função aceita um parâmetro numTriangles para indicar quantos triângulos devem ser renderizados. O padrão é -1 e significa que todos os triângulos devem ser processados.

  • void setupVAO(GLuint program).

    Esta função deve ser chamada para configurar o VAO do modelo de acordo com o programa de shader atualmente utilizado. O identificador do programa de shader deve ser passado no parâmetro program.

  • void terminateGL().

    Esta função deve ser chamada em OpenGLWindow::terminateGL para liberar os recursos do OpenGL gerenciados pela classe.

  • int getNumTriangles() const.

    Esta função retorna o número de triângulos do modelo. Como usamos GL_TRIANGLES com geometria indexada, esse número é o número de índices dividido por 3.

Observe que a classe não contém a matriz de modelo. A matriz de modelo será mantida em OpenGLWindow. Neste visualizador, a classe Model representa apenas o VBO, EBO e VAO do objeto. Vimos no projeto lookat que uma cena 3D pode ter diferentes instâncias de um mesmo objeto, e que cada instância precisa ter sua própria matriz de modelo. Então, é uma boa decisão de projeto manter os dados geométricos originais em uma classe, e manter os dados da instância (matriz de modelo) em outra classe.

Como nosso visualizador só mostra uma instância do objeto, a escolha de deixar Model sem a matriz de modelo não vai fazer muita diferença. Entretanto, essa classe pode ser reutilizada em outros projetos para compor cenas mais complexas (faremos isso no projeto starfield!). Nesse caso, é recomendável criar uma outra classe ou estrutura só para manter a matriz de modelo e outros dados específicos de cada instância. Se cada instância usar um shader diferente, é recomendável também desacoplar o VAO e deixar apenas o VBO/EBO em Model.

model.cpp

A definição das funções membro de Model ficará como a seguir:

#include "model.hpp"

#include <fmt/core.h>
#include <tiny_obj_loader.h>

#include <cppitertools/itertools.hpp>
#include <glm/gtx/hash.hpp>
#include <unordered_map>

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

void Model::createBuffers() {
  // Delete previous buffers
  abcg::glDeleteBuffers(1, &m_EBO);
  abcg::glDeleteBuffers(1, &m_VBO);

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

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

void Model::loadObj(std::string_view path, bool standardize) {
  tinyobj::ObjReader reader;

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

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

  const auto& attrib{reader.GetAttrib()};
  const auto& 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 (const auto& shape : shapes) {
    // Loop over indices
    for (const auto offset : iter::range(shape.mesh.indices.size())) {
      // Access to vertex
      const tinyobj::index_t index{shape.mesh.indices.at(offset)};

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

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

      // If hash doesn't contain this vertex
      if (hash.count(vertex) == 0) {
        // 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]);
    }
  }

  if (standardize) {
    this->standardize();
  }

  createBuffers();
}

void Model::render(int numTriangles) const {
  abcg::glBindVertexArray(m_VAO);

  const auto numIndices{(numTriangles < 0) ? m_indices.size()
                                           : numTriangles * 3};

  abcg::glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(numIndices),
                       GL_UNSIGNED_INT, nullptr);

  abcg::glBindVertexArray(0);
}

void Model::setupVAO(GLuint program) {
  // Release previous VAO
  abcg::glDeleteVertexArrays(1, &m_VAO);

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

  // Bind EBO and VBO
  abcg::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBO);

  // Bind vertex attributes
  const GLint positionAttribute{
      abcg::glGetAttribLocation(program, "inPosition")};
  if (positionAttribute >= 0) {
    abcg::glEnableVertexAttribArray(positionAttribute);
    abcg::glVertexAttribPointer(positionAttribute, 3, GL_FLOAT, GL_FALSE,
                                sizeof(Vertex), nullptr);
  }

  // End of binding
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
  abcg::glBindVertexArray(0);
}

void Model::standardize() {
  // Center to origin and normalize largest bound to [-1, 1]

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

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

void Model::terminateGL() {
  abcg::glDeleteBuffers(1, &m_EBO);
  abcg::glDeleteBuffers(1, &m_VBO);
  abcg::glDeleteVertexArrays(1, &m_VAO);
}

Não há nada de realmente novo na definição das funções de Model. O código foi quase todo reaproveitado dos projetos loadmodel e lookat.

trackball.hpp

Essa é a classe que implementa o trackball virtual:

#ifndef TRACKBALL_HPP_
#define TRACKBALL_HPP_

#include "abcg.hpp"

class TrackBall {
 public:
  void mouseMove(const glm::ivec2& mousePosition);
  void mousePress(const glm::ivec2& mousePosition);
  void mouseRelease(const glm::ivec2& mousePosition);
  void resizeViewport(int width, int height);

  [[nodiscard]] glm::mat4 getRotation();

 private:
  const float m_maxVelocity{glm::radians(720.0f / 1000.0f)};

  glm::vec3 m_axis{1.0f};
  float m_velocity{};
  glm::mat4 m_rotation{1.0f};

  glm::vec3 m_lastPosition{};
  abcg::ElapsedTimer m_lastTime{};
  bool m_mouseTracking{};

  float m_viewportWidth{};
  float m_viewportHeight{};

  [[nodiscard]] glm::vec3 project(const glm::vec2& mousePosition) const;
};

#endif

A classe contém as seguintes funções membro:

  • void mouseMove(const glm::ivec2& mousePosition).

    void mousePress(const glm::ivec2& mousePosition).

    void mouseRelease(const glm::ivec2& mousePosition).

    Essas são as funções que devem ser chamadas em OpenGLWindow::handleEvent sempre que ocorrer um evento de movimentação do mouse, pressionamento ou liberação do botão (usaremos o botão esquerdo). A posição do mouse em coordenadas do espaço da janela deve ser passada como parâmetro.

  • void resizeViewport(int width, int height).

    Esta função deve ser chamada sempre o tamanho do viewport for modificado. O tamanho do viewport é necessário para que possamos fazer a conversão das coordenadas de um ponto no espaço da janela para coordenadas no intervalo \([-1,1]\) e assim fazer a projeção sobre o trackball virtual.

  • glm::mat4 getRotation().

    Esta é a função que retorna a atual matriz de rotação do trackball. Podemos utilizar a matriz diretamente como matriz de modelo do objeto que está sendo manipulado.

  • glm::vec3 project(const glm::vec2& mousePosition) const.

    Esta função recebe uma posição do mouse no espaço da janela e retorna a posição 3D correspondente sobre o trackball. É utilizada internamente para atualizar a posição do cursor sobre o hemisfério sempre que o mouse se mover (TrackBall::mouseMove) ou quando um botão for pressionado (TrackBall::mousePress).

As variáveis membro da classe são as seguintes:

  • glm::vec3 m_axis: atual eixo de rotação.

  • glm::mat4 m_rotation: atual matriz de rotação.

  • glm::vec3 m_lastPosition: corresponde à posição projetada do ponto \(P_1\) visto na seção 8.3. Essa posição é utilizada com a posição \(P_2\) do evento mais recente do mouse de modo a calcular os dois vetores necessários para gerar o vetor m_axis.

  • abcg::ElapsedTimer m_lastTime: é um temporizador que mede o tempo entre \(P_1\) e \(P_2\), isto é, o tempo entre os últimos dois eventos do mouse.

  • float m_velocity: velocidade de rotação, em radianos por segundo. É o ângulo de rotação, mas multiplicado por m_lastTime.

    Sempre que o usuário soltar o botão do mouse, o objeto continuará sendo rodado por m_velocity, simulando um objeto sem inércia rotacional. A velocidade será zero somente se o usuário soltar o botão com o mouse parado, pois assim \(P_1=P_2\) e o ângulo de rotação será zero. Caso contrário, a velocidade será proporcional à velocidade de arrasto no momento da liberação do botão.

  • bool m_mouseTracking: é true se o usuário está segurando o botão do mouse, e false caso contrário.

  • float m_viewportWidth e float m_viewportHeight são as dimensões do viewport informadas em TrackBall::resizeViewport.

trackball.cpp

A definição das funções membro de TrackBall ficará como a seguir:

#include "trackball.hpp"

#include <glm/gtc/epsilon.hpp>
#include <limits>

const auto epsilon{std::numeric_limits<float>::epsilon()};

void TrackBall::mouseMove(const glm::ivec2 &position) {
  if (!m_mouseTracking) return;

  const auto msecs{static_cast<float>(m_lastTime.restart()) * 1000.0f};

  // Return if mouse cursor hasn't moved wrt last position
  const auto currentPosition{project(position)};
  if (glm::all(glm::epsilonEqual(m_lastPosition, currentPosition, epsilon)))
    return;

  // Rotation axis
  m_axis = glm::cross(m_lastPosition, currentPosition);

  // Rotation angle
  const auto angle{glm::length(m_axis)};

  m_axis = glm::normalize(m_axis);

  // Compute an angle velocity that will be used as a constant rotation angle
  // when the mouse is not being tracked.
  m_velocity = angle / (msecs + epsilon);
  m_velocity = glm::clamp(m_velocity, 0.0f, m_maxVelocity);

  // Concatenate the rotation: R_old = R_new * R_old
  m_rotation = glm::rotate(glm::mat4(1.0f), angle, m_axis) * m_rotation;

  m_lastPosition = currentPosition;
}

void TrackBall::mousePress(const glm::ivec2 &position) {
  m_rotation = getRotation();
  m_mouseTracking = true;

  m_lastTime.restart();

  m_lastPosition = project(position);

  m_velocity = 0.0f;
}

void TrackBall::mouseRelease(const glm::ivec2 &position) {
  mouseMove(position);
  m_mouseTracking = false;
}

void TrackBall::resizeViewport(int width, int height) {
  m_viewportWidth = static_cast<float>(width);
  m_viewportHeight = static_cast<float>(height);
}

glm::mat4 TrackBall::getRotation() {
  if (m_mouseTracking) return m_rotation;

  // If not tracking, rotate by velocity. This will simulate
  // an inertia-free rotation.
  const auto angle{m_velocity * static_cast<float>(m_lastTime.elapsed()) *
                   1000.0f};

  return glm::rotate(glm::mat4(1.0f), angle, m_axis) * m_rotation;
}

glm::vec3 TrackBall::project(const glm::vec2 &position) const {
  // Convert from window coordinates to NDC
  auto v{glm::vec3(2.0f * position.x / m_viewportWidth - 1.0f,
                   1.0f - 2.0f * position.y / m_viewportHeight, 0.0f)};

  // Project to centered unit hemisphere
  const auto squaredLength{glm::length2(v)};
  if (squaredLength >= 1.0f) {
    // Outside sphere
    v = glm::normalize(v);
  } else {
    // Inside sphere
    v.z = std::sqrt(1.0f - squaredLength);
  }

  return v;
}

A implementação segue a abordagem descrita na seção 8.3.

É interessante observar como é atualizada a matriz de rotação durante o arrasto do mouse, neste trecho de TrackBall::mouseMove:

// Concatenate the rotation: R_old = R_new * R_old
m_rotation = glm::rotate(glm::mat4(1.0f), angle, m_axis) * m_rotation;

A cada evento de movimentação do mouse, a matriz de rotação (m_rotation) torna-se uma composição da rotação mais recente (glm::rotate) com as rotações anteriores (m_rotation). Assim, m_rotation é uma concatenação

\[ \mathbf{R}=\mathbf{R}_k\dots\mathbf{R}_3\mathbf{R}_2\mathbf{R}_1, \]

onde \(\mathbf{R}_1\) é a matriz que representa a rotação em torno do eixo gerado a partir do ponto \(P_1\), quando o usuário pressionou o botão do mouse pela primeira vez, e o ponto \(P_2\), do primeiro evento de movimentação do mouse. A matriz \(\mathbf{R}_2\) representa a rotação em torno do eixo gerado a partir do ponto \(P_2\) e o ponto \(P_3\) do segundo evento de movimentação do mouse. Isso é repetido continuamente, até \(\mathbf{R}_k\), que representa a rotação em torno do eixo gerado pelas duas últimas posições do mouse.

Quando o botão do mouse é liberado, m_rotation continua sendo concatenada consigo mesma na forma

\[ \mathbf{R}=\mathbf{R}_n\mathbf{R}, \]

onde \(\mathbf{R}_n\) é a rotação em torno do eixo gerado pelas duas últimas posições do mouse enquanto o botão ainda estava sendo pressionado.

openglwindow.hpp

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

#ifndef OPENGLWINDOW_HPP_
#define OPENGLWINDOW_HPP_

#include "abcg.hpp"
#include "model.hpp"
#include "trackball.hpp"

class OpenGLWindow : public abcg::OpenGLWindow {
 protected:
  void handleEvent(SDL_Event& ev) override;
  void initializeGL() override;
  void paintGL() override;
  void paintUI() override;
  void resizeGL(int width, int height) override;
  void terminateGL() override;

 private:
  GLuint m_program{};

  int m_viewportWidth{};
  int m_viewportHeight{};

  Model m_model;
  int m_trianglesToDraw{};

  TrackBall m_trackBall;
  float m_zoom{};

  glm::mat4 m_modelMatrix{1.0f};
  glm::mat4 m_viewMatrix{1.0f};
  glm::mat4 m_projMatrix{1.0f};

  void update();
};

#endif

Veja que há uma instância da classe Model (linha 23) e TrackBall (linha 26). Também temos uma variável m_zoom para controlar o tamanho do objeto quando o usuário rolar o botão de rolagem do mouse.

Nas linhas 29 a 31 temos as matrizes de modelo (m_modelMatrix), visão (m_viewMatrix) e projeção (m_projMatrix).

openglwindow.cpp

No início de openglwindow.cpp definimos OpenGLWindow::handleEvent:

#include "openglwindow.hpp"

#include <imgui.h>

#include <cppitertools/itertools.hpp>

void OpenGLWindow::handleEvent(SDL_Event& event) {
  glm::ivec2 mousePosition;
  SDL_GetMouseState(&mousePosition.x, &mousePosition.y);

  if (event.type == SDL_MOUSEMOTION) {
    m_trackBall.mouseMove(mousePosition);
  }
  if (event.type == SDL_MOUSEBUTTONDOWN &&
      event.button.button == SDL_BUTTON_LEFT) {
    m_trackBall.mousePress(mousePosition);
  }
  if (event.type == SDL_MOUSEBUTTONUP &&
      event.button.button == SDL_BUTTON_LEFT) {
    m_trackBall.mouseRelease(mousePosition);
  }
  if (event.type == SDL_MOUSEWHEEL) {
    m_zoom += (event.wheel.y > 0 ? 1.0f : -1.0f) / 5.0f;
    m_zoom = glm::clamp(m_zoom, -1.5f, 1.0f);
  }
}

Veja que as funções de TrackBall são chamadas de acordo com os eventos do mouse, e a variável m_zoom é modificada de acordo com o botão de rolagem.

m_zoom é um valor de translação que é utilizado para posicionar a câmera LookAt ao longo do eixo \(z\) do espaço do mundo. Na posição inicial, a câmera está em \(P_{\textrm{eye}}=(0,0,2)\), olhando para \(P_{\textrm{at}}=(0,0,0)\). m_zoom é apenas um valor somado à coordenada \(z\) de \(P_{\textrm{eye}}\), aproximando ou distanciando a câmera da origem.

Vamos agora à definição de OpenGLWindow::initializeOpenGL:

void OpenGLWindow::initializeGL() {
  abcg::glClearColor(0, 0, 0, 1);

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

  // Create program
  m_program = createProgramFromFile(getAssetsPath() + "depth.vert",
                                    getAssetsPath() + "depth.frag");

  // Load model
  m_model.loadObj(getAssetsPath() + "bunny.obj");

  m_model.setupVAO(m_program);

  m_trianglesToDraw = m_model.getNumTriangles();
}

Todo o trabalho de carregamento do modelo foi transferido para a classe Model. Só precisamos chamar Model::loadObj e chamar Model::setupVAO com o identificador do programa de shader.

Vamos à definição de OpenGLWindow::paintGL:

void OpenGLWindow::paintGL() {
  update();

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

  abcg::glViewport(0, 0, m_viewportWidth, m_viewportHeight);

  abcg::glUseProgram(m_program);

  // Get location of uniform variables (could be precomputed)
  const GLint viewMatrixLoc{
      abcg::glGetUniformLocation(m_program, "viewMatrix")};
  const GLint projMatrixLoc{
      abcg::glGetUniformLocation(m_program, "projMatrix")};
  const GLint modelMatrixLoc{
      abcg::glGetUniformLocation(m_program, "modelMatrix")};
  const GLint colorLoc{abcg::glGetUniformLocation(m_program, "color")};

  // Set uniform variables used by every scene object
  abcg::glUniformMatrix4fv(viewMatrixLoc, 1, GL_FALSE, &m_viewMatrix[0][0]);
  abcg::glUniformMatrix4fv(projMatrixLoc, 1, GL_FALSE, &m_projMatrix[0][0]);

  // Set uniform variables of the current object
  abcg::glUniformMatrix4fv(modelMatrixLoc, 1, GL_FALSE, &m_modelMatrix[0][0]);
  abcg::glUniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f);  // White

  m_model.render(m_trianglesToDraw);

  abcg::glUseProgram(0);
}

O código é semelhante ao utilizado no projeto anterior, mas agora está mais simples pois a chamada a glDrawElements é feita em Model::render.

Em OpenGLWindow::paintUI definimos os controles de interface da ImGui:

void OpenGLWindow::paintUI() {
  abcg::OpenGLWindow::paintUI();

  // Create window for slider
  {
    ImGui::SetNextWindowPos(ImVec2(5, m_viewportHeight - 94));
    ImGui::SetNextWindowSize(ImVec2(m_viewportWidth - 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_viewportWidth - 25);

      ImGui::SliderInt("", &m_trianglesToDraw, 0, m_model.getNumTriangles(),
                       "%d triangles");

      ImGui::PopItemWidth();
    }

    ImGui::End();
  }

  // Create a window for the other widgets
  {
    const auto widgetSize{ImVec2(222, 90)};
    ImGui::SetNextWindowPos(ImVec2(m_viewportWidth - 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{};
      const std::vector<std::string> comboItems{"CCW", "CW"};

      ImGui::PushItemWidth(120);
      if (ImGui::BeginCombo("Front face",
                            comboItems.at(currentIndex).c_str())) {
        for (const auto index : iter::range(comboItems.size())) {
          const bool 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_CCW);
      } else {
        abcg::glFrontFace(GL_CW);
      }
    }

    // Projection combo box
    {
      static std::size_t currentIndex{};
      std::vector<std::string> comboItems{"Perspective", "Orthographic"};

      ImGui::PushItemWidth(120);
      if (ImGui::BeginCombo("Projection",
                            comboItems.at(currentIndex).c_str())) {
        for (auto index : iter::range(comboItems.size())) {
          const bool 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) {
        const auto aspect{static_cast<float>(m_viewportWidth) /
                          static_cast<float>(m_viewportHeight)};
        m_projMatrix =
            glm::perspective(glm::radians(45.0f), aspect, 0.1f, 5.0f);
      } else {
        m_projMatrix = glm::ortho(-1.0f, 1.0f, -1.0f, 1.0f, 0.1f, 5.0f);
      }
    }

    ImGui::End();
  }
}

Observe, na estrutura condicional das linhas 160 a 167, como a matriz de projeção é criada com glm::perspective ou glm::ortho, dependendo da escolha do usuário.

O restante de openglwindow.cpp ficará assim:

void OpenGLWindow::resizeGL(int width, int height) {
  m_viewportWidth = width;
  m_viewportHeight = height;

  m_trackBall.resizeViewport(width, height);
}

void OpenGLWindow::terminateGL() {
  m_model.terminateGL();
  abcg::glDeleteProgram(m_program);
}

void OpenGLWindow::update() {
  m_modelMatrix = m_trackBall.getRotation();

  m_viewMatrix =
      glm::lookAt(glm::vec3(0.0f, 0.0f, 2.0f + m_zoom),
                  glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
}

Em OpenGLWindow::resizeGL, chamamos TrackBall::resizeViewport (linha 178) para atualizar as dimensões da janela ao trackball.

Em OpenGLWindow::updateGL, fazemos com que a matriz de modelo seja a própria matriz de rotação do trackball (linha 187). É também nesta função que calculamos a matriz de visão usando a câmera LookAt. Note como m_zoom altera a posição \(z\) da câmera.

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