Skip to content

Algoritmo de renderização de polígonos 3D, utilizando a linguagem de programação Python e a biblioteca tkinter. Primeiramente foi realizado a coleta dos pontos num ambiente 2D chamado de Wireframe, posteriormente é realizado a revolução do objeto em torno de um eixo, gerando um objeto em 3D, posteriormente sendo plotado em tela no canvas

License

Notifications You must be signed in to change notification settings

gabrielmazz/software-render

Repository files navigation

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

About

Algoritmo de renderização de polígonos 3D, utilizando a linguagem de programação Python e a biblioteca tkinter. Primeiramente foi realizado a coleta dos pontos num ambiente 2D chamado de Wireframe, posteriormente é realizado a revolução do objeto em torno de um eixo, gerando um objeto em 3D, posteriormente sendo plotado em tela no canvas

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published