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 window.cpp trackball.cpp)enable_abcg(${PROJECT_NAME})
Crie os seguintes arquivos vazios:
main.cpp
;model.cpp
emodel.hpp
;window.cpp
ewindow.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
│ 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 booleanostandardize
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â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 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 vetorm_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, efalse
caso contrário.glm::ivec2 m_viewportSize
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 <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.