Transformação MVP e Rasterização em Gráficos Computacionais

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:

  1. Transformação de Modelo: Converte coordenadas do espaço local do objeto para o espaço global, aplicando translação, rotação e escala.
  2. Transformação de Visualização: Adapta o espaço global para a perspectiva da câmera, simulando sua posição e orientação.
  3. 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() e carregar_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:

  1. Calcula-se a profundidade (z) de cada pixel.
  2. Compara-se com o valor armazenado no buffer.
  3. 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

  1. Transformação de Modelo: obter_matriz_modelo(angulo) constrói uma matriz de rotação no espaço global.
  2. Transformação de Visualização: obter_matriz_visualizacao(posicao_camera) ajusta o espaço para a câmera.
  3. 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.

Tags: Computer Graphics MVP Transformation Rasterization Bresenham Algorithm Eigen

Publicado em 7-5 02:07