Introdução
Nesta implementação, renderizamos um triângulo tridimensoinal em uma tela bidimensional. Os vértices iniciais do triângulo são definidos como v0(2.0, 0.0, −2.0), v1(0.0, 2.0, −2.0) e v2(−2.0, 0.0, −2.0). O objetivo é converter essas coordenadas para o espaço da tela e desenhar o contorno do triângulo.
Matriz MVP (Modelo-Visualização-Projeção)
Em gráficos computacionais, a matriz MVP é a cadeia de transformações que mapeia pontos do espaço 3D para a tela 2D. Ela consiste em três etapas principais:
- Transformação de Modelo: Converte coordenadas do espaço local do objeto para o espaço global, aplicando translação, rotação e escala.
- Transformação de Visualização: Adapta o espaço global para a perspectiva da câmera, simulando sua posição e orientação.
- Transformação de Projeção: Projeta o espaço da câmera para o espaço de recorte, suportando projeções perspectiva ou ortográfica.
Em resumo, a matriz MVP é o produto das matrizes de projeção, visualização e modelo, nesta ordem. Para animar um objeto, modifica-se a matriz de modelo, controlando sua posição, direção e tamanho no espaço global.
Espaços de Coordenadas
- Espaço do Modelo: Sistema de coordenadas local a cada objeto.
- Espaço Global: Coordenadas unificadas para todos os objetos após transformações de modelo.
- Espaço da Câmera: Perspectiva da câmera sobre a cena.
- Espaço de Recorte: Resultado da transformação de projeção, preparado para renderização.
Arquitetura do Código
Classe Renderizador
Implementa a rasterização de linhas e triângulos em um pipeline de software. Gerencia buffers de vértices, índices, framebuffer e profundidade.
Estruturas e Tipos
- BufferType: Enumeração para tipos de buffer (cor, profundidade), suportando operações bitwise.
- TipoPrimitiva: Define primitivas gráficas (linha, triângulo).
- ID_BufferVertices, ID_BufferIndices: Encapsulam IDs dos buffers para type safety.
Membros e Métodos Principais
- Variáveis: Matrizes de transformação (modelo, visualização, projeção), buffers, dimensões da janela.
- Métodos:
carregar_vertices()ecarregar_indices(): Armazenam dados e retornam IDs.definir_modelo(),definir_visualizacao(),definir_projecao(): Configuram as matrizes.limpar(): Reinicializa os buffers de cor e profundidade.desenhar(): Executa transformações e rasterização.definir_pixel(): Atualiza o framebuffer.obter_framebuffer(): Retorna o conteúdo do buffer.
- Métodos Privados:
desenhar_linha(): Algoritmo de Bresenham para linhas.rasterizar_contorno(): Desenha as bordas de um triângulo.calcular_indice(): Mapeia coordenadas de pixel para o buffer.
Buffer de Profundidade
O buffer de profundidade (Z-buffer) resolve a oclusão, garantindo que pixels próximos à câmera obscureçam distantes. Durante a rasterização:
- Calcula-se a profundidade (z) de cada pixel.
- Compara-se com o valor armazenado no buffer.
- Se mais próximo, atualiza-se a cor e a profundidade; caso contrário, ignora-se.
No código, o buffer é inicializado com infinito (std::numeric_limits<float>::infinity()) ao limpar.
Implementação Básica
A versão básica renderiza o triângulo estático, sem rotação. O loop principal configura as transformações e desenha o triângulo.
// Fragmento do loop principal (adaptado)
while (tecla != 27) {
renderizador.limpar(BufferType::Cor | BufferType::Profundidade);
renderizador.definir_modelo(obter_matriz_modelo(angulo));
renderizador.definir_visualizacao(obter_matriz_visualizacao(posicao_camera));
renderizador.definir_projecao(obter_matriz_projecao(45, 1, 0.1, 50));
renderizador.desenhar(id_vertices, id_indices, TipoPrimitiva::Triangulo);
cv::Mat imagem(700, 700, CV_32FC3, renderizador.obter_framebuffer().data());
imagem.convertTo(imagem, CV_8UC3, 1.0f);
cv::imshow("renderizacao", imagem);
tecla = cv::waitKey(10);
}
Fluxo de Transformação
- Transformação de Modelo:
obter_matriz_modelo(angulo)constrói uma matriz de rotação no espaço global. - Transformação de Visualização:
obter_matriz_visualizacao(posicao_camera)ajusta o espaço para a câmera. - Transformação de Projeção:
obter_matriz_projecao(fov, razao_aspecto, z_perto, z_longe)converte para o espaço de recorte, com conversão necessária para aplicar a fórmula.
Detalhes da Função desenhar()
Esta função recebe IDs dos buffers de vértices e índices, e processa cada triângulo.
// Carregamento de vértices e índices
std::vector<Eigen::Vector3f> pontos{{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}};
auto id_pontos = renderizador.carregar_vertices(pontos);
std::vector<Eigen::Vector3i> indices{{0, 1, 2}};
auto id_indices = renderizador.carregar_indices(indices);
No método desenhar():
void Renderizador::desenhar(ID_BufferVertices id_vertices, ID_BufferIndices id_indices, TipoPrimitiva tipo) {
if (tipo != TipoPrimitiva::Triangulo) {
throw std::runtime_error("Somente triângulos são suportados!");
}
auto& vertices = buffer_vertices[id_vertices.id];
auto& indices = buffer_indices[id_indices.id];
float fator_escala = (100 - 0.1) / 2.0f;
float deslocamento = (100 + 0.1) / 2.0f;
Eigen::Matrix4f mvp = matriz_projecao * matriz_visualizacao * matriz_modelo;
for (auto& idx : indices) {
Triangulo tri;
Eigen::Vector4f coords[] = {
mvp * para_vec4(vertices[idx[0]], 1.0f),
mvp * para_vec4(vertices[idx[1]], 1.0f),
mvp * para_vec4(vertices[idx[2]], 1.0f)
};
// Divisão perspectiva
for (auto& coord : coords) {
coord /= coord.w();
}
// Transformação de viewport
for (auto& vert : coords) {
vert.x() = 0.5f * largura * (vert.x() + 1.0f);
vert.y() = 0.5f * altura * (vert.y() + 1.0f);
vert.z() = vert.z() * fator_escala + deslocamento;
}
// Atribuir vértices ao triângulo
for (int i = 0; i < 3; ++i) {
tri.definirVertice(i, coords[i].head<3>());
}
tri.definirCor(0, 255.0f, 0.0f, 0.0f);
tri.definirCor(1, 0.0f, 255.0f, 0.0f);
tri.definirCor(2, 0.0f, 0.0f, 255.0f);
rasterizar_contorno(tri);
}
}
O contorno é desenhado usando linhas:
void Renderizador::rasterizar_contorno(const Triangulo& t) {
desenhar_linha(t.verticeC(), t.verticeA());
desenhar_linha(t.verticeC(), t.verticeB());
desenhar_linha(t.verticeB(), t.verticeA());
}
Os pixels são definidos no framebuffer:
void Renderizador::definir_pixel(const Eigen::Vector3f& ponto, const Eigen::Vector3f& cor) {
if (ponto.x() < 0 || ponto.x() >= largura || ponto.y() < 0 || ponto.y() >= altura) return;
auto indice = (altura - ponto.y()) * largura + ponto.x();
framebuffer[indice] = cor;
}
Esta abordagem usa a biblioteca Eigen para álgebra linear, com type safety via IDs, suporte a Bresenham para linhas, e teste de profundidade para oclusão.