Neste projeto, implementaremos a projeção de um mundo 3D em uma tela 2D usando o algoritmo da pinhole camera.
O objetivo deste projeto é fazer uma projeção em tempo real de um cubo em wireframe que gira em todas as direções.
- Para isso, vamos primeiro analisar a matriz que representa as posições dos vértices do cubo em um plano 3D.
Matriz inicial do Cubo 3D
- Agora, vamos analisar a matriz final deseja que representa as posições dos vértices do cubo em um plano 2D.
Matriz Final Desejada representação do Cubo 2D
Objetivo: Para representar o cubo em 2D, é necessário encontrar as projeções Xp e YP das coordenadas de cada ponto do cubo 3D.
Passo 1: Encontrar as projeções Xp dos pontos do Cubo.
- Para isso vamos assumir um ponto genérico (Xo,Yo,Zo) do cubo.
- Como queremos encontrar apenas a projeção de X nesse momento, iremos imaginar que esse ponto não pode se movimentar no eixo y.
- Neste momento, vamos usar conhecimentos de geometria para simular uma câmera no espaço 2D - mais especificamente, o plano cartesiano
$x,z$
- O pinhole, que é o buraco por onde a luz entra, ficará exatamente na origem do plano cartesiano, isto é, no ponto
$[0,0]$ . - O anteparo está a uma distância
$d$ do pinhole - O anteparo ficará sobre a reta
$z=-d$ , que é a reta horizontal que passa pelo ponto$[0,-d]$ .
Um ponto que está no ponto
Onde, x_p será a nossa projeção de x desejada e z_p será constante e igual a -d.
- Como demonstrado na imagem chegamos no Sitema de equação considerando a projecao em X.
- Para encontrar a projeção em Y podemos repetir o mesmo processo, encontraremos o Sitema de equação considerando a projecao em Y
- Com os dois sistemas, podemos selecionar apenas as equações que contenham a projeção Xp e yp, e o valor da incógnita W, que será necessária para encontrar os valores de Xp e Yp. Com isso montamos o Sitema de equação considerando as projecoes em X e Y (2D)
Matriz Projecao ou M pinhole
Dividindo a matriz cubo_2d por w chegamos na matriz projecoes_2d, onde estão as projeções possíveis para plotar o a projeção do cubo na plano 2D.
Primeiramente, precisávamos importar as bibliotecas necessárias para o desenvolvimento do projeto. Para isso, utilizamos a biblioteca numpy para trabalhar com matrizes e vetores, e pygame para a renderização gráfica do cubo.
import numpy as np
import pygame
Para que executássemos com sucesso o código, precisávamos definir algumas variáveis importantes para a construção das matrizes que seriam usadas na transformação do cubo:
- d: variável que armazena a distância "focal" da câmera, que é usada para a projeção do cubo no plano 2D. Na renderização gráfica, a distância focal é definida como a distância entre o centro da tela e o centro da projeção do cubo no plano 2D. Essa variável precisa ser grande o suficiente para que o usuário consiga visualizar o cubo na tela, mas não muito grande para que o cubo não fique muito pequeno.
d = 700
- angulo: variável que armazena o ângulo de rotação do cubo em relação aos eixos. Esse valor é usado para a construção das matrizes de rotação, as quais dependem do valor do coseno e do seno do ângulo. No código, o ângulo é definido como 1 grau para que as transformações fossem feitas de forma suave ("lenta").
angulo = 1
No código, utilizamos grande parte das matrizes que descrevemos no modelo matemático. Para facilitar a visualização, listamos abaixo as matrizes que foram utilizadas no código:
-
cubo: esta matriz representa os pontos do cubo 3D
cubo = np.array([[-150, -150, -150, 1], [150, -150, -150, 1], [150, 150, -150, 1], [-150, 150, -150, 1], [-150, -150, 150, 1], [150, -150, 150, 1], [150, 150, 150, 1], [-150, 150, 150, 1]]).T
Nesta matriz, cada coluna representa um ponto do cubo 3D. Por exemplo, a primeira coluna representa o ponto (-150, -150, -150), a segunda coluna representa o ponto (150, -150, -150), e assim por diante. Quando transposta, cada coluna representa um ponto do cubo.
-
Matrizes de rotação: estas matrizes são responsáveis por rotacionar o cubo em relação aos eixos.
# Rotação em relação ao eixo X rotacao_x = np.array([[1, 0, 0, 0], [0, np.cos(angulo), -np.sin(angulo), 0], [0, np.sin(angulo), np.cos(angulo), 0], [0, 0, 0, 1]]) # Rotação em relação ao eixo Y rotacao_y = np.array([[np.cos(angulo), 0, np.sin(angulo), 0], [0, 1, 0, 0], [-np.sin(angulo), 0, np.cos(angulo), 0], [0, 0, 0, 1]]) # Rotação em relação ao eixo Z rotacao_z = np.array([[np.cos(angulo), -np.sin(angulo), 0, 0], [np.sin(angulo), np.cos(angulo), 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
-
rotacao_total: matriz criada através da multiplicação matricial de todas as matrizes de rotação, resultando, portanto, em uma única matriz responsável por todo o movimento do cubo.
rotacao_total = rotacao_z @ rotacao_y @ rotacao_x
No loop principal do código, esta matriz precisa ser atualizada a cada iteração, pois o ângulo de rotação é incrementado em 1 grau a cada iteração. Caso contrário, o cubo não iria se movimentar. Logo, dentro do loop principal, a matriz de rotação total é atualizada da seguinte forma:
rotacao_total = rotacao_total @ rotacao_z @ rotacao_y @ rotacao_x
-
-
translacao_z: matriz responsável portransladar o cubo em relação ao eixo Z. Essa matrizé usada para que usuário consiga visualizar o cubo.
translacao_z = np.array([[1, 0, 0, 0], [0, 1, 0,0], [0, 0, 1, d], [0, 0, 0, 1]])
-
translacao_centro: matriz responsável por transladar o cubo para o centro da tela. Caso esta matriz não existisse, o cubo estaria presente no ponto (0,0), o qual na janela criada pela biblioteca pygame seria no canto superior esquerdo.
translacao_centro = np.array([[1, 0, 0, 400], [0, 1, 0, 300], [0, 0, 1, 0], [0, 0, 0, 1]])
Para transladar para o centro da janela do pygame, precisávamos transladar o cubo para o ponto (400, 300), pois a janela do pygame tem dimensões 800x600.
-
m_pinhole: esta matriz equivale à matriz de projeção do modelo matemático. Ela é responsável por projetar o cubo no plano 2D.
m_pinhole = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -d], [0, 0, -(1/d), 0]])
-
M: esta matriz representa todas as transformações aplicadas sobre o cubo:
M = translacao_centro @ m_pinhole @ translacao_z @ rotacao_total
-
final: matriz final, resultante da multiplicação matricial entre a matriz M e a matriz cubo. Esta matriz representa os pontos do cubo projetados no plano 2D.
final = M @ cubo
Após aplicar todas as transformações necessárias no cubo utilizando as matrizes descritas no bloco acima e desenhar as linhas do cubo que ligam todos os pontos definidos na matriz cubo, conseguimos gerar com sucesso um cubo 3D rotacionando em 3 dimensões e projetado em 2D:
Álem disso, implementamos a funcionalidade de afastar / aproximar o cubo da tela utilizando o scroll do mouse. Isso foi feito através da altreação do valor da variável d, que é responsável por transladar o cubo em relação ao eixo Z. Quando o usuário aproxima o cubo da tela, o valor de d é decrementado, e quando o usuário afasta o cubo da tela, o valor de d é incrementado.
Para aproximar o cubo da tela, basta usar o scroll do mouse para cima. Para afastar o cubo da tela, faça o contrário, ou seja, use o scroll do mouse para baixo.
# Aproxima o cubo da tela
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 4: # Scroll para cima
d += 25
translacao_z = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, d], [0, 0, 0, 1]])
M = translacao_centro @ m_pinhole @ translacao_z @ rotacao_total
final = M @ cubo
if event.button == 5: # Scroll para baixo
d -= 25
translacao_z = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, d], [0, 0, 0, 1]])
M = translacao_centro @ m_pinhole @ translacao_z @ rotacao_total
final = M @ cubo
Estas alterações podem ser visualizadas no gif abaixo:
1. Clone o repositório:
git clone https://github.com/alessitomas/2D-cube-projection.git
2. Instale as dependências:
pip install -r requirements.txt
3. Execute o arquivo main.py:
python main.py
4. Agora abrirá uma janela do pygame com o cubo 3D projetado em 2D. Assim como dito anteriormente, caso queira aproximar o cubo da tela, use o scroll do mouse para cima. Caso queira afastar o cubo da tela, use o scroll do mouse para baixo.