7.7 LookAt na prática
Nesta seção, seguiremos o passo a passo de desenvolvimento de uma aplicação que renderiza uma cena 3D do ponto de vista de uma câmera LookAt.
A cena 3D será composta por quatro instâncias do modelo “Stanford Bunny” dispostos sobre o plano \(xz\) do espaço do mundo. A câmera LookAt simulará um observador em primeira pessoa. A figura 7.32 mostra o posicionamento dos objetos e a localização inicial da câmera.
Na figura acima, os vetores \(\hat{\mathbf{i}}\),\(\hat{\mathbf{j}}\),\(\hat{\mathbf{k}}\) correspondem às direções dos eixos \(x\),\(y\),\(z\) do espaço euclidiano, e \(\hat{\mathbf{u}}\),\(\hat{\mathbf{v}}\),\(\hat{\mathbf{n}}\) são os vetores do frame da câmera. A câmera está localizada em \((0, 0.5, 2.5)\) e está olhando na direção do eixo \(z\) negativo.
- O coelho vermelho está na posição \((0,0,0)\) e tem escala de \(10\%\) do tamanho original.
- O coelho cinza está na posição \((-1,0,0)\) e está rodado em \(90^{\circ}\) em torno de seu eixo \(y\) local.
- O coelho azul está na posição \((1,0,0)\) e está rodado em \(-90^{\circ}\) em torno de seu eixo \(y\) local.
- O coelho amarelo está na posição \((0,0,-1)\) e está com sua orientação original.
A posição e orientação da câmera pode ser modificada através do teclado:
- As setas para cima/baixo (ou
W
/S
) fazem a câmera ir para a frente e para trás ao longo da direção de visão (direção de \(\pm\hat{\mathbf{n}}\)). Esse movimento de câmera é conhecido como dolly no jargão da cinematografia. - As setas para os lados (ou
A
/D
) fazem a câmera girar em torno de seu eixo \(y\) (vetor \(\hat{\mathbf{v}}\)). Esse movimento é chamado de pan. - As teclas
Q
/E
fazem a câmera deslizar para os lados (direção de \(\pm\hat{\mathbf{u}}\)). Esse movimento é chamado de truck ou dolly lateral.
Neste exemplo, a altura da câmera permanecerá sempre em \(y=0.5\).
O resultado ficará como a seguir:
Para controlar a câmera usando o teclado é necessário abrir o link original e clicar na área de desenho. Desse modo a aplicação terá o foco do teclado.
Configuração inicial
No arquivo
abcg/examples/CMakeLists.txt
, inclua a linha:add_subdirectory(lookat)
Crie o subdiretório
abcg/examples/lookat
e o arquivoabcg/examples/lookat/CMakeLists.txt
com o seguinte conteúdo:project(lookat) add_executable(${PROJECT_NAME} camera.cpp ground.cpp main.cpp openglwindow.cpp) enable_abcg(${PROJECT_NAME})
Crie os arquivos
camera.cpp
,camera.hpp
,ground.cpp
,ground.hpp
,main.cpp
,openglwindow.cpp
eopenglwindow.hpp
.Crie o subdiretório
abcg/examples/lookat/assets
. Dentro dele, crie os arquivoslookat.frag
elookat.vert
. Além disso, baixe o arquivobunny.zip
e descompacte-o emassets
.
A estrutura de abcg/examples/lookat
ficará assim:
lookat/
│ CMakeLists.txt
│ camera.hpp
│ camera.cpp
│ ground.hpp
│ ground.cpp
│ main.cpp
│ openglwindow.hpp
│ openglwindow.cpp
│
└───assets/
│ bunny.obj
│ lookat.frag
└ lookat.vert
main.cpp
Exceto pelo título da janela, o conteúdo de main.cpp
é o mesmo do projeto anterior:
#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 = "LookAt Camera"});
app.run(std::move(window));
} catch (const abcg::Exception &exception) {
fmt::print(stderr, "{}\n", exception.what());
return -1;
}
return 0;
}
lookat.vert
O vertex shader ficará como a seguir:
#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 / 5.0);
fragColor = vec4(i, i, i, 1) * color;
gl_Position = projMatrix * posEyeSpace;
}
O atributo de entrada, inPosition
, é a posição \((x,y,z)\) do vértice. Vamos considerar que estas coordenadas estão no espaço do objeto.
O atributo de saída, fragColor
, é uma cor RGBA.
As variáveis uniformes são utilizadas para determinar a cor do objeto (color
, na linha 5) e as matrizes \(4 \times 4\) de transformação geométrica (linhas 6 a 8):
- Matriz de modelo:
modelMatrix
; - Matriz de visão:
viewMatrix
; - Matriz de projeção:
projMatrix
.
Embora ainda não tenhamos visto o conteúdo teórico sobre a construção de uma matriz de projeção, vamos utilizar essa matriz desde já. Ela será necessária para obter o efeito de perspectiva e assim manter a ilusão de que a câmera LookAt é um observador dentro de um cenário 3D.
No código de main
, a linha 13 transforma a posição de entrada usando as matrizes de modelo e visão. Para entendermos a ordem das transformações, temos de ler os operandos da linha 13 da direita para a esquerda:
Primeiro, vec4(inPosition, 1)
produz a posição \((x,y,z,1)\), isto é, o ponto/vértice em coordenadas homogêneas que corresponde à posição \((x,y,z)\) no espaço do objeto. Esse vértice é transformado, através do produto matricial, pela matriz de modelo modelMatrix
. A transformação pela matriz de modelo converte coordenadas do espaço do objeto para o espaço do mundo. Em seguida há uma transformação pela matriz de visão viewMatrix
. A matriz de visão converte coordenadas do espaço do mundo para coordenadas do espaço da câmera. Assim, o resultado armazenado em posEyeSpace
é a posição do vértice no espaço da câmera.
Na linha 15, calculamos um valor i
de intensidade de cor a partir da coordenada \(z\) do vértice no espaço da câmera:
Lembre-se que, no espaço da câmera, a câmera está olhando na direção de seu eixo \(z\) negativo. Logo, do ponto de vista da câmera, todos os objetos à sua frente têm valor \(z\) negativo. Ao fazermos -PosEyeSpace.z
, tornamos esse valor positivo, correspondendo à distância entre o vértice e a câmera ao longo do eixo \(z\). A ideia aqui é transformar essa distância em um valor de intensidade de cor. A intensidade será máxima (1) se o objeto estiver o mais próximo possível da câmera (isto é, se estiver na mesma posição da câmera), e mínima (0) se estiver a 5 ou mais unidades de distância na direção de visão. Na linha 16, esse valor de intensidade é utilizado para multiplicar as componentes RGB da cor color
.
Assim, quanto mais longe o objeto estiver da câmera, mais escuro ele ficará. A partir da distância 5, a intensidade fica negativa, mas nesse caso o OpenGL fixa automaticamente o valor de cor para zero (não existe intensidade negativa de cor).
Na linha 18, projMatrix * posEyeSpace
faz com que as coordenadas no espaço da câmera sejam convertidas para o espaço de recorte. É esse o resultado final em gl_Position
:
lookat.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 = fragColor * 0.5;
}
}
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 terá metade da intensidade original (a cor RGB é multiplicada por 0.5). Assim, se a câmera estiver dentro de um objeto, os triângulos serão desenhados com uma cor mais escura, pois estaremos vendo o lado de trás da malha triangular.
camera.hpp
Neste arquivo definiremos a classe Camera
que gerenciará a câmera LookAt. O conteúdo ficará como a seguir:
#ifndef CAMERA_HPP_
#define CAMERA_HPP_
#include <glm/mat4x4.hpp>
#include <glm/vec3.hpp>
class OpenGLWindow;
class Camera {
public:
void computeViewMatrix();
void computeProjectionMatrix(int width, int height);
void dolly(float speed);
void truck(float speed);
void pan(float speed);
private:
friend OpenGLWindow;
glm::vec3 m_eye{glm::vec3(0.0f, 0.5f, 2.5f)}; // Camera position
glm::vec3 m_at{glm::vec3(0.0f, 0.5f, 0.0f)}; // Look-at point
glm::vec3 m_up{glm::vec3(0.0f, 1.0f, 0.0f)}; // "up" direction
// Matrix to change from world space to camera soace
glm::mat4 m_viewMatrix;
// Matrix to change from camera space to clip space
glm::mat4 m_projMatrix;
};
#endif
Observe, nas linhas 21 a 23, que a classe tem todos os atributos necessários para criar o frame de uma câmera LookAt:
m_eye
: posição da câmera \((0, 0.5, 2.5)\).m_at
: posição para onde a câmera está olhando \((0, 0.5, 0)\).m_up
: vetor de direção para cima \((0, 1, 0)\).
Na linha 26 temos a matriz de visão (m_viewMatrix
) que será calculada pela função Camera::computeViewMatrix
declarada na linha 11.
Na linha 29 temos a matriz de projeção (m_projMatrix
) que será calculada pela função Camera::computeProjectionMatrix
declarada na linha 12.
As funções Camera::dolly
, Camera::truck
e Camera::pan
serão chamadas a partir de OpenGLWindow
em resposta à entrada do teclado. Internamente, essas funções modificarão as variáveis m_eye
e m_at
, fazendo a câmera mudar de posição e orientação.
camera.cpp
A definição das funções membro de Camera
ficará como a seguir:
#include "camera.hpp"
#include <glm/gtc/matrix_transform.hpp>
void Camera::computeProjectionMatrix(int width, int height) {
m_projMatrix = glm::mat4(1.0f);
const auto aspect{static_cast<float>(width) / static_cast<float>(height)};
m_projMatrix = glm::perspective(glm::radians(70.0f), aspect, 0.1f, 5.0f);
}
void Camera::computeViewMatrix() {
m_viewMatrix = glm::lookAt(m_eye, m_at, m_up);
}
void Camera::dolly(float speed) {
// Compute forward vector (view direction)
const glm::vec3 forward{glm::normalize(m_at - m_eye)};
// Move eye and center forward (speed > 0) or backward (speed < 0)
m_eye += forward * speed;
m_at += forward * speed;
computeViewMatrix();
}
void Camera::truck(float speed) {
// Compute forward vector (view direction)
const glm::vec3 forward{glm::normalize(m_at - m_eye)};
// Compute vector to the left
const glm::vec3 left{glm::cross(m_up, forward)};
// Move eye and center to the left (speed < 0) or to the right (speed > 0)
m_at -= left * speed;
m_eye -= left * speed;
computeViewMatrix();
}
void Camera::pan(float speed) {
glm::mat4 transform{glm::mat4(1.0f)};
// Rotate camera around its local y axis
transform = glm::translate(transform, m_eye);
transform = glm::rotate(transform, -speed, m_up);
transform = glm::translate(transform, -m_eye);
m_at = transform * glm::vec4(m_at, 1.0f);
computeViewMatrix();
}
No próximo capítulo, quando tivermos visto o conteúdo teórico sobre matrizes de projeção, descreveremos o funcionamento da função Camera::computeProjectionMatrix
. Por enquanto, basta sabermos que ela calcula uma matriz de projeção perspectiva.
Em Camera::computeViewMatrix
, chamamos a função lookAt
da GLM usando os atributos da câmera:
Camera::computeViewMatrix
será chamada sempre que houver alguma alteração em m_eye
ou m_at
.
Em Camera::dolly
, os pontos m_eye
e m_at
são deslocados para a frente ou para trás ao longo da direção de visão (vetor forward
):
void Camera::dolly(float speed) {
// Compute forward vector (view direction)
const glm::vec3 forward{glm::normalize(m_at - m_eye)};
// Move eye and center forward (speed > 0) or backward (speed < 0)
m_eye += forward * speed;
m_at += forward * speed;
computeViewMatrix();
}
Veja que, ao final, Camera::computeViewMatrix
é chamada para reconstruir a matriz de visão.
Camera::truck
funciona de forma parecida com Camera::dolly
. Os pontos m_eye
e m_at
são deslocados nas laterais de acordo com a direção do vetor left
. O vetor left
é o produto vetorial entre o vetor up
e o vetor forward
.
void Camera::truck(float speed) {
// Compute forward vector (view direction)
const glm::vec3 forward{glm::normalize(m_at - m_eye)};
// Compute vector to the left
const glm::vec3 left{glm::cross(m_up, forward)};
// Move eye and center to the left (speed < 0) or to the right (speed > 0)
m_at -= left * speed;
m_eye -= left * speed;
computeViewMatrix();
}
Camera::pan
faz o movimento de girar a câmera em torno de seu eixo \(y\). Isso é feito alterando apenas o ponto m_at
:
void Camera::pan(float speed) {
glm::mat4 transform{glm::mat4(1.0f)};
// Rotate camera around its local y axis
transform = glm::translate(transform, m_eye);
transform = glm::rotate(transform, -speed, m_up);
transform = glm::translate(transform, -m_eye);
m_at = transform * glm::vec4(m_at, 1.0f);
computeViewMatrix();
}
Após a linha 45, a matriz transform
representa uma concatenação de transformações na forma:
\[ \mathbf{M}=\mathbf{I}.\mathbf{T}(\mathbf{p}_{\textrm{eye}}).\mathbf{R}_y(\theta).\mathbf{T}(-\mathbf{p}_{\textrm{eye}}). \]
A ordem de aplicação das transformações é obtida lendo a expressão acima da direita para a esquerda (no código, lemos de baixo para cima, da linha 45 à linha 40):
- \(\mathbf{T}(-\mathbf{p}_{\textrm{eye}})\) (linha 45) tem o efeito de transladar a câmera para a origem do mundo, isto é, faz o ponto \(\mathbf{p}_{\textrm{eye}}\) virar a origem \(O\).
- \(\mathbf{R}_y(\theta)\) (linha 44) roda a câmera em torno do eixo \(y\) do mundo. Como a câmera agora está na origem, é como se a câmera fosse girada em torno de seu próprio eixo \(y\).
- \(\mathbf{T}(\mathbf{p}_{\textrm{eye}})\) (linha 43) é a transformação inversa da primeira, isto é, faz a câmera voltar à sua posição original (mas note que, por causa do passo anterior, a orientação da câmera não é mais a orientação original).
- \(\mathbf{I}\) é a matriz identidade (criada na linha 40).
A linha 47 transforma m_at
por transform
. O resultado é rodar m_at
em torno do eixo \(y\) local da câmera.
As operações da linha 40 até a linha 45 em Camera::pan
são equivalentes ao pseudocódigo:
transform = I;
transform = transform * T(m_eye);
transform = transform * Ry(-speed);
transform = transform * T(-m_eye);
que é o mesmo que
transform = I * T(m_eye) * Ry(-speed) * T(-m_eye);
onde I
, Ry
e T
são as matrizes de transformação identidade, rotação em \(y\), e translação.
openglwindow.hpp
Deixaremos a definição da classe OpenGLWindow
como a seguir:
#ifndef OPENGLWINDOW_HPP_
#define OPENGLWINDOW_HPP_
#include <vector>
#include "abcg.hpp"
#include "camera.hpp"
#include "ground.hpp"
struct Vertex {
glm::vec3 position;
bool operator==(const Vertex& other) const {
return position == other.position;
}
};
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_VAO{};
GLuint m_VBO{};
GLuint m_EBO{};
GLuint m_program{};
int m_viewportWidth{};
int m_viewportHeight{};
Camera m_camera;
float m_dollySpeed{0.0f};
float m_truckSpeed{0.0f};
float m_panSpeed{0.0f};
Ground m_ground;
std::vector<Vertex> m_vertices;
std::vector<GLuint> m_indices;
void loadModelFromFile(std::string_view path);
void update();
};
#endif
O código é semelhante ao do projeto loadmodel
visto no capítulo anterior. As diferenças estão nas seguintes linhas:
- Linhas 7 e 8: inclusão dos cabeçalhos
camera.hpp
eground.hpp
; - Linha 20: declaração da função
handleEvent
para tratar os eventos do teclado; - Linha 36: definição de um objeto da classe
Camera
para controlar a câmera LookAt; - Linhas 37 a 39: definição de variáveis de controle de velocidade de dolly, truck e pan;
- Linha 41: definição de um objeto da classe
Ground
para desenhar o chão. - Linha 47: definição de uma função
update
que será chamada empaintGL
.
Algumas coisas foram removidas em relação ao projeto loadmodel
, como a variável que controlava o número de triângulos exibidos e a função OpenGLWindow::standardize
que normalizava e centralizava o modelo no NDC. Dessa vez, o modelo armazenado no VBO será o modelo sem modificações, isto é, o modelo lido diretamente do arquivo. Para mudar a escala e posição do modelo, usaremos a matriz de modelo.
openglwindow.cpp
O início de openglwindow.cpp
é exatamente o mesmo do projeto anterior:
#include "openglwindow.hpp"
#include <fmt/core.h>
#include <imgui.h>
#include <tiny_obj_loader.h>
#include <cppitertools/itertools.hpp>
#include <glm/gtx/fast_trigonometry.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
A definição de OpenGLWindow::handleEvent
vem a seguir:
void OpenGLWindow::handleEvent(SDL_Event& ev) {
if (ev.type == SDL_KEYDOWN) {
if (ev.key.keysym.sym == SDLK_UP || ev.key.keysym.sym == SDLK_w)
m_dollySpeed = 1.0f;
if (ev.key.keysym.sym == SDLK_DOWN || ev.key.keysym.sym == SDLK_s)
m_dollySpeed = -1.0f;
if (ev.key.keysym.sym == SDLK_LEFT || ev.key.keysym.sym == SDLK_a)
m_panSpeed = -1.0f;
if (ev.key.keysym.sym == SDLK_RIGHT || ev.key.keysym.sym == SDLK_d)
m_panSpeed = 1.0f;
if (ev.key.keysym.sym == SDLK_q) m_truckSpeed = -1.0f;
if (ev.key.keysym.sym == SDLK_e) m_truckSpeed = 1.0f;
}
if (ev.type == SDL_KEYUP) {
if ((ev.key.keysym.sym == SDLK_UP || ev.key.keysym.sym == SDLK_w) &&
m_dollySpeed > 0)
m_dollySpeed = 0.0f;
if ((ev.key.keysym.sym == SDLK_DOWN || ev.key.keysym.sym == SDLK_s) &&
m_dollySpeed < 0)
m_dollySpeed = 0.0f;
if ((ev.key.keysym.sym == SDLK_LEFT || ev.key.keysym.sym == SDLK_a) &&
m_panSpeed < 0)
m_panSpeed = 0.0f;
if ((ev.key.keysym.sym == SDLK_RIGHT || ev.key.keysym.sym == SDLK_d) &&
m_panSpeed > 0)
m_panSpeed = 0.0f;
if (ev.key.keysym.sym == SDLK_q && m_truckSpeed < 0) m_truckSpeed = 0.0f;
if (ev.key.keysym.sym == SDLK_e && m_truckSpeed > 0) m_truckSpeed = 0.0f;
}
}
Os eventos de teclado são tratados de forma separada para as teclas pressionadas (SDL_KEYDOWN
, linhas 24 a 35) e para as teclas liberadas (SDL_KEYUP
, linhas 36 a 51).
Quando uma tecla é pressionada (seta ou QWEASD
), a velocidade de dolly, pan ou truck é modificada para +1 ou -1. Quando a tecla é liberada, a velocidade correspondente volta para 0.
Vamos agora à definição de OpenGLWindow::initializeOpenGL
, que também é bem parecida com a do projeto loadmodel
:
void OpenGLWindow::initializeGL() {
abcg::glClearColor(0, 0, 0, 1);
// Enable depth buffering
abcg::glEnable(GL_DEPTH_TEST);
// Create program
m_program = createProgramFromFile(getAssetsPath() + "lookat.vert",
getAssetsPath() + "lookat.frag");
m_ground.initializeGL(m_program);
// Load model
loadModelFromFile(getAssetsPath() + "bunny.obj");
// Generate 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);
// Generate 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);
// Create VAO
abcg::glGenVertexArrays(1, &m_VAO);
// Bind vertex attributes to current VAO
abcg::glBindVertexArray(m_VAO);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
const GLint 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);
resizeGL(getWindowSettings().width, getWindowSettings().height);
}
Em relação ao projeto anterior, modificamos o nomes dos shaders lidos (linhas 61 a 62), chamamos Ground::initializeGL
na linha 64 (para inicializar o VAO/VBO do chão) e incluímos a chamada a OpenGLWindow::resizeGL
na linha 103.
A função Camera::computeProjectioMatrix
é chamada dentro de OpenGLWindow::resizeGL
para reconstruir a matriz de projeção. Os valores da matriz dependem do tamanho atual da janela. Assim, ao chamarmos OpenGLWindow::resizeGL
em OpenGLWindow::initializeGL
, garantimos que a aplicação começará com uma matriz de projeção válida para as dimensões da janela.
A definição de OpenGLWindow::loadModelFromFile
é a mesma do projeto anterior.
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 for viewMatrix and projMatrix
// These matrices are used for every scene object
abcg::glUniformMatrix4fv(viewMatrixLoc, 1, GL_FALSE,
&m_camera.m_viewMatrix[0][0]);
abcg::glUniformMatrix4fv(projMatrixLoc, 1, GL_FALSE,
&m_camera.m_projMatrix[0][0]);
abcg::glBindVertexArray(m_VAO);
// Draw white bunny
glm::mat4 model{1.0f};
model = glm::translate(model, glm::vec3(-1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(90.0f), glm::vec3(0, 1, 0));
model = glm::scale(model, glm::vec3(0.5f));
abcg::glUniformMatrix4fv(modelMatrixLoc, 1, GL_FALSE, &model[0][0]);
abcg::glUniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f);
abcg::glDrawElements(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT,
nullptr);
// Draw yellow bunny
model = glm::mat4(1.0);
model = glm::translate(model, glm::vec3(0.0f, 0.0f, -1.0f));
model = glm::scale(model, glm::vec3(0.5f));
abcg::glUniformMatrix4fv(modelMatrixLoc, 1, GL_FALSE, &model[0][0]);
abcg::glUniform4f(colorLoc, 1.0f, 0.8f, 0.0f, 1.0f);
abcg::glDrawElements(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT,
nullptr);
// Draw blue bunny
model = glm::mat4(1.0);
model = glm::translate(model, glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(-90.0f), glm::vec3(0, 1, 0));
model = glm::scale(model, glm::vec3(0.5f));
abcg::glUniformMatrix4fv(modelMatrixLoc, 1, GL_FALSE, &model[0][0]);
abcg::glUniform4f(colorLoc, 0.0f, 0.8f, 1.0f, 1.0f);
abcg::glDrawElements(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT,
nullptr);
// Draw red bunny
model = glm::mat4(1.0);
model = glm::scale(model, glm::vec3(0.1f));
abcg::glUniformMatrix4fv(modelMatrixLoc, 1, GL_FALSE, &model[0][0]);
abcg::glUniform4f(colorLoc, 1.0f, 0.25f, 0.25f, 1.0f);
abcg::glDrawElements(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT,
nullptr);
abcg::glBindVertexArray(0);
// Draw ground
m_ground.paintGL();
abcg::glUseProgram(0);
}
A função OpenGLWindow::update
é chamada logo do início (linha 161) para atualizar a posição e orientação da câmera LookAt.
Nas linhas 179 e 184, o conteúdo das matrizes de visão e projeção é enviado às variáveis uniformes no shader:
// Set uniform variables for viewMatrix and projMatrix
// These matrices are used for every scene object
abcg::glUniformMatrix4fv(viewMatrixLoc, 1, GL_FALSE,
&m_camera.m_viewMatrix[0][0]);
abcg::glUniformMatrix4fv(projMatrixLoc, 1, GL_FALSE,
&m_camera.m_projMatrix[0][0]);
Observe o uso da função glUniformMatrix4fv
. Essa função tem a assinatura
void glUniformMatrix4fv(GLint location,
GLsizei count,
GLboolean transpose,const GLfloat *value);
onde
location
é o identificador de localização da variável uniforme no shader;count
é o número de matrizes que queremos transferir à variável uniforme;transpose
é um valor booleano que indica se queremos enviar a transposta da matriz;value
é o ponteiro para o primeiro elemento do arranjo de elementos da matriz.
A renderização do coelho branco é configurada nas linhas 188 a 197:
// Draw white bunny
glm::mat4 model{1.0f};
model = glm::translate(model, glm::vec3(-1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(90.0f), glm::vec3(0, 1, 0));
model = glm::scale(model, glm::vec3(0.5f));
abcg::glUniformMatrix4fv(modelMatrixLoc, 1, GL_FALSE, &model[0][0]);
abcg::glUniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f);
abcg::glDrawElements(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT,
nullptr);
Nas linhas 189 a 192 é criada a concatenação de transformações que forma a matriz de modelo (model
). Para o coelho branco, essa concatenação é
\[
\mathbf{M}_{\textrm{model}}=\mathbf{I}.\mathbf{T}(-1,0,0).\mathbf{R}_y\left(\frac{\pi}{2}\right).\mathbf{S}(0.5, 0.5, 0.5).
\]
Essas transformações servem para posicionar o modelo do coelho no mundo. Inicialmente o modelo está na posição e orientação definida no arquivo bunny.obj
: na origem, sobre o plano \(y=0\), como vimos na figura 7.17. As transformações são aplicadas da seguinte forma:
- Transformação de escala para reduzir o tamanho do coelho para \(50\%\) de seu tamanho original (linha 192);
- Rotação em \(90^{\circ}\) em torno do eixo \(y\) do espaço do objeto, que é o mesmo eixo \(y\) do espaço do mundo (linha 191);
- Translação pelo vetor \((-1,0,0)\), que posiciona o coelho em sua posição final na cena (linha 190);
- Transformação identidade (linha 189).
Na linha 194, a matriz de modelo é enviada à variável uniforme m_modelMatrix
no vertex shader.
Na linha 195, a variável uniforme color
é definida com \((1,1,1,1)\) (branco) no vertex shader.
Finalmente, na linha 196 é feita a chamada ao comando de renderização.
Observe como um procedimento semelhante é feito para os outros coelhos. Mudam apenas as transformações que serão usadas para criar a matriz model
, e o valor de cor definido na variável uniforme color
.
Para o coelho amarelo:
// Draw yellow bunny
model = glm::mat4(1.0);
model = glm::translate(model, glm::vec3(0.0f, 0.0f, -1.0f));
model = glm::scale(model, glm::vec3(0.5f));
abcg::glUniformMatrix4fv(modelMatrixLoc, 1, GL_FALSE, &model[0][0]);
abcg::glUniform4f(colorLoc, 1.0f, 0.8f, 0.0f, 1.0f);
abcg::glDrawElements(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT,
nullptr);
Para o coelho azul:
// Draw blue bunny
model = glm::mat4(1.0);
model = glm::translate(model, glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(-90.0f), glm::vec3(0, 1, 0));
model = glm::scale(model, glm::vec3(0.5f));
abcg::glUniformMatrix4fv(modelMatrixLoc, 1, GL_FALSE, &model[0][0]);
abcg::glUniform4f(colorLoc, 0.0f, 0.8f, 1.0f, 1.0f);
abcg::glDrawElements(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT,
nullptr);
Para o pequeno coelho vermelho:
// Draw red bunny
model = glm::mat4(1.0);
model = glm::scale(model, glm::vec3(0.1f));
abcg::glUniformMatrix4fv(modelMatrixLoc, 1, GL_FALSE, &model[0][0]);
abcg::glUniform4f(colorLoc, 1.0f, 0.25f, 0.25f, 1.0f);
abcg::glDrawElements(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT,
nullptr);
Note que todos os modelos foram renderizados com o mesmo VAO (linha 186), pois todos compartilham o mesmo VBO. É a matriz de modelo que faz com que cada coelho tenha uma transformação diferente no cenário 3D.
No final de OpenGLWindow::paintGL
, temos o seguinte código:
Na linha 229, o VAO dos coelhos deixa de ser usado. Em seguida, na linha 232, o chão é desenhado. O chão tem seu próprio VAO, mas usa os mesmos shaders dos coelhos. É por isso que os shaders só são desabilitados na linha 234 com a chamada a glUseProgram(0)
.
A definição de OpenGLWindow::terminateGL
ficará como a seguir:
void OpenGLWindow::terminateGL() {
m_ground.terminateGL();
abcg::glDeleteProgram(m_program);
abcg::glDeleteBuffers(1, &m_EBO);
abcg::glDeleteBuffers(1, &m_VBO);
abcg::glDeleteVertexArrays(1, &m_VAO);
}
Não há nada de muito novo nesse código, exceto a chamada a Ground::terminateGL
para liberar o VAO e VBO do chão.
Finalmente, a definição de OpenGLWindow::update
ficará como a seguir:
void OpenGLWindow::update() {
const float deltaTime{static_cast<float>(getDeltaTime())};
// Update LookAt camera
m_camera.dolly(m_dollySpeed * deltaTime);
m_camera.truck(m_truckSpeed * deltaTime);
m_camera.pan(m_panSpeed * deltaTime);
}
Aqui, as funções de movimentação da câmera são chamadas usando as variáveis de velocidade que tiveram seus valores determinados em OpenGLWindow::handleEvent
de acordo com as teclas pressionadas.
ground.hpp
A classe Ground
é responsável pelo desenho do chão. Embora não seja uma classe derivada de abcg::OpenGLWindow
, os nomes de funções são os mesmos (initializeGL
, paintGL
e terminateGL
). Como vimos anteriormente, essas funções são chamadas nas respectivas funções de OpenGLWindow
:
#ifndef GROUND_HPP_
#define GROUND_HPP_
#include "abcg.hpp"
class Ground {
public:
void initializeGL(GLuint program);
void paintGL();
void terminateGL();
private:
GLuint m_VAO{};
GLuint m_VBO{};
GLint m_modelMatrixLoc{};
GLint m_colorLoc{};
};
#endif
Ground::initializeGL
recebe como parâmetro o identificador de um programa de shader já existente. Assim, o chão pode usar os mesmos shaders dos coelhos.
Em Ground::paintGL
, veremos que o chão é desenhado como um padrão de xadrez. Como é um padrão composto por quadriláteros, o VBO não precisa ser a malha geométrica do chão inteiro. O VBO é apenas um quadrilátero de tamanho unitário. Em Ground::paintGL
, esse quadrilátero será desenhado várias vezes para formar um ladrilho com padrão de xadrez.
ground.cpp
Vamos começar com a definição de Ground::initializeGL
:
#include "ground.hpp"
#include <cppitertools/itertools.hpp>
void Ground::initializeGL(GLuint program) {
// Unit quad on the xz plane
std::array vertices{glm::vec3(-0.5f, 0.0f, 0.5f),
glm::vec3(-0.5f, 0.0f, -0.5f),
glm::vec3( 0.5f, 0.0f, 0.5f),
glm::vec3( 0.5f, 0.0f, -0.5f)};
// Generate VBO
abcg::glGenBuffers(1, &m_VBO);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
abcg::glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices.data(),
GL_STATIC_DRAW);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// Create VAO and bind vertex attributes
abcg::glGenVertexArrays(1, &m_VAO);
abcg::glBindVertexArray(m_VAO);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
const GLint posAttrib{abcg::glGetAttribLocation(program, "inPosition")};
abcg::glEnableVertexAttribArray(posAttrib);
abcg::glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
abcg::glBindVertexArray(0);
// Save location of uniform variables
m_modelMatrixLoc = abcg::glGetUniformLocation(program, "modelMatrix");
m_colorLoc = abcg::glGetUniformLocation(program, "color");
}
No início da função, definimos os vértices de um quadrilátero de tamanho unitário centralizado no plano \(xz\). Em seguida, criamos o VBO e fazemos a ligação do VBO com o atributo inPosition
do shader program
. Por fim, salvamos a localização das variáveis uniformes que serão utilizadas em Ground::paintGL
.
A propósito, eis o código de Ground::paintGL
:
void Ground::paintGL() {
// Draw a grid of tiles centered on the xz plane
const int N{5};
abcg::glBindVertexArray(m_VAO);
for (const auto z : iter::range(-N, N + 1)) {
for (const auto x : iter::range(-N, N + 1)) {
// Set model matrix
glm::mat4 model{1.0f};
model = glm::translate(model, glm::vec3(x, 0.0f, z));
abcg::glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &model[0][0]);
// Set color (checkerboard pattern)
const float gray{(z + x) % 2 == 0 ? 1.0f : 0.5f};
abcg::glUniform4f(m_colorLoc, gray, gray, gray, 1.0f);
abcg::glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
}
abcg::glBindVertexArray(0);
}
Aqui, desenhamos uma grade de 11x11 quadriláteros (variando \(z\) e \(x\) de -5 a 5). Cada quadrilátero é transladado através de uma matriz de modelo e então desenhado com glDrawArrays
usando a primitiva GL_TRIANGLE_STRIP
. A cor utilizada (configurada pela variável uniforme do shader) é modificada de acordo com a paridade das coordenadas da grade de modo a formar o padrão de xadrez.
Em Ground::terminateGL
, apenas o VBO e o VAO são liberados:
void Ground::terminateGL() {
abcg::glDeleteBuffers(1, &m_VBO);
abcg::glDeleteVertexArrays(1, &m_VAO);
}
Como o programa de shader é o mesmo dos coelhos, o responsável pela liberação dos shaders é OpenGLWindow
, como vimos em OpenGLWindow::terminateGLP
.
Isso conclui o projeto lookat
. Baixe o código completo a partir deste link.