6.4 Lendo um modelo 3D
Nos capítulos anteriores vimos como usar o pipeline gráfico do OpenGL para renderizar primitivas formadas a partir de arranjos ordenados de vértices. Em particular, conseguimos representar diferentes formas no plano através de malhas de triângulos. Tomemos, como exemplo, o polígono da figura 6.24:
O polígono pode ser convertido em uma malha de triângulos através de uma triangulação. Uma possível triangulação é mostrada na figura 6.25:
Os triângulos \(T_1\) a \(T_4\) podem ser renderizados com glDrawArrays(GL_TRIANGLES, ...)
usando um VBO formado por um arranjo de posições de vértices:
std::array<glm::ve2, 12> positions{// Triângulo T_1
{-3, 3}, {-4,-2}, { 1,-4},
// Triângulo T_2
{-3, 3}, { 1,-4}, { 4,-1},
// Triângulo T_3
{-3, 3}, { 4,-1}, { 4, 2},
// Triângulo T_4
{-3, 3}, { 4, 2}, { 1, 4}};
...
(GL_TRIANGLES, 0, 12); glDrawArrays
Uma desvantagem dessa representação é que há repetição de coordenadas. Por exemplo, a coordenada \((-3,3)\) é repetida quatro vezes, uma para cada triângulo. Felizmente, como a triangulação forma um leque, podemos usar GL_TRIANGLE_FAN
com um arranjo mais compacto:
std::array<glm::ve2, 6> positions{{-3, 3},
{-4,-2},
{ 1,-4},
{ 4,-1},
{ 4, 2},
{ 1, 4}};
...
(GL_TRIANGLE_FAN, 0, 6); glDrawArrays
Outra possibilidade é usar geometria indexada. A figura 6.26 mostra uma possível indexação dos vértices do polígono da figura 6.25. Os índices são numerados de 0 a 5:
Na geometria indexada, um arranjo ordenado de posições de vértices é armazenado no VBO, e um arranjo de números inteiros que representam os índices para esses vértices é armazenado no EBO:
std::array<glm::vec2, 6> positions{{-3, 3}, // Vértice 0
{-4,-2}, // Vértice 1
{ 1,-4}, // Vértice 2
{ 4,-1}, // Vértice 3
{ 4, 2}, // Vértice 4
{ 1, 4}}; // Vértice 5
std::array indices{0, 1, 2, // Triângulo T_1
0, 2, 3, // Triângulo T_2
0, 3, 4, // Triângulo T_3
0, 4, 5}; // Triângulo T_4
...
(GL_TRIANGLES, 12, GL_UNSIGNED_INT, nullptr); glDrawElements
A geometria indexada é o formato mais utilizado para descrever malhas de triângulos. É, por exemplo, o formato utilizado nos modelos OBJ que utilizaremos nas atividades da disciplina.
Até agora, só utilizamos formas no plano. Entretanto, não há qualquer limitação no OpenGL que nos impeça de representar geometria no espaço. Basta especificarmos a posição dos vértices como coordenadas \((x,y,z)\) do espaço euclidiano usando tuplas de três elementos com glm::vec3
.
Nesta seção descreveremos um passo a passo de construção de uma aplicação de leitura de modelos 3D no formato OBJ. Neste formato, os dados são gravados em formato texto e são fáceis de serem lidos. Veja a seguir o conteúdo de um arquivo OBJ contendo a definição de um cubo unitário centralizado na origem. Observe que há inicialmente a definição dos 8 vértices do cubo, e então a definição dos índices das 12 faces. Neste arquivo, cada face é um triângulo (o cubo tem seis 6 lados e cada lado é formado por 2 faces coplanares).
# object Box
v -0.5000 -0.5000 0.5000
v -0.5000 -0.5000 -0.5000
v 0.5000 -0.5000 -0.5000
v 0.5000 -0.5000 0.5000
v -0.5000 0.5000 0.5000
v 0.5000 0.5000 0.5000
v 0.5000 0.5000 -0.5000
v -0.5000 0.5000 -0.5000
# 8 vertices
o Box
g Box
f 1 3 2
f 3 1 4
f 5 7 6
f 7 5 8
f 1 6 4
f 6 1 5
f 4 7 3
f 7 4 6
f 3 8 2
f 8 3 7
f 2 5 1
f 5 2 8
# 12 faces
Antes de iniciarmos o passo a passo de leitura do modelo 3D, veremos o conceito de orientação de triângulos e como isso pode afetar a renderização. Se os triângulos de um modelo 3D estiverem com orientação diferente do esperado, o modelo pode ser renderizado de forma incorreta.
Orientação e face culling
A direção do vetor normal pode ser utilizada para definir a orientação de uma superfície, isto é, qual é o “lado da frente” da superfície. Entretanto, reservaremos o uso dos vetores normais para o cálculo da iluminação de superfícies. Há uma forma mais simples de determinar a orientação de uma superfície quando triângulos são utilizados.
A orientação de um triângulo pode ser definida pela ordem em que seus vértices estão ordenados no arranjo de vértices quando o triângulo é visto de frente. Só há duas orientações possíveis (figura 6.27):
Sentido horário (clockwise ou CW): os vértices estão orientados no sentido horário quando o triângulo é visto de frente;
Sentido anti-horário (counterclockwise ou CCW): os vértices estão orientados no sentido anti-horário quando o triângulo é visto de frente.
Volte no exemplo anterior de geometria indexada (figura 6.26) e observe como os índices de cada triângulo \(T_1\) a \(T_4\) estão ordenados no sentido anti-horário (CCW).
Por padrão, o OpenGL considera que o lado da frente é o lado orientado no sentido anti-horário (CCW). Isso pode ser modificado com a função glFrontFace
, usando o argumento GL_CW
ou GL_CCW
, como a seguir:
glFrontFace(GL_CW)
: para indicar que o lado da frente tem vértices no sentido horário;glFrontFace(GL_CCW)
: para indicar que o lado da frente tem vértices no sentido anti-horário (padrão).CCW é a orientação padrão do OpenGL porque essa é também a orientação padrão utilizada na matemática. Por exemplo, os ângulos medidos no plano cartesiano são medidos no sentido anti-horário e seguem a regra da mão direita (figura 6.28):
- \(0\) radianos (\(0^{\circ}\)) aponta para o eixo \(x\) positivo (para a direita);
- \(\frac{\pi}{2}\) radianos (\(90^{\circ}\)) aponta para o eixo \(y\) positivo (para cima);
- \(\pi\) radianos (\(180^{\circ}\)) aponta para o eixo \(x\) negativo (para a esquerda).
Há algumas vantagens em saber qual é o lado da frente de um triângulo:
- Podemos desenhar cada lado com uma cor ou efeito diferente. No fragment shader, a variável embutida
gl_FrontFacing
é uma variável booleana que étrue
sempre que o fragmento pertencer a um triângulo visto de frente, efalse
caso contrário. - Podemos fazer uso da técnica de face culling, também chamada de back-face culling.
Face culling
Face culling é uma técnica que consiste em descartar todas as faces (triângulos no OpenGL) que não estão de frente em relação ao plano de projeção. O uso de face culling pode aumentar de forma considerável a eficiência da renderização, pois os triângulos podem ser removidos antes da rasterização, poupando tempo de processamento.
Se a malha de triângulos formar um sólido opaco e fechado, então o face culling pode remover cerca de metade dos triângulos. Esse é o caso da malha que aproxima uma esfera. Na figura 6.29, parte das faces da frente da malha foram deslocadas para revelar as faces voltadas para trás, em vermelho. Essas faces em vermelho podem ser removidas completamente se a esfera estiver fechada, pois elas estarão totalmente encobertas pelas faces da frente.
O descarte de primitivas usando face culling pode ser feito automaticamente pelo pipeline gráfico do OpenGL, após a etapa de recorte de primitivas, e imediatamente antes da rasterização. Podemos ativar o face culling através de glEnable(GL_CULL_FACE)
e desativá-lo através de glDisable(GL_CULL_FACE)
.
Por padrão, o face culling está desativado.
A função glCullFace
pode ser utilizada para especificar qual lado deve ser descartado quando o face culling estiver habilitado. Por exemplo:
glCullFace(GL_FRONT)
: para descartar os triângulos que estão de frente quando projetados no viewport;glCullFace(GL_BACK)
: para descartar os triângulos que estão voltados para trás quando projetados no viewport (padrão).glCullFace(GL_FRONT_AND_BACK)
: para descartar todos os triângulos, mas ainda renderizar pontos e segmentos.
Configuração inicial
A configuração inicial do nosso leitor de arquivos OBJ é semelhante a dos projetos anteriores.
No arquivo
abcg/examples/CMakeLists.txt
, inclua a linha:add_subdirectory(loadmodel)
Crie o subdiretório
abcg/examples/loadmodel
e o arquivoabcg/examples/loadmodel/CMakeLists.txt
, que ficará assim:project(loadmodel) add_executable(${PROJECT_NAME} main.cpp window.cpp) enable_abcg(${PROJECT_NAME})
Crie os arquivos
main.cpp
,window.cpp
ewindow.hpp
.Crie o subdiretório
abcg/examples/loadmodel/assets
. Dentro dele, crie os arquivosloadmodel.frag
eloadmodel.vert
.Baixe o arquivo
bunny.zip
e descompacte-o emassets
. O conteúdo é o arquivobunny.obj
que contém o modelo 3D de um coelho de cerâmica escaneado: o Stanford Bunny (figura 6.30). O modelo utilizado aqui não é o original, mas uma versão processada e simplificada no MeshLab.
O conteúdo de abcg/examples/loadmodel
ficará com a seguinte estrutura:
loadmodel/
│ CMakeLists.txt
│ main.cpp
│ window.hpp
│ window.cpp
│
└───assets/
│ bunny.obj
│ loadmodel.frag
└ loadmodel.vert
main.cpp
O conteúdo de main.cpp
é bem similar ao dos projetos anteriores. Não há nada de realmente novo aqui:
#include "window.hpp"
int main(int argc, char **argv) {
try {
abcg::Application app(argc, argv);
Window window;
window.setOpenGLSettings({.samples = 4});
window.setWindowSettings({
.width = 600,
.height = 600,
.title = "Load Model",
});
app.run(window);
} catch (std::exception const &exception) {
fmt::print(stderr, "{}\n", exception.what());
return -1;
}
return 0;
}
loadmodel.vert
O vertex shader ficará como a seguir:
#version 300 es
layout(location = 0) in vec3 inPosition;
uniform float angle;
void main() {
float sinAngle = sin(angle);
float cosAngle = cos(angle);
gl_Position =
vec4(inPosition.x * cosAngle + inPosition.z * sinAngle, inPosition.y,
inPosition.z * cosAngle - inPosition.x * sinAngle, 1.0);
}
Só há um atributo de entrada, inPosition
, que é a posição \((x,y,z)\) do vértice.
Observe que não há atributo de saída. A cor dos fragmentos será determinada unicamente no fragment shader.
A variável uniforme angle
(linha 5) é um ângulo em radianos que será incrementado continuamente para produzir uma animação de rotação.
No código de main
, gl_Position
recebe inPosition
transformado através de uma operação de rotação em torno do eixo \(y\) pelo ângulo angle
. A transformação de inPosition
(posição \(x,y,z\)) para gl_Position
(posição \(x',y',z',1\)) é como segue:
\[ \begin{align} x' &= x \cos(\theta) + z \sin(\theta),\\ y' &= y,\\ z' &= z \cos(\theta) - x \sin(\theta). \end{align} \]
Os fundamentos de transformação geométrica, incluindo rotação, serão abordados no próximo capítulo. Veremos também que será possível simplificar esse código através do uso de operações matriciais.
loadmodel.frag
O conteúdo do fragment shader ficará assim:
#version 300 es
precision mediump float;
out vec4 outColor;
void main() {
float i = 1.0 - gl_FragCoord.z;
if (gl_FrontFacing) {
outColor = vec4(i, i, i, 1);
} else {
outColor = vec4(i, 0, 0, 1);
}
}
A variável i
é um valor de intensidade de cor, calculado a partir da componente z
da variável embutida gl_FragCoord
.
gl_FragCoord
é um vec4
que contém a posição do fragmento no espaço da janela. As componentes \(x\) e \(y\) são a posição do fragmento na janela, em pixels. A componente \(z\) é a “profundidade” do fragmento, que varia de 0 (mais perto) a 1 (mais distante). Lembre-se que esse é o valor \(z\) que estava no intervalo \([-1, 1]\) em coordenadas normalizadas do dispositivo (NDC) e que, após a rasterização, foi mapeado para \([0, 1]\) no espaço da janela (o mapeamento pode ser controlado com glDepthRange
).
A cor de saída depende do valor da variável embutida gl_FrontFacing
, que indica se o fragmento pertence a uma face de frente ou de trás. Se é true
(frente), a cor de saída é um tom de cinza dado pelo valor de i
. Caso contrário (trás), a cor de saída é um tom de vermelho.
O resultado será um modelo renderizado em tons de cinza (frente) ou vermelho (trás). Quanto maior for a profundidade do fragmento, menor será sua intensidade. Com isso conseguiremos distinguir melhor a forma e o volume do objeto.
window.hpp
A definição da classe Window
ficará assim:
#ifndef WINDOW_HPP_
#define WINDOW_HPP_
#include "abcgOpenGL.hpp"
struct Vertex {
glm::vec3 position{};
friend bool operator==(Vertex const &, Vertex const &) = default;
};
class Window : public abcg::OpenGLWindow {
protected:
void onCreate() override;
void onPaint() override;
void onPaintUI() override;
void onResize(glm::ivec2 const &size) override;
void onDestroy() override;
private:
glm::ivec2 m_viewportSize{};
GLuint m_VAO{};
GLuint m_VBO{};
GLuint m_EBO{};
GLuint m_program{};
float m_angle{};
int m_verticesToDraw{};
std::vector<Vertex> m_vertices;
std::vector<GLuint> m_indices;
void loadModelFromFile(std::string_view path);
void standardize();
};
#endif
Primeiro, observe a estrutura Vertex
definida nas linhas 6 a 10:
struct Vertex {
glm::vec3 position{};
friend bool operator==(Vertex const &, Vertex const &) = default;
};
Essa estrutura define os atributos que compõem um vértice. Há apenas uma posição \((x,y,z)\) e a definição de um operador de igualdade (==
) que verifica se dois vértices são iguais.
Os objetos to tipo Vertex
serão os elementos de uma tabela hash implementada com std::unordered_map
. Durante a leitura do modelo OBJ, a tabela hash será utilizada para verificar se há algum vértice com posição repetida (por isso o operador de igualdade). Através disso conseguiremos criar uma geometria indexada o mais compacta possível. Veremos mais sobre isso na implementação da função de leitura do modelo OBJ.
A variável m_angle
(linha 28) é o ângulo de rotação que será enviado à variável uniforme do vertex shader.
A variável m_verticesToDraw
(linha 29) é a quantidade de vértices do VBO que será processada pela função de renderização, glDrawElements
. O valor de m_verticesToDraw
será controlado por um slider da ImGui. Assim conseguiremos controlar quantos triângulos queremos renderizar.
Nas linhas 31 e 32, m_vertices
e m_indices
são os arranjos de vértices e índices lidos do arquivo OBJ. Esses são os dados que serão enviados ao VBO (m_VBO
) e EBO (m_EBO
).
O carregamento do arquivo OBJ será feito pela função Window::loadModelFromFile
(linha 34).
A função Window::standardize
(linha 35) será chamada após Window::loadModelFromFile
e servirá para centralizar o modelo na origem e normalizar as coordenadas de todos os vértices no intervalo \([-1, 1]\).
window.cpp
O início de window.cpp
começa como a seguir:
#include "window.hpp"
#include <glm/gtx/fast_trigonometry.hpp>
#include <unordered_map>
// Explicit specialization of std::hash for Vertex
template <> struct std::hash<Vertex> {
size_t operator()(Vertex const &vertex) const noexcept {
auto const h1{std::hash<glm::vec3>()(vertex.position)};
return h1;
}
};
Nas linhas 7 a 12 há uma especialização explícita de std::hash
para a nossa estrutura Vertex
. Isso é necessário para que possamos usar Vertex
como chave de uma tabela hash (como std::unordered_map
).
Por padrão, std::hash
gera valores de hashing (do tipo std::size_t
) a partir de tipos de dados mais simples, como char
, int
e float
. Como queremos usar um Vertex
, precisamos definir como o valor de hashing será gerado. Isso é feito através da sobrecarga do operador de chamada de função ()
na linha 8. O valor de hashing é o h1
da linha 11, gerado a partir da posição do vértice. A biblioteca GLM implementa sua própria especialização de std::hash
para glm::vec3
(definido no cabeçalho glm/gtx/hash.cpp
). Na verdade, para este projeto poderíamos ter usado glm::vec3
diretamente no lugar de Vertex
. Entretanto, em projetos futuros ampliaremos o número de atributos de Vertex
e nossas chaves serão um pouco mais complexas. Esse código será reutilizado e ficará maior nos próximos projetos.
Vamos agora à definição de Window::onCreate
:
void Window::onCreate() {
auto const &assetsPath{abcg::Application::getAssetsPath()};
abcg::glClearColor(0, 0, 0, 1);
// Enable depth buffering
abcg::glEnable(GL_DEPTH_TEST);
// Create program
m_program =
abcg::createOpenGLProgram({{.source = assetsPath + "loadmodel.vert",
.stage = abcg::ShaderStage::Vertex},
{.source = assetsPath + "loadmodel.frag",
.stage = abcg::ShaderStage::Fragment}});
// Load model
loadModelFromFile(assetsPath + "bunny.obj");
standardize();
m_verticesToDraw = m_indices.size();
// Generate VBO
abcg::glGenBuffers(1, &m_VBO);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
abcg::glBufferData(GL_ARRAY_BUFFER,
sizeof(m_vertices.at(0)) * m_vertices.size(),
m_vertices.data(), GL_STATIC_DRAW);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// Generate EBO
abcg::glGenBuffers(1, &m_EBO);
abcg::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO);
abcg::glBufferData(GL_ELEMENT_ARRAY_BUFFER,
sizeof(m_indices.at(0)) * m_indices.size(),
m_indices.data(), GL_STATIC_DRAW);
abcg::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
// Create VAO
abcg::glGenVertexArrays(1, &m_VAO);
// Bind vertex attributes to current VAO
abcg::glBindVertexArray(m_VAO);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
auto const positionAttribute{
abcg::glGetAttribLocation(m_program, "inPosition")};
abcg::glEnableVertexAttribArray(positionAttribute);
abcg::glVertexAttribPointer(positionAttribute, 3, GL_FLOAT, GL_FALSE,
sizeof(Vertex), nullptr);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
abcg::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO);
// End of binding to current VAO
abcg::glBindVertexArray(0);
}
Na linha 20, o teste de profundidade é habilitado. Isso faz com que os fragmentos sejam descartados durante a renderização com base na comparação de sua profundidade com o valor atual do buffer de profundidade. Precisamos usar o teste de profundidade pois, em cenas 3D, geralmente é inviável ordenar e renderizar os triângulos do mais distante para o mais próximo, como fizemos com os objetos do projeto asteroids
(seção 5.2) usando o “algoritmo do pintor”.
Na linha 30 carregamos o arquivo bunny.obj
. Internamente, Window::loadModelFromFile
armazena os vértices e índices em m_vertices
e m_indices
. Na linha 31, Window::standardize
modifica esses dados para fazer com que a geometria caiba no volume de visão do pipeline gráfico, que é o cubo de tamanho \(2 \times 2 \times 2\) centralizado em \((0,0,0)\) no espaço normalizado do dispositivo (figura 6.31).
O restante do código de Window::onCreate
contém a criação do VAO, VBO e EBO usando os dados de m_vertices
e m_indices
.
A definição de Window::loadModelFromFile
será como a seguir:
void Window::loadModelFromFile(std::string_view path) {
tinyobj::ObjReader reader;
if (!reader.ParseFromFile(path.data())) {
if (!reader.Error().empty()) {
throw abcg::RuntimeError(
fmt::format("Failed to load model {} ({})", path, reader.Error()));
}
throw abcg::RuntimeError(fmt::format("Failed to load model {}", path));
}
if (!reader.Warning().empty()) {
fmt::print("Warning: {}\n", reader.Warning());
}
auto const &attributes{reader.GetAttrib()};
auto const &shapes{reader.GetShapes()};
m_vertices.clear();
m_indices.clear();
// A key:value map with key=Vertex and value=index
std::unordered_map<Vertex, GLuint> hash{};
// Loop over shapes
for (auto const &shape : shapes) {
// Loop over indices
for (auto const offset : iter::range(shape.mesh.indices.size())) {
// Access to vertex
auto const index{shape.mesh.indices.at(offset)};
// Vertex position
auto const startIndex{3 * index.vertex_index};
auto const vx{attributes.vertices.at(startIndex + 0)};
auto const vy{attributes.vertices.at(startIndex + 1)};
auto const vz{attributes.vertices.at(startIndex + 2)};
Vertex const vertex{.position = {vx, vy, vz}};
// If map doesn't contain this vertex
if (!hash.contains(vertex)) {
// Add this index (size of m_vertices)
hash[vertex] = m_vertices.size();
// Add this vertex
m_vertices.push_back(vertex);
}
m_indices.push_back(hash[vertex]);
}
}
}
A variável reader
(linha 72) é um objeto da classe tinyobj::ObjReader
, da biblioteca TinyObjLoader, responsável pela leitura e parsing do arquivo. O resultado é um conjunto de malhas (shapes
) e um conjunto de atributos de vértices (attributes
).
Cada malha pode ser um objeto, ou apenas parte de um objeto. No nosso caso, trataremos todas as malhas como um único objeto. Para mais detalhes sobre a estrutura utilizada pelo TinyObjLoader, consulte a documentação.
Na linha 92 definimos a tabela hash que será utilizada para fazer a consulta de vértices não repetidos.
Embora o formato OBJ utilize geometria indexada, durante a leitura do modelo é possível que tenhamos vértices em uma mesma posição, embora com índices diferentes. Isso acontece porque os vértices podem diferir em relação a outros atributos além da posição. Por exemplo, dois vértices podem ter a mesma posição no espaço, mas cada um pode ter uma cor diferente. Neste projeto, cada vértice só contém o atributo de posição. Podemos então simplificar o modelo mantendo apenas um índice para cada posição de vértice.
Cada vértice lido do modelo OBJ será inserido na tabela hash usando a posição \((x,y,z)\) como chave, e a ordem de leitura do vértice (isto é, seu índice) como valor associado à chave. Se o vértice está sendo inserido na tabela hash pela primeira vez, ele é inserido também na lista de vértices m_vertices
, que contém os dados do VBO. Por sua vez, a lista de índices m_indices
(que contém os dados do EBO) será formada pela inserção do valor associado à chave do vértice que está sendo lido. No fim, teremos uma lista de vértices não repetidos (m_vertices
), e uma lista de índices (m_indices
) a esses vértices.
No laço da linha 96, o conjunto de malhas (shapes
) é iterado para ler todos os triângulos e vértices.
A posição de cada vértice é lida nas linhas 104 a 106, nas variáveis vx
, vy
e vz
, e utilizada para criar o vértice vertex
na linha 108.
Na linha 111 verificamos se o vértice atual existe na tabela hash. Se não existir, ele é incluído na tabela e em m_vertices
.
Na linha 118, o índice do vértice atual é inserido em m_indices
. O índice é o valor da tabela hash para a chave de vertex
.
A definição da função Window::standardize
é dada a seguir:
void Window::standardize() {
// Center to origin and normalize bounds to [-1, 1]
// Get bounds
glm::vec3 max(std::numeric_limits<float>::lowest());
glm::vec3 min(std::numeric_limits<float>::max());
for (auto const &vertex : m_vertices) {
max = glm::max(max, vertex.position);
min = glm::min(min, vertex.position);
}
// Center and scale
auto const center{(min + max) / 2.0f};
auto const scaling{2.0f / glm::length(max - min)};
for (auto &vertex : m_vertices) {
vertex.position = (vertex.position - center) * scaling;
}
}
As maiores e menores coordenadas \(x\), \(y\) e \(z\) dos vértices são calculadas no laço da linha 129. Esses valores determinam os limites de uma caixa delimitante do modelo geométrico. O centro dessa caixa e um fator de escala de normalização são calculados nas linhas 135 e 136. No laço da linha 141, essas variáveis são utilizadas para centralizar o modelo na origem e mudar sua escala de modo que o modelo fique contido no volume de visão em NDC, isto é, todos os vértices terão coordenadas no intervalo \([-1, 1]\).
Definiremos Window::onPaint
como a seguir:
void Window::onPaint() {
// Animate angle by 15 degrees per second
auto const deltaTime{gsl::narrow_cast<float>(getDeltaTime())};
m_angle = glm::wrapAngle(m_angle + glm::radians(15.0f) * deltaTime);
// Clear color buffer and depth buffer
abcg::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
abcg::glViewport(0, 0, m_viewportSize.x, m_viewportSize.y);
abcg::glUseProgram(m_program);
abcg::glBindVertexArray(m_VAO);
// Update uniform variable
auto const angleLocation{abcg::glGetUniformLocation(m_program, "angle")};
abcg::glUniform1f(angleLocation, m_angle);
// Draw triangles
abcg::glDrawElements(GL_TRIANGLES, m_verticesToDraw, GL_UNSIGNED_INT,
nullptr);
abcg::glBindVertexArray(0);
abcg::glUseProgram(0);
}
Na linha 145, o valor de m_angle
é incrementado a uma taxa de 15 graus por segundo.
Observe, na linha 148, que glClear
agora usa GL_DEPTH_BUFFER_BIT
além de GL_COLOR_BUFFER_BIT
. Isso é necessário para limpar o buffer de profundidade antes de renderizar o quadro atual.
O restante do código é similar ao que já usamos em projetos anteriores.
Vamos agora à definição de Window::onPaintUI
:
void Window::onPaintUI() {
abcg::OpenGLWindow::onPaintUI();
// Create window for slider
{
ImGui::SetNextWindowPos(ImVec2(5, m_viewportSize.y - 94));
ImGui::SetNextWindowSize(ImVec2(m_viewportSize.x - 10, -1));
ImGui::Begin("Slider window", nullptr, ImGuiWindowFlags_NoDecoration);
// Create a slider to control the number of rendered triangles
{
// Slider will fill the space of the window
ImGui::PushItemWidth(m_viewportSize.x - 25);
static auto n{m_verticesToDraw / 3};
ImGui::SliderInt(" ", &n, 0, m_indices.size() / 3, "%d triangles");
m_verticesToDraw = n * 3;
ImGui::PopItemWidth();
}
ImGui::End();
}
// Create a window for the other widgets
{
auto const widgetSize{ImVec2(172, 62)};
ImGui::SetNextWindowPos(ImVec2(m_viewportSize.x - widgetSize.x - 5, 5));
ImGui::SetNextWindowSize(widgetSize);
ImGui::Begin("Widget window", nullptr, ImGuiWindowFlags_NoDecoration);
static bool faceCulling{};
ImGui::Checkbox("Back-face culling", &faceCulling);
if (faceCulling) {
abcg::glEnable(GL_CULL_FACE);
} else {
abcg::glDisable(GL_CULL_FACE);
}
// CW/CCW combo box
{
static std::size_t currentIndex{};
std::vector<std::string> const comboItems{"CW", "CCW"};
ImGui::PushItemWidth(70);
if (ImGui::BeginCombo("Front face",
comboItems.at(currentIndex).c_str())) {
for (auto const index : iter::range(comboItems.size())) {
auto const isSelected{currentIndex == index};
if (ImGui::Selectable(comboItems.at(index).c_str(), isSelected))
currentIndex = index;
if (isSelected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::PopItemWidth();
if (currentIndex == 0) {
abcg::glFrontFace(GL_CW);
} else {
abcg::glFrontFace(GL_CCW);
}
}
ImGui::End();
}
}
No escopo da linha 177 é definido o slider que controla o número de triângulos que será renderizado.
Na linha 199 é criada uma caixa de seleção (checkbox) de ativação do back-face culling (estamos usando o padrão do glCullFace
, que é GL_BACK
). O resultado da variável booleana faceCulling
é utilizado para ativar ou desativar o face culling nas linhas 202 e 204.
Uma caixa de combinação (combo box) com as opções CW
e CCW
é definida no escopo a partir da linha 208. Nas linhas 227 e 229, a função glFrontFace
é chamada com GL_CW
ou GL_CCW
de acordo com o que foi selecionado pelo usuário. Como a opção CW
é a primeira opção da caixa, a aplicação iniciará com GL_CW
.
O conteúdo restante de window.cpp
é similar ao utilizado nos projetos anteriores:
void Window::onResize(glm::ivec2 const &size) { m_viewportSize = size; }
void Window::onDestroy() {
abcg::glDeleteProgram(m_program);
abcg::glDeleteBuffers(1, &m_EBO);
abcg::glDeleteBuffers(1, &m_VBO);
abcg::glDeleteVertexArrays(1, &m_VAO);
}
Baixe o código completo do projeto a partir deste link.
Outros modelos OBJ estão disponíveis neste link.