Skip to content

Latest commit

 

History

History
526 lines (386 loc) · 18.2 KB

README.md

File metadata and controls

526 lines (386 loc) · 18.2 KB

Algoritmo Software Render e Wireframe

Trabalho de Computação Gráfica, faculdade de Ciência da Computação, UNIOESTE (Universidade Estadual do Oeste do Paraná)

Desenvolvido por: Gabriel Mazzuco e Rodrigo Rocha

Introdução

Neste trabalho foi desenvolvido um algoritmo de renderização de polígonos 3D, utilizando a linguagem de programação Python e a biblioteca tkinter. Primeiramento foi realizado a coleta dos pontos num ambiente 2D chamado de Wireframe, posteriormente foi realizado a revolução do objeto em torno de um eixo, gerando um objeto em 3D


Implementação

O objeto 3D está apresentado na técnica wireframe, sem ocultação de superfícies. O número de fatias usadas na revolução foi igual a 30, ou seja, a cada 360º/30 = 12º criou-se um paralelo para delimitar as faces do objeto. É possivel colocar mais de um objeto em tela também, basta apenas desenhar os objetos no wireframe.

No wireframe é possível escolher a quantidade de fatias que o objeto terá para revolucionar, alem de escolher a própria cor do objeto

Com o sólido totalmente gerado, parte-se para a rederização do objeto, é feito a partir do tkinter em um canvas determinado, mostrando-o previamente em tela. É feito a partir de uma técnica de renderização de polígonos 3D em um ambiente 2D, utilizando a técnica de projeção

Parametrização

Para realizar as matrizes de transformações, o usuário deverá informar os valores utilizados na view-port, view-up, vrp, ponto focal, distância ao plano e da janela do mundo. Dependendo dos valores informados, o objeto terá uma visualização diferente.

Com estes valores, é realizado o pipeline que consiste na transformação dos pontos do objeto 3D, utilizando as matrizes SRU (Sistema de Referência do Universo), de projeção (perspectiva ou ortogonal), janela view-port e por fim a matriz SRT (Sistema de Referência da Tela).

Na realização da parametrização do sombreamento, será passado os valores de refração ambiente (Ka), difusa (Kd) e especular (Ks), além do grau de luz do ambiente (Ila) e da luz focal (Il). Por fim também deve-se informar a aproximação da distribuição espacial da luz refletida especularmente (n)

Depois de realizar a parametrização de todos os valores, é possivel realizar o sombreamento (constante) em todas as faces do objeto, com base nos calculos de iluminação, visto durante a matéria de Computação Gráfica.

Detalhes da implementação

O código funciona com três núcleos principais, sendo eles o wireframe, a revolução do objeto e o software render, sendo todos implementados usando orientação a objetos. São criado duas telas direntes, primeiro sendo o próprio wireframe, e posteriormente ele é "destruido", dando lugar ao software render.

  • Todo o programa, só é possivel por causa da utilização do openmesh pois ele cria uma malha com os pontos aonde é salvo seus vertices, faces geradas pós a revolução
# Wireframe
    
# Cria a classe do wireframe
screen_wireframe = Screen_Wireframe()

# Deleta os arquivos de pontos
screen_wireframe.delete_points_file()

# Registra os clicks no programa
screen_wireframe.register_click()

# Roda o programa
screen_wireframe.run()

O cerne do programa está na manipulação da lista de classes, uma lista em que possui todo o objeto armazenado no arquivo object_3d.py

# Revolução do objeto
files_classes = []

# Cria a quantidade de classes de objetos referente a quantidade de arquivos de pontos
for file in os.listdir(os.path.join("Wireframe", "points")):
        
    # Cria as classes dos objetos
    files_classes.append(Points_Object(file))
    
# Converte os pontos dos arquivos para pontos na linha
for file in files_classes:
    file.points_file_to_points_line()
    
# Roda a revolução dos objetos
for file in files_classes:
    file.revolucion()

