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 arquivoabcg/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
emodel.hpp
;openglwindow.cpp
eopenglwindow.hpp
;trackball.cpp
etrackball.hpp
.
Crie o subdiretório
abcg/examples/viewer1/assets
. Dentro dele, crie os arquivos vaziosdepth.frag
edepth.vert
. Além disso, baixe o arquivobunny.zip
e descompacte-o emassets
.
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 booleanostandardize
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âmetronumTriangles
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 vetorm_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 porm_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, efalse
caso contrário.float m_viewportWidth
efloat m_viewportHeight
são as dimensões do viewport informadas emTrackBall::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.