Explorando o uso de texturas no OpenGL com Python.
O trabalho tem como objetivo fazer o uso da biblioteca pyopenGL para a criação de um sólido de nossa escolha (no nossa caso uma nave) para realizar as operações de translação, rotação e escalonamento
As orientações estão divididas nos seguintes tópicos:
- Mover nave nos eixos (X,Y);
- Rotaciona a nave nos eixos (X,Y);
- Rotaciona nave no eixo Z;
- Move câmera cima/baixo;
- Move câmera esquerda/direita;
- Move câmera frente/trás;
- Aumenta/diminui escala da nave;
O projeto possui o arquivo -main.py que é responsável por inicializar a aplicação, em conjunto com um pseudo MVC, composto pelos arquivos view.py (responsável por renderizar os componentes, sendo eles o ambiente e a nave), controller.py (responsável por receber as teclas pressionadas do teclado e aplicar as transformações ao sólido) e por fim o model.py (onde é definido 2 classes, uma que representa a nave e outra que representa o chão)
-
A nave é uma matriz que forma o polígono de uma pirâmide de base triangular e as asas são dois triângulos planos;
-
Para evitar a definição de várias matrizes para cada triângulo, apenas uma matriz estrutural principal é utilizada e os índices são utilizados para construir o polígono com triângulos;
-
As linhas também foram adicionadas com largura maior para ressaltar as arestas;
-
O processo de renderização apenas da nave é:
- Definir a geometria;
def create_geometry(self): # Pontos no espaço self.vertices = [ [0.0, 1.0, 0.0], # Topo [-1.0, -1.0, 1.0], # Frente esquerda [1.0, -1.0, 1.0], # Frente direita [1.0, -1.0, -1.0], # Traseira direita [-1.0, -1.0, -1.0] # Traseira esquerda ] # Ligar os pontos self.indices = [ 0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 1, 1, 2, 3, 1, 3, 4 ] # Textura self.tex_coords = [ [0.5, 1.0], # Topo [0.0, 0.0], # Frente esquerda [1.0, 0.0], # Frente direita [1.0, 1.0], # Traseira direita [0.0, 1.0] # Traseira esquerda ] # Linhas self.edges = [ [0, 1], [0, 2], [0, 3], [0, 4], [1, 2], [2, 3], [3, 4], [4, 1] ]
- Criar instância da nave;
- Renderizar na View (pirâmide, textura e linhas).
def render_spacecraft(self): glBindTexture(GL_TEXTURE_2D, self.spacecraft_texture_id) glPushMatrix() glTranslatef(*self.spacecraft.position) glRotatef(self.spacecraft.rotation[0], 1.0, 0.0, 0.0) glRotatef(self.spacecraft.rotation[1], 0.0, 1.0, 0.0) glRotatef(self.spacecraft.rotation[2], 0.0, 0.0, 1.0) glScalef(*self.spacecraft.scale) glBegin(GL_TRIANGLES) for i in range(0, len(self.spacecraft.indices), 3): for j in range(3): glTexCoord2f(*self.spacecraft.tex_coords[self.spacecraft.indices[i + j]]) vertex = self.spacecraft.vertices[self.spacecraft.indices[i + j]] glVertex3f(*vertex) glEnd() glLineWidth(4.0) glColor3f(1.0, 0.0, 0.0) glBegin(GL_LINES) for edge in self.spacecraft.edges: for vertex in edge: glVertex3f(*self.spacecraft.vertices[vertex]) glEnd() glColor3f(1.0, 1.0, 1.0) glPopMatrix()
-
A aplicação de textura envolve o uso de coordenadas de textura (UV mapping), que mapeiam uma imagem de textura aos vértices do objeto 3D;
-
Cada face do triângulo recebe a mesma textura;
-
As transformações aplicadas à nave (translação, rotação e escala) são aplicadas à matriz de modelagem. Em OpenGL, essas transformações afetam todas as operações de desenho subsequentes, incluindo as coordenadas de vértice e as coordenadas de textura.
-
É feito um bind da textura com
glBindTexture(GL_TEXTURE_2D, self.spacecraft_texture_id)
, vale ressaltar que com o bind das cordenadas de textura 2D as operações deglTranslatef
,glRotatef
eglScalef
também serão aplicadas para textura conforme texto supracitado. -
O processo de renderização da nave com textura é:
- Renderização padrão da nave;
- Bind de textura;
- Definir as matrizes de tranformação;
glPushMatrix() glTranslatef(*self.spacecraft.position) glRotatef(self.spacecraft.rotation[0], 1.0, 0.0, 0.0) glRotatef(self.spacecraft.rotation[1], 0.0, 1.0, 0.0) glRotatef(self.spacecraft.rotation[2], 0.0, 0.0, 1.0) glScalef(*self.spacecraft.scale)
- Renderizar com as coordenadas de textura
glBegin(GL_TRIANGLES) for i in range(0, len(self.spacecraft.indices), 3): for j in range(3): glTexCoord2f(*self.spacecraft.tex_coords[self.spacecraft.indices[i + j]]) vertex = self.spacecraft.vertices[self.spacecraft.indices[i + j]] glVertex3f(*vertex) glEnd()
-
O chão consiste uma matriz que forma um quadrado plano a partir de dois triângulos;
-
O processo de renderização do chão é:
- Definir a geometria;
class Ground: def __init__(self): # Vértices no espaço self.vertices = np.array([ [-40.0, -8.0, -40.0], [40.0, -8.0, -40.0], [40.0, -8.0, 40.0], [-40.0, -8.0, 40.0] ], dtype=np.float32) # Indices dos vértices para ligar os pontos e formar os triângulos self.indices = np.array([ 0, 1, 2, 2, 3, 0 ], dtype=np.uint32) # Textura self.tex_coords = np.array([ [0.0, 0.0], ])
- Criar instância do chão;
- Renderizar na View (plano e textura).
def render_ground(self): glBindTexture(GL_TEXTURE_2D, self.ground_texture_id) glPushMatrix() glBegin(GL_QUADS) for i, vertex in enumerate(self.ground.vertices): glTexCoord2f((i % 2) + self.ground_texture_offset, (i // 2) + self.ground_texture_offset) glVertex3f(*vertex) glEnd() glPopMatrix()
-
A textura do chão é formada a partir da imagem
space.png
, que é carregada e aplicada objeto -
É feito um bind da textura com
glBindTexture(GL_TEXTURE_2D, self.ground_texture_id)
-
O processo de renderização do chão com textura é:
- Renderização padrão do chão;
- Bind de textura;
- Renderizar com as coordenadas de textura
glPushMatrix() glBegin(GL_QUADS) for i, vertex in enumerate(self.ground.vertices): glTexCoord2f((i % 2) + self.ground_texture_offset, (i // 2) + self.ground_texture_offset) glVertex3f(*vertex) glEnd() glPopMatrix()
O código controlller.py é responsável pela movimentação da nave. através da classe Controller, o método process_input() é responsável por fazer todas as tranformações de escala, rotação e translação do modelo.
- W / S / A / D: Movimentam o modelo para cima, baixo, esquerda e direita, respectivamente, alterando suas coordenadas de transladação no plano XY.
- Setas Direcionais (↑ / ↓ / ← / →): Rotacionam o modelo ao redor dos eixos X e Y. As teclas de setas para cima e para baixo controlam a rotação em torno do eixo X, enquanto as teclas para esquerda e direita controlam a rotação em torno do eixo Y.
- Q / E: Rotacionam o modelo ao redor do eixo Z, com a tecla Q para rotação no sentido anti-horário e a tecla E para rotação no sentido horário.
- I / K / J / L / U / O: Movem a posição da câmera. As teclas I e K controlam o movimento vertical da câmera, J e L controlam o movimento horizontal, e U e O controlam o movimento na profundidade (zoom) da câmera.
- = (igual) / - (traço): Aumentam e diminuem a escala do modelo. A tecla = aumenta a escala em todas as direções (x, y, z), enquanto a tecla - diminui a escala, mantendo um valor mínimo de escala de 0.1 para cada eixo.
Este controlador permite uma interação dinâmica com a cena 3D, oferecendo controle sobre o modelo e a visualização através da câmera, facilitando a navegação e manipulação do ambiente virtual criado com OpenGL.
O código do controller é quem define que ações serão feitas com base na tecla que for pressionada, centralizando todas as ações,assim chamando as funções de rotação, translação ou escala no model ou alterando a posição da câmera na view.
As funções abaixo são definidas no model e são responsáveis pela escala, translação e rotação do modelo.
def translate(self, dx, dy, dz):
self.position += np.array([dx, dy, dz], dtype=np.float32)
def rotate(self, angle, axis):
self.rotation += np.array(angle) * np.array(axis)
def scale(self, sx, sy, sz):
self.scale *= np.array([sx, sy, sz], dtype=np.float32)
Para a movimentaçã oda câmera, a variavel camera_position é alterada.
class View:
def __init__(self, spacecraft, ground, spacecraft_texture_path, ground_texture_path):
self.spacecraft = spacecraft
self.ground = ground
self.spacecraft_texture_path = spacecraft_texture_path
self.ground_texture_path = ground_texture_path
self.camera_position = [0.0, 2.0, 25.0]
self.camera_target = [0.0, 0.0, 0.0]
self.camera_up = [0.0, 1.0, 0.0]
self.spacecraft_texture_id = None
self.ground_texture_id = None
self.ground_texture_offset = 0.0
Para fazer a animação do chão, foi se utilizado uma estratégia utilizando o tempo, conforme o tempo passa a textura é renderizada em uma posição, dessa maneira dando a impressao de que o chão, no caso as estrelas, estão se movendo.
A função update_ground_texture_offset(self, delta_time)
é responsável por atualizar o deslocamento da textura do chão ao longo do tempo. Ela recebe delta_time
, que representa o tempo decorrido desde a última atualização, e utiliza esse valor para calcular o novo deslocamento da textura:
def update_ground_texture_offset(self, delta_time):
self.ground_texture_offset += delta_time * 0.01 # velocidade animação do chão
A variável self.ground_texture_offset armazena a posição atual da textura do chão. Multiplicando delta_time por 0.01, a função define a velocidade da animação do chão.
A função render_ground(self) desenha o chão na cena utilizando OpenGL. Ela vincula a textura do chão (self.ground_texture_id) e utiliza self.ground_texture_offset para aplicar o deslocamento na textura:
def render_ground(self):
glBindTexture(GL_TEXTURE_2D, self.ground_texture_id)
glPushMatrix()
glBegin(GL_QUADS)
for i, vertex in enumerate(self.ground.vertices):
glTexCoord2f((i % 2) + self.ground_texture_offset, (i // 2) + self.ground_texture_offset)
glVertex3f(*vertex)
glEnd()
glPopMatrix()
glBindTexture(GL_TEXTURE_2D, self.ground_texture_id)
: Vincula a textura do chão para uso.
glTexCoord2f((i % 2) + self.ground_texture_offset, (i // 2) + self.ground_texture_offset)
: Define as coordenadas de textura para cada vértice do chão, aplicando o deslocamento (self.ground_texture_offset) para criar a ilusão de movimento da textura.
A função render(self) é o ponto de entrada para renderizar a cena completa. Ela é chamada repetidamente para atualizar e desenhar todos os elementos na janela OpenGL:
def render(self):
current_time = time.time()
if not hasattr(self, 'last_time'):
self.last_time = current_time
delta_time = current_time - self.last_time
self.last_time = current_time
self.update_ground_texture_offset(delta_time)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
gluLookAt(
self.camera_position[0], self.camera_position[1], self.camera_position[2],
self.camera_target[0], self.camera_target[1], self.camera_target[2],
self.camera_up[0], self.camera_up[1], self.camera_up[2]
)
self.render_ground()
self.render_spacecraft()
self.render_commands()
glfw.swap_buffers(self.window)
self.update_ground_texture_offset(delta_time)
: Chama a função de atualização para mover a textura do chão com base no tempo decorrido desde o último quadro (delta_time).
self.render_ground()
: Renderiza o chão na posição atualizada, aplicando o deslocamento da textura conforme calculado em update_ground_texture_offset.
glfw.swap_buffers(self.window)
: Troca os buffers para exibir a cena renderizada na janela.
Essas funções trabalham juntas para criar uma animação contínua e fluida da textura do chão, proporcionando um efeito visual dinâmico à cena 3D renderizada com OpenGL.
# Clone o repo
git clone https://github.com/MatMB115/texture-spaceship-opengl.git
# Acessar path
cd texture-spaceship-opengl
# Crie o env python
python -m venv nave_env
# Ativar o enviroment
source nave_env/bin/activate
# Instale as depedências
pip install -r requirements.txt
# Executar o projeto
python main.py
# Desativar o env
deactivate
O ponto de início deste projeto é um enviroment python, as dependências utilizadas estão presentes no requirements.txt.
glfw==2.7.0
numpy==1.26.4
pillow==10.3.0
PyOpenGL==3.1.7
Visual Studio Code 1.89.1
Bruno Vercelli 🧑💻 |
Gabriel Ciriaco 🧑💻 |
Luiz Fernando 🧑💻 |
Matheus Martins 🧑💻 |