Dentro do arquivo object_3d.py, temos toda a classe, indo dos seus pontos 2D resgatados do wireframe, até seus pontos xn, yn e zn posteriores a sua revolução. Além de salvar qual a sua cor principal e seus parametros de transformações (rotação, translacao e escala) que são funções dentro do objeto.

class Points_Object():
    
    def __init__(self, name):
        
        # Nome do arquivo
        self.name = name
        
        # Pontos no campo 2D
        self.points_line = []    
        self.x = []
        self.y = []
        
        # Pontos no campo 3D
        self.points_x = []
        self.points_y = []
        self.points_z = []
        
        # Revolução
        self.slices = None
        self.theta = None
        self.xn = None
        self.yn = None
        self.zn = None
        
        # Define a cor do objeto
        self.color = None
        
        # Define os parametros de rotacao, translacao e escala
        self.rotacao = np.array([0, 0, 0]) # -> [rotacao_x, rotacao_y, rotacao_z]
        self.translacao = np.array([0, 0, 0])
        self.escala = 1
def update_rotacao(self, dx, dy, dz):
    self.rotacao += np.array([dx, dy, dz])
    
def update_translacao(self, dx, dy):
    self.translacao += np.array([dx, dy, 0])
    
def update_escala(self, fator):
    self.escala *= fator
    
def update_color(self, color):
    self.color = color

Para a realização das transformações do objeto em tela, são usado as matrizes abaixo para calcular a posição nova do objeto, dependendo de qual foi escolhida

$$Translacao \rightarrow \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & dx \\ 0 & 1 & dy \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} $$

$$Escala \rightarrow \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} S_x & 0 & 0 \\ 0 & S_y & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} $$

$$Rotacao \ em \ x \rightarrow \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 \\ 0 & \cos(\theta_x) & -\sin(\theta_x) \\ 0 & \sin(\theta_x) & \cos(\theta_x) \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} $$

$$Rotacao \ em \ y \rightarrow \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} \cos(\theta_y) & 0 & \sin(\theta_y) \\ 0 & 1 & 0 \\ -\sin(\theta_y) & 0 & \cos(\theta_y) \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} $$

$$Rotacao \ em \ z \rightarrow \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} \cos(\theta_z) & -\sin(\theta_z) & 0 \\ \sin(\theta_z) & \cos(\theta_z) & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} $$

Dentro do software render, temos toda a composição da tela que nem o wireframe, mas toda a manipulação é feita a partir do tkinter. Com parametros definidos, ele começa a realizar as transformações, com vários arquivos em várias outras pastas, como são as matrizes do pipeline

# Calcula a matriz de transformação Msru_src
self.Matrix_sru_src = Msru_src(vrp_x, vrp_y, vrp_z, 
                    ponto_focal_x, ponto_focal_y, ponto_focal_z, 
                    view_up_x, view_up_y, view_up_z)

# Calcula a matriz de projeção
self.Matrix_proj = Mproj_perspectiva(dp)

# Calcula a matriz de janela de projeção
self.Matrix_jp = Mjp(uMin, uMax, vMin, vMax,
            janela_mundo_xMin, janela_mundo_xMax, janela_mundo_yMin, janela_mundo_yMax)

# Calcula a matriz de transformação Msru_srt
self.Matrix_sru_srt = Msru_srt(self.Matrix_sru_src, self.Matrix_proj, self.Matrix_jp)

Já para a realização das matrizes de projeção, são utilizado as fórmulas abaixo para chegar no resultado esperado

  • Para a realização da matriz SRU para SRC

$$ \vec{N} = \vec{VRP} - \vec{P} \\ \hat{n} = \frac{\vec{N}}{|\vec{N}|} = (n_1, n_2, n_3) $$


$$ \vec{V} = \vec{Y} - (\vec{Y} \cdot \hat{n})\hat{n} \\ \hat{v} = \frac{\vec{V}}{|\vec{V}|} = (v_1, v_2, v_3) $$


$$ \vec{u} = \vec{v} \times \vec{n} $$


$$ M_{SRU, SRC} = R \cdot T = \begin{bmatrix} u_{1} & u_{2} & u_{3} & -VRP\cdot\hat{u} \\ v_{1} & v_{2} & v_{3} & -VRP\cdot\hat{v} \\ n_{1} & n_{2} & n_{3} & -VRP\cdot\hat{n}\\ 0 & 0 & 0 & 1 \end{bmatrix} $$

  • Para a realização da matriz de projeção

$$ M_{ort} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \ ou \ M_{proj}=\begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & \frac{-z_{vp}}{d_p} & z_{vp}(\frac{z_{prp}}{d_p}) & 0 \\ 0 & \frac{-1}{d_p} & \frac{z_{prp}}{d_p} & 1 \end{bmatrix} $$

  • Para a realização da matriz de janela de projeção

$$ M_{jp} = \begin{bmatrix} \frac{u_{max} - u_{min}}{x_{max} - x_{min}} & 0 & 0 & -\frac{x_{min}(u_{max} - u_{min})}{x_{max} - x_{min}} + u_{min} \\ 0 & \frac{v_{min} - v_{max}}{y_{max} - y_{min}} & 0 & -\frac{y_{min}(v_{max} - v_{min})}{y_{max} - y_{min}} + v_{min} \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} $$

  • Por fim, realiza-se a concatenação das matrizes para obter a matriz de transformação final

$$ P'=M_{SRU,SRT} \cdot P_{SRU} \rightarrow \begin{bmatrix} x_h \\ y_h \\ z' \\ h \end{bmatrix} = M_{SRU, SRT} \cdot \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} $$

$$ x_{SRT}= \frac{x_h}{h} \ \ y_{SRT}= \frac{y_h}{h} \\ $$

Quando tudo já está feito e o programa já possui a matriz concatenada da transformação so SRU para o SRT, ele computa os vertices multiplicando ele pela matriz obtido. Lembrando que isso é feito para todos os objetos simultaneamente

# Computa os vertices com a matriz de transformação obtida anteriomente
for i in range(len(self.files_classes)):
    
    # Separa a mash do objeto (apenas por conveniência)
    mesh = copy.deepcopy(self.files_classes[i].mesh)
    
    mesh_mod = computacao_dos_vertices(mesh, self.Matrix_sru_srt)
    
    mesh_objetos_modificado.append(mesh_mod)

Depois de computado, parte-se para o teste de visibilidade de face, algo que testa se é possivel enxergar com os parametros já definidos (Não está sendo feito de forma correta e pode deformar o objeto)

 mesh_objetos_modificado_verificacao_faces = []
for mesh in mesh_objetos_modificado:
    
    # Verifica se as faces são visíveis
    mesh_objetos_modificado_verificacao_faces.append(verifica_faces_visiveis(mesh, vrp_x, vrp_y, vrp_z, ponto_focal_x, ponto_focal_y, ponto_focal_z))

Ao descobrir quais faces são visíveis, parte-se para o sombreamento, algo que é feito com base nos parametros de iluminação e de sombreamento, que são passados pelo usuário, também sendo feito em outro arquivo de forma separada

mesh_objeto_modificado_sombreamento = []
for i in range(len(mesh_objetos_modificado_verificacao_faces)):
    
    # Aplica uma copia da mesh modificada
    mesh_objeto_sombreamento = copy.deepcopy(mesh_objetos_modificado_verificacao_faces[i])
    
    print(mesh_objeto_sombreamento)
    
    # Percorre todas as faces da mesh
    for fh in mesh_objetos_modificado[i].faces():
        
        mesh_objeto_sombreamento = aplicacao_sombreamento(mesh_objetos_modificado[i], fh, 
                                                        vrp_x, vrp_y, vrp_z,
                                                        luz_ambiente_Ila_r, luz_ambiente_Ila_g, luz_ambiente_Ila_b,
                                                        luz_pontual_Il_r, luz_pontual_Il_g, luz_pontual_Il_b,
                                                        coordenadas_fonte_luz_x, coordenadas_fonte_luz_y, coordenadas_fonte_luz_z,
                                                        sombreamento_Ka_r, sombreamento_Ka_g, sombreamento_Ka_b,
                                                        sombreamento_Kd_r, sombreamento_Kd_g, sombreamento_Kd_b,
                                                        sombreamento_Ks_r, sombreamento_Ks_g, sombreamento_Ks_b, n)
                                
    # Adiciona a mesh modificada com o sombreamento constante
    mesh_objeto_modificado_sombreamento.append(mesh_objeto_sombreamento)
  • Para realizar o calculo da Luz Ambiente

$$ I_{a} = K_{a} \cdot I_{la} $$

  • Para realizar o calculo da Luz Difusa

$$ I_{d} = K_{d} \cdot I_{l} \cdot (\hat{N} \cdot \hat{L}) \\ \vec{L} = \vec{L} - CENTROIDE $$

  • Para realizar o calculo da Luz Especular

$$ I_{s} = K_{s} \cdot I_{l} \cdot (\hat{R} \cdot \hat{V})^{n} \\ \hat{R} = 2(\hat{N} \cdot \hat{L})\hat{N} - \hat{L} $$

Com tudo isso realizado, é dado o update no canvas e é rederizado o objeto com seus vertices modificados, faces visíveis e com o sombreamento constante, podendo ser manipulado novamente com as suas transformações

Como usar o aplicativo

Wireframe

Para desenhar o objeto 2D, basta apenas clicar no grid com botão direito do mouse, isso ira desenhar as retas e formando um objeto em tela, tendo no mínimo 2 pontos para revolucionar. Com ele desenhando, clica-se q para salvar o objeto na fila de classes indicada para ele

Renderização

Dentro do aplicativo de fato, é possivel realizar alguns comandos. Quando o usuário parametrizar tudo que deseja, clica-se o botão F2 para rederizar o pipeline e sombreamento.

Botões extras

  • F1 - Printa os valores armazenados no terminal
  • F9 - Mostra num gráfico do matplotlib o sólido(s) gerado(s)
  • F10 - Retira os gráficos da tela
  • F12 - Fecha o aplicativo (também aplicado no wireframe)
  • U - Atualiza o canvas depois de realizar as transformações
  • R - Reseta o objeto para antes da parametrização
  • ←, →, ↑, ↓ - Movimenta o objeto no canvas
  • w - Rotação no eixo x (+)
  • s - Rotação no eixo x (-)
  • a - Rotação no eixo y (+)
  • d - Rotação no eixo y (-)
  • q - Rotação no eixo z (+)
  • e - Rotação no eixo z (-)
  • z - Escala o objeto (+)
  • x - Escala o objeto (-)

Para as movimentações dos objetos, é possivel selecionar qual objeto quer que se altere, basta escolhe-lo no menu. Ele irá seleciona-lo previamente possibilitando a movimentação.

Como executar

Para executar o aplicativo, basta apenas rodar o arquivo main.py que está na raiz do projeto. Para isso, é necessário ter instalado as bibliotecas customtkinter e openmesh. Você pode apenas dar o comando ./atualizacao.bash mas apenas no linux, para instalar as dependências.

Arquivos extras

  • Planilha.xlsx - Planilha com os valores de parametrização (disponibilizado pelo professor da disciplina)
  • atualizacao.bash - Script para instalar as dependências do projeto
  • Trabalho 2 - CG 2023 - PDF com a descrição do trabalho

Problemas que o software possui

  • Seleção dos pontos no wireframe é feito corretamente e sua revolução também, mas quando aplicado as transformações junto com a ocultação de faces, não é feito de forma correta
  • Parametrização do valor n não está sendo feito corretamente, no calculo é gerado um vetor quando se eleva (potencia), por isso por padrão está 2.15
  • Quanto mais objeto e faces que eles possuirem, o software pode ficar lento, pois não foi feito a otimização do código e o tkinter tem várias limitações em relação a mostrar objetos que não sejam simples e que exijam muitos cálculos, transformações e renderizações em tempo de execução

Requisitos mínimos de hardware

  • Processador: Intel Core i3 ou superior
  • Memória RAM: 4GB ou superior
  • Espaço em disco: 1GB ou superior
  • Sistema operacional: Windows 10 | 11 ou Linux

Referências