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 window.cpp
                                   trackball.cpp)
    enable_abcg(${PROJECT_NAME})
  • Crie os seguintes arquivos vazios:

    • main.cpp;
    • model.cpp e model.hpp;
    • window.cpp e window.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
│   window.hpp
│   window.cpp
│   trackball.hpp
│   trackball.cpp
│
└───assets/
    │   bunny.obj
    │   depth.frag    
    └   depth.vert

main.cpp

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

#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 = "Model Viewer (version 1)",
    });

    app.run(window);
  } catch (std::exception const &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 300 es

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 300 es

precision mediump float;

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 "abcgOpenGL.hpp"

struct Vertex {
  glm::vec3 position{};

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

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

  [[nodiscard]] int getNumTriangles() const {
    return gsl::narrow<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 6 a 10 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:

  • 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 padrão é normalizar o objeto.

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

    Esta é a função que deve ser chamada em Window::onPaint 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 destroy().

    Esta função deve ser chamada em Window::onDestroy 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 Window. 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, faz sentido 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

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

#include "model.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;
  }
};

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.at(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.at(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::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 &attrib{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{attrib.vertices.at(startIndex + 0)};
      auto const vy{attrib.vertices.at(startIndex + 1)};
      auto const vz{attrib.vertices.at(startIndex + 2)};

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

      // If hash 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]);
    }
  }

  if (standardize) {
    Model::standardize();
  }

  createBuffers();
}

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

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

  abcg::glDrawElements(GL_TRIANGLES, 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
  auto const 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 (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;
  }
}

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

trackball.hpp

Essa é a classe que implementa o trackball virtual:

#ifndef TRACKBALL_HPP_
#define TRACKBALL_HPP_

#include "abcg.hpp"

class TrackBall {
public:
  void mouseMove(glm::ivec2 const &position);
  void mousePress(glm::ivec2 const &position);
  void mouseRelease(glm::ivec2 const &position);
  void resizeViewport(glm::ivec2 const &size);

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

  void setAxis(glm::vec3 const axis) noexcept { m_axis = axis; }
  void setVelocity(float const velocity) noexcept { m_velocity = velocity; }

private:
  constexpr static float m_maxVelocity{glm::radians(720.0f)};

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

  glm::vec3 m_lastPosition{};
  abcg::Timer m_lastTime{};

  float m_velocity{};

  bool m_mouseTracking{};

  glm::ivec2 m_viewportSize{};

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

#endif

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

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

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

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

    Essas são as funções que devem ser chamadas em Window::onEvent 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 argumento.

  • void resizeViewport(glm::ivec2 const &size).

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

  • void setAxis(glm::vec3 const axis).

    setVelocity(float const velocity).

    Essas funções podem ser usadas para ajustar manualmente o eixo e a velocidade de rotação.

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

    Essa 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::Timer m_timer: é um temporizador que mede o tempo desde que o trackball foi iniciado, ou desde o último evento do mouse.

  • float m_velocity: velocidade de rotação em radianos por segundo.

    Sempre que o usuário soltar o botão do mouse, o objeto continuará sendo rodado por m_velocity, simulando um movimento com conservação do momento angular. A velocidade será zero se \(P_1=P_2\) e o tempo desde o último evento for maior que 10 ms. Caso contrário, a velocidade será proporcional à última velocidade calculada.

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

  • glm::ivec2 m_viewportSize 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 <algorithm>
#include <limits>

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

  auto const currentPosition{project(position)};

  if (m_lastPosition == currentPosition) {
    // Scale velocity to zero if time since last event > 10ms
    m_velocity *= m_lastTime.elapsed() > 0.01 ? 0.0 : 1.0;
    return;
  }

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

  // Rotation angle
  auto const 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 / (gsl::narrow_cast<float>(m_lastTime.restart()) +
                        std::numeric_limits<float>::epsilon());
  m_velocity = glm::clamp(m_velocity, 0.0f, m_maxVelocity);

  // Concatenate 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(glm::ivec2 const &position) {
  m_rotation = getRotation();
  m_mouseTracking = true;
  m_lastTime.restart();
  m_lastPosition = project(position);
  m_velocity = 0.0f;
}

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

void TrackBall::resizeViewport(glm::ivec2 const &size) {
  m_viewportSize = size;
}

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

  // Rotate by velocity when not tracking to simulate an inertia-free rotation
  auto const angle{m_velocity * gsl::narrow_cast<float>(m_lastTime.elapsed())};

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

glm::vec3 TrackBall::project(glm::vec2 const &position) const {
  // Convert from window coordinates to NDC
  auto projected{glm::vec3(
      2.0f * position.x / gsl::narrow<float>(m_viewportSize.x) - 1.0f,
      1.0f - 2.0f * position.y / gsl::narrow<float>(m_viewportSize.y), 0.0f)};

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

  return projected;
}

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

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

// Concatenate 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.

window.hpp

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

#ifndef WINDOW_HPP_
#define WINDOW_HPP_

#include "abcgOpenGL.hpp"
#include "model.hpp"
#include "trackball.hpp"

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

private:
  glm::ivec2 m_viewportSize{};

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

  GLuint m_program{};
};

#endif

Veja que há uma instância da classe Model (linha 21) e TrackBall (linha 24). 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 27 a 29 temos as matrizes de modelo (m_modelMatrix), visão (m_viewMatrix) e projeção (m_projMatrix).

window.cpp

No início de window.cpp definimos Window::onEvent:

#include "window.hpp"

void Window::onEvent(SDL_Event const &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 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 Window::onCreate:

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

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

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

  m_model.loadObj(assetsPath + "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 Window::onUpdate:

void Window::onUpdate() {
  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));
}

Aqui, fazemos com que a matriz de modelo seja a própria matriz de rotação do trackball (linha 43), e calculamos a matriz de visão usando a câmera LookAt. Note como m_zoom altera a posição \(z\) da câmera.

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

void Window::onPaint() {
  abcg::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

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

  abcg::glUseProgram(m_program);

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

  // Set uniform variables that have the same value for every model
  abcg::glUniformMatrix4fv(viewMatrixLoc, 1, GL_FALSE, &m_viewMatrix[0][0]);
  abcg::glUniformMatrix4fv(projMatrixLoc, 1, GL_FALSE, &m_projMatrix[0][0]);

  // Set uniform variables for the current model
  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 Window::onPaintUI definimos os controles de interface da ImGui:

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);
      ImGui::SliderInt(" ", &m_trianglesToDraw, 0, m_model.getNumTriangles(),
                       "%d triangles");
      ImGui::PopItemWidth();
    }

    ImGui::End();
  }

  // Create a window for the other widgets
  {
    auto const widgetSize{ImVec2(222, 90)};
    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{"CCW", "CW"};

      ImGui::PushItemWidth(120);
      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_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 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) {
        auto const aspect{gsl::narrow<float>(m_viewportSize.x) /
                          gsl::narrow<float>(m_viewportSize.y)};
        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 159 a 166, como a matriz de projeção é criada com glm::perspective ou glm::ortho, dependendo da escolha do usuário.

O restante de window.cpp ficará assim:

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

void Window::onDestroy() {
  m_model.destroy();
  abcg::glDeleteProgram(m_program);
}

A única novidade neste trecho é que, em Window::resizeGL, chamamos TrackBall::resizeViewport (linha 175) para atualizar as dimensões da janela ao trackball.

